Replies & callbacks
These endpoints let your program read what the user typed into a banner, and this page also documents the
request Herald sends to your server when a callback button is pressed. Together they are the two ways a
notification becomes a conversation. The examples use the $HERALD and $TOKEN variables from
Connect.
Two ways to hear back#
A banner can carry buttons, and the user's choice has to reach your program somehow. Herald offers two paths, and which one you use depends on whether your program can receive HTTP requests.
| Path | How it works | Use it when |
|---|---|---|
| Replies queue | The user types into a reply field. Herald stores the text, and you ask for it. | Your program cannot run a server: a script, an agent, a CLI tool. |
| Callback | The user presses a button. Herald sends an HTTP request to your server. | Your program runs a server and wants to be told at once. |
A reply comes from a button of kind reply, which swaps the banner's buttons for a text field. The text
is kept on the notification's History record and added to a queue for the app. The queue holds the 200 most
recent replies of each app.
Endpoints#
| Endpoint | Purpose |
|---|---|
GET /v1/replies | Read the replies waiting in the queue. |
GET / | Wait for the reply to one notification. |
GET /v1 /replies#
Returns the replies waiting in the queue, oldest first. Use it to poll for answers to several notifications at once.
Request
| Name | In | Type | Required | Description |
|---|---|---|---|---|
app | query | string | optional | Return only replies to this app's notifications. Default: every app. |
since | query | string | optional | Return only replies after this time: an ISO 8601 date or epoch seconds. |
consume | query | boolean | optional | true removes the returned replies from the queue. Default false. |
curl -s "$HERALD/v1/replies?app=example.bidbot&consume=true" -H "Authorization: Bearer $TOKEN"
{
"count": 1,
"replies": [
{
"notificationId": "bid-42",
"app": "example.bidbot",
"title": "Counter-offer from Acme",
"text": "Accept if they include delivery.",
"repliedAt": "2026-10-02T13:04:41.526Z"
}
]
}
Response fields
| Field | Type | Description |
|---|---|---|
count | integer | How many replies are in replies. |
replies[]. | string | The id of the notification that was answered. |
replies[].app | string | The app that sent that notification. |
replies[].title | string | The notification's title, so you can tell which question it was. |
replies[].text | string | What the user typed. |
replies[]. | string | When the user replied, as an ISO 8601 date. |
Errors
| Status | When |
|---|---|
400 | since is neither an ISO 8601 date nor a number. |
Notes
- Without
consume=truea reply stays in the queue and is returned again by the next request. - A reply removed from the queue is still on its History record.
GET /v1 /replies /wait#
Waits until one notification has been answered, then returns the reply. The request stays open for up to the timeout you give, so you do not have to poll. Use it right after sending a question.
Request
| Name | In | Type | Required | Description |
|---|---|---|---|---|
id | query | string | required | The id of the notification you are waiting on. |
app | query | string | optional | The app that sent it. Give it so that a reply already in History is found too. |
timeout | query | number | optional | Seconds to wait, from 1 to 300. Default 60. |
consume | query | boolean | optional | false leaves the reply in the queue. Default true. |
curl -s "$HERALD/v1/replies/wait?id=bid-42&app=example.bidbot&timeout=120" \
-H "Authorization: Bearer $TOKEN"
{
"replied": true,
"reply": {
"notificationId": "bid-42",
"app": "example.bidbot",
"title": "Counter-offer from Acme",
"text": "Accept if they include delivery.",
"repliedAt": "2026-10-02T13:04:41.526Z"
}
}
When the timeout passes with no reply, the status is still 200:
{"replied": false, "timedOut": true, "waitedSeconds": 120}
Response fields
| Field | Type | Description |
|---|---|---|
replied | boolean | true when the user answered. |
reply | object | The reply, in the shape of GET /v1/replies. Present when replied is true. |
timedOut | boolean | true when the wait ended without a reply. |
waitedSeconds | number | How long the request waited. Present when it timed out. |
Errors
| Status | When |
|---|---|
400 | id is missing. |
Notes
- A reply that already exists is returned at once.
- Set your HTTP client's own timeout above the
timeoutyou send, or the client gives up first.
Callback request#
This is not an endpoint you call. It is the request Herald sends to your server when the user presses a
button of kind callback, or sends a reply from a reply button that has a callback.
Herald posts to the url of the button's callback, or to the callbackURL the app registered with
POST /v1/register when the button names none.
Request
| Name | In | Type | Required | Description |
|---|---|---|---|---|
X-Herald-Attempt | header | string | required | 1 for the first attempt, 2 for the retry. |
notificationId | body | string | required | The id of the notification whose button was pressed. |
app | body | string | required | The app that sent the notification. |
action | body | string | required | The label of the button. |
payload | body | any | optional | The payload you attached to the button. |
event | body | string | optional | unattended when a follow-up sent the request. Absent for a pressed button. |
unattendedSeconds | body | number | optional | With unattended: how long the banner went unanswered. |
POST /herald HTTP/1.1
Host: 127.0.0.1:5123
Content-Type: application/json
X-Herald-Attempt: 1
{"notificationId": "bid-42", "app": "example.bidbot", "action": "Accept", "payload": {"decision": "accept"}}
Example response
Answer with any 2xx status once the action has happened. The body is ignored.
HTTP/1.1 204 No Content
Notes
- Herald adds two things to
payloadwhen they apply: the text of a reply aspayload.reply, and the values a template attached to the button aspayload.extra. - A
2xxanswer dismisses the banner. Any other outcome leaves the banner up and shows that the action failed. - Each attempt may take 5 seconds. Herald retries once after a network error or a
408,429or5xxstatus. UsenotificationIdandactionto ignore a repeat. - Redirects are never followed.
- A callback URL on this Mac (
127.0.0.1,localhost,::1) needs no approval. Any other host must be approved by the user the first time.
How callbacks fit with the other action kinds, and what the user is asked, is in the actions reference.
Related#
- Two-way notifications: build a banner that asks a question and act on the answer.
- Actions reference: every action kind, including
replyandcallback. - Notifications API: the button object.
- MCP tools for notifications:
get_repliesandwait_for_replyfor an agent.