Notifications
These endpoints put a notification on screen and take it away again: show a banner, speak without a banner,
dismiss, and snooze. This is the part of the HTTP API most integrations need, and often the only
part. The examples use the $HERALD and $TOKEN variables from Connect.
How a notification works#
A notification is one JSON object sent by an app. The app is identified by an id you choose, such as
example.bidbot. You do not have to register the app first: Herald registers an unknown id the first time it
sends something.
Herald does four things with a notification:
- It applies a template if the notification names one, or if the app's manifest sets a default. The template decides the layout. Without one, Herald uses a built-in layout.
- It shows a banner, plays the app's sound, and speaks the text if you asked for speech.
- It stores the notification in History, where it stays after the banner is gone.
- It keeps the banner until the user closes it, a button is pressed, a
timeoutends it, or you dismiss it.
Every notification has an id. If you send one, you can use it later to replace, dismiss or snooze that
banner. If you leave it out, Herald generates one and returns it.
What happens when the user clicks a banner is described in How banners behave.
Endpoints#
| Endpoint | Purpose |
|---|---|
POST /v1/notify | Show a notification. |
POST /v1/speak | Say text aloud with no banner. |
POST /v1/dismiss | Close one banner. |
POST / | Close every banner of an app, or one stack. |
POST /v1/snooze | Hide a banner and bring it back later. |
POST /v1/unsnooze | Bring a snoozed banner back now. |
POST /v1/compose | Open the Composer window. |
POST /v1 /notify#
Shows a notification as a banner and stores it in History. Sending the same id again replaces the banner
that is on screen in place, which is how you update a progress or count without stacking new banners.
Request
Only app is required, plus title unless the template supplies one. The fields are grouped below; all of
them go in the JSON body.
| Name | In | Type | Required | Description |
|---|---|---|---|---|
app | body | string | required | The id of the app that is sending. |
title | body | string | required | The headline. Optional when template names a template that sets a title. |
id | body | string | optional | Your id for this notification. Herald generates one when you omit it. |
subtitle | body | string | optional | A second line under the title. |
body | body | string | optional | The main text. Markdown links such as [Open](https:// work. |
image | body | string | optional | A picture: a file path, an https URL or a data: URI. |
url | body | string | optional | A link the banner opens when clicked. Only http, https and mailto open. |
template | body | string | optional | The name of a template saved for this app. Default: the manifest's defaultTemplate. |
metadata | body | object | optional | Extra values for template tokens. Stored in History. |
group | body | string | optional | A key for stacking: banners of one app with the same group stack together. |
Fields that control how the banner behaves:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
persistent | body | boolean | optional | true keeps the banner until it is dismissed. Default: the app's setting. |
timeout | body | number | optional | Seconds until the banner closes itself. Hovering pauses the countdown. |
sound | body | string | optional | default, a system sound name such as Glass, a file path, or none. |
priority | body | string | optional | low, normal, high or urgent. |
snooze | body | boolean | optional | true adds a clock menu to the banner for snoozing it. |
reminder | body | object | optional | Adds an Add to Reminders button. See Reminder object. |
Fields that add buttons:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
buttons | body | array | optional | Up to 8 buttons. See Button object. |
actionIds | body | array | optional | Ids of actions the app's manifest declares, used when you send no buttons. |
followUp | body | object | optional | One action to run if the banner goes unanswered. See Follow-up object. |
Fields that add speech (explained in the voice reference):
| Name | In | Type | Required | Description |
|---|---|---|---|---|
speak | body | boolean or object | optional | true speaks the title and body. An object sets the text, voice, speed and language. |
audio | body | string | optional | A recorded voice message to play: a file path, an https URL or a data: URI. |
presentation | body | string | optional | banner, voice (speech only, no banner) or both. Default banner. |
Fields for the built-in layouts, used only when no template applies:
| Name | In | Type | Required | Description |
|---|---|---|---|---|
layout | body | string | optional | imageLeft, imageRight, hero or compact. Default imageLeft. |
accentColor | body | string | optional | A hex colour such as #34C759. |
showSubtitle | body | boolean | optional | false hides the subtitle. |
showBody | body | boolean | optional | false hides the body. |
showTimestamp | body | boolean | optional | false hides the time. |
maxBodyLines | body | integer | optional | The number of body lines shown before the text is cut. |
curl -s -X POST "$HERALD/v1/notify" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"app": "example.bidbot",
"id": "bid-42",
"title": "Bid accepted",
"subtitle": "Acme RFP",
"body": "Your bid of $4,200 was accepted.",
"url": "https://example.com/bids/42",
"buttons": [{"label": "Open", "url": "https://example.com/bids/42"}]
}'
{"ok": true, "id": "bid-42"}
Response fields
| Field | Type | Description |
|---|---|---|
ok | boolean | Always true on success. |
id | string | The notification id: yours, or the one Herald generated. |
Errors
| Status | When |
|---|---|
400 | app is missing, or there is no title and no template that supplies one. |
400 | A speak value is invalid: invalid voice, invalid lang, or a speed outside 0.5 to 2.0. |
413 | A field is over its limit. The message names the field. See Limits. |
429 | The app id is new and 200 apps are already registered. |
Notes
- A top-level key that is not in the tables above is treated as a template field and moved into
metadata. You can send"amount": "$4,200"at the top level and bind{amount}in a template. A top-level key wins over the same key insidemetadata. - An unknown
templatename is not an error while the notification has its own title. Herald shows it with the built-in layout. - An
imageis checked by its bytes, not its name. PNG, JPEG, GIF, WebP, HEIC, AVIF, TIFF and BMP are accepted, up to 10 MB. Anything else is ignored and the banner shows without a picture. - An explicit empty list,
"buttons": [], means no buttons, even when the manifest declares actions. priority: "urgent"breaks through quiet hours only for apps where the user allowed it.
Button object#
A button has a label and one thing it does. Give exactly one of url, command, script, shortcut, callback,
openApp or reply. The full behaviour of each kind is in the actions reference.
| Field | Type | Required | Description |
|---|---|---|---|
label | string | required | The text on the button. |
style | string | optional | default, destructive or cancel. |
url | string | optional | Opens this link. |
command | string | optional | Runs this shell command. The user must allow commands for the app first. |
script | string | optional | Runs this file from Herald's scripts folder. A plain file name, not a path. The user must allow the app to run commands, scripts and Shortcuts first. |
shortcut | string | optional | Runs this Apple Shortcut. The same permission applies. |
input | string | optional | With shortcut or script: the text handed over, with {tokens} filled. Default: the notification as JSON. |
callback | object | optional | Posts to your callback URL. Fields: url and payload, both optional. |
openApp | object | optional | Brings an app to the front. Fields: bundleId or path; empty means the sender. |
reply | object | optional | Shows a text field in the banner. Fields: placeholder, callback, voice. |
{
"app": "example.bidbot",
"title": "Counter-offer from Acme",
"body": "They offer $3,900. Accept?",
"buttons": [
{"label": "Accept", "callback": {"payload": {"decision": "accept"}}},
{"label": "Decline", "style": "destructive", "callback": {"payload": {"decision": "decline"}}},
{"label": "Reply", "reply": {"placeholder": "Message to Acme"}}
]
}
{
"app": "example.bidbot",
"title": "Bid accepted",
"buttons": [
{"label": "Forward", "shortcut": "Forward to phone", "input": "{title}"},
{"label": "Log", "script": "log.sh"}
]
}
The first time a script or shortcut button runs for an app, the banner asks the person to allow it. The question
names the script with its SHA-256 or the Shortcut with its input. snooze is not a button an app can send.
Follow-up object#
A follow-up runs one action when the banner is left unattended: nobody dismisses it, presses a button, replies or opens it for the time you set. Use it to forward a missed banner. How it works is in Follow-ups.
| Field | Type | Required | Description |
|---|---|---|---|
after | number or string | required | Seconds from 5 to 604800, or "90s", "10m", "2h". Not needed when enabled is false. |
actionRef | string | optional | The id, or label, of one of this notification's buttons. |
action | object | optional | An action written inline, as a button with an id and a label. |
enabled | boolean | optional | false switches it off. Default true. |
Give exactly one of actionRef and action. The kind must be shortcut, script, command or callback: a link,
an app, a reply, a snooze or a dismiss would take focus, need the person or hide the banner.
{
"app": "example.bidbot",
"id": "bid-42",
"title": "Bid accepted",
"persistent": true,
"followUp": {
"after": "10m",
"action": {"id": "fwd", "label": "Forward", "kind": "shortcut",
"shortcut": "Forward to phone", "input": "{title}"}
}
}
- A template's follow-up wins over the notification's, and the notification's over the manifest's.
- The timer starts when the banner is on screen. Dismissing, any button, a reply and opening cancel it. Snoozing restarts it when the banner comes back.
- A follow-up runs at most once for a notification.
- A follow-up that runs code runs under the app's permission. A notification cannot grant it.
- A
timeoutthat is not longer thanaftermeans the banner closes first and never follows up. - Herald keeps the timer in memory. Quitting Herald drops it.
- An app that reaches Herald through the cloud relay cannot send
followUp. Its owner adds one to the template.
Reminder object#
| Field | Type | Required | Description |
|---|---|---|---|
title | string | optional | The reminder's title. Default: the notification's title. |
due | string | optional | When it is due, as an ISO 8601 date such as 2026-. |
POST /v1 /speak#
Says text aloud and shows no banner. It is a shortcut for a notification with presentation: "voice", and the
text is stored in History like any other notification. Use it for a short spoken status when a banner would be
noise.
Request
| Name | In | Type | Required | Description |
|---|---|---|---|---|
app | body | string | required | The id of the app that is speaking. |
text | body | string | required | What to say. At most 2000 characters are spoken. |
voice | body | string | optional | A voice id such as af_bella. Default: the app's voice, else the engine default. |
speed | body | number | optional | From 0.5 to 2.0. Default 1.0. |
lang | body | string | optional | A language code such as en-us. |
id | body | string | optional | Your id for the History entry. |
curl -s -X POST "$HERALD/v1/speak" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"app": "example.bidbot", "text": "Deploy finished", "voice": "af_bella", "speed": 1.1}'
{"ok": true, "id": "7C1D0E52-3F0B-4B8E-9A55-0F3A1C0D2E11"}
Errors
| Status | When |
|---|---|
400 | app or text is missing or empty. |
400 | voice, lang or speed is invalid. |
Notes
- If speech cannot play (Herald is muted, the app's Speak switch is off, or the engine is off), the text is shown as a normal banner instead, so it is never lost.
- The History title is the first 80 characters of the text.
POST /v1 /dismiss#
Closes one banner. The notification stays in History. Use it when the event behind a banner is over, for example when the user handled it in your own app.
Request
| Name | In | Type | Required | Description |
|---|---|---|---|---|
app | body | string | required | The app id the notification was sent with. |
id | body | string | required | The notification id. |
curl -s -X POST "$HERALD/v1/dismiss" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"app": "example.bidbot", "id": "bid-42"}'
{"ok": true}
Errors
| Status | When |
|---|---|
400 | app or id is missing. |
Notes
- Dismissing an id that is not on screen is not an error. The reply is still
{"ok": true}.
POST /v1 /dismissAll#
Closes several banners at once. What it closes depends on the body.
Request
| Name | In | Type | Required | Description |
|---|---|---|---|---|
app | body | string | optional | Close only this app's banners. Omit it to close every banner of every app. |
group | body | string | optional | Close only the stack with this group. Requires app. |
curl -s -X POST "$HERALD/v1/dismissAll" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"app": "example.bidbot", "group": "acme"}'
{"ok": true}
Errors
| Status | When |
|---|---|
400 | group is sent without app. |
Notes
- An empty body closes every banner on screen.
- Dismissed notifications stay in History.
POST /v1 /snooze#
Hides a banner and shows it again, with the same id, after a number of minutes. A snooze survives a restart of Herald, and History marks the notification as snoozed while it waits.
Request
| Name | In | Type | Required | Description |
|---|---|---|---|---|
app | body | string | required | The app id the notification was sent with. |
id | body | string | required | The notification id. |
minutes | body | number | required | How long to hide it. More than 0 and at most 43200 (30 days). Fractions are allowed. |
curl -s -X POST "$HERALD/v1/snooze" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"app": "example.bidbot", "id": "bid-42", "minutes": 15}'
{"ok": true, "until": "2026-10-02T13:15:00.000Z"}
Response fields
| Field | Type | Description |
|---|---|---|
ok | boolean | Always true on success. |
until | string | When the banner comes back, as an ISO 8601 date. |
Errors
| Status | When |
|---|---|
400 | app, id or minutes is missing, or minutes is out of range. |
400 | The notification was already dismissed. |
404 | No notification with this app and id exists. |
Notes
- Sending a new notification with the same
idcancels a pending snooze. - The duration is elapsed time: one hour is one hour even across a daylight-saving change.
POST /v1 /unsnooze#
Cancels a snooze and shows the banner again at once. It plays no sound, because it is not a new alert.
Request
| Name | In | Type | Required | Description |
|---|---|---|---|---|
app | body | string | required | The app id the notification was sent with. |
id | body | string | required | The notification id. |
curl -s -X POST "$HERALD/v1/unsnooze" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"app": "example.bidbot", "id": "bid-42"}'
{"ok": true}
Errors
| Status | When |
|---|---|
400 | app or id is missing. |
POST /v1 /compose#
Opens Herald's Composer window, where a person fills in a notification by hand and sends it. It is what
herald compose calls. It takes no body and shows no banner.
Request
No parameters.
curl -s -X POST "$HERALD/v1/compose" -H "Authorization: Bearer $TOKEN"
{"ok": true}
Related#
- Send your first notification: a walk through the first
POST /v1/notify. - How banners behave: clicking, expanding, closing and timeouts.
- Templates: control the layout a notification is drawn with.
- Actions: buttons that call you back, run commands or take a reply.
- Voice reference: the
speak,audioandpresentationfields in full. - History API: read back what was sent.