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:

  1. 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.
  2. It shows a banner, plays the app's sound, and speaks the text if you asked for speech.
  3. It stores the notification in History, where it stays after the banner is gone.
  4. It keeps the banner until the user closes it, a button is pressed, a timeout ends 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.

One notification as a banner: the title, the subtitle, the body, the time it arrived and the close button.
A Herald banner with a title, a subtitle, two lines of body text, the time and a close button

One notification as a banner: the title, the subtitle, the body, the time it arrived and the close button.

What happens when the user clicks a banner is described in How banners behave.

Endpoints#

EndpointPurpose
POST /v1/notifyShow a notification.
POST /v1/speakSay text aloud with no banner.
POST /v1/dismissClose one banner.
POST /v1/dismissAllClose every banner of an app, or one stack.
POST /v1/snoozeHide a banner and bring it back later.
POST /v1/unsnoozeBring a snoozed banner back now.
POST /v1/composeOpen 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.

NameInTypeRequiredDescription
appbodystringrequiredThe id of the app that is sending.
titlebodystringrequiredThe headline. Optional when template names a template that sets a title.
idbodystringoptionalYour id for this notification. Herald generates one when you omit it.
subtitlebodystringoptionalA second line under the title.
bodybodystringoptionalThe main text. Markdown links such as [Open](https://example.com) work.
imagebodystringoptionalA picture: a file path, an https URL or a data: URI.
urlbodystringoptionalA link the banner opens when clicked. Only http, https and mailto open.
templatebodystringoptionalThe name of a template saved for this app. Default: the manifest's defaultTemplate.
metadatabodyobjectoptionalExtra values for template tokens. Stored in History.
groupbodystringoptionalA key for stacking: banners of one app with the same group stack together.

Fields that control how the banner behaves:

NameInTypeRequiredDescription
persistentbodybooleanoptionaltrue keeps the banner until it is dismissed. Default: the app's setting.
timeoutbodynumberoptionalSeconds until the banner closes itself. Hovering pauses the countdown.
soundbodystringoptionaldefault, a system sound name such as Glass, a file path, or none.
prioritybodystringoptionallow, normal, high or urgent.
snoozebodybooleanoptionaltrue adds a clock menu to the banner for snoozing it.
reminderbodyobjectoptionalAdds an Add to Reminders button. See Reminder object.

Fields that add buttons:

NameInTypeRequiredDescription
buttonsbodyarrayoptionalUp to 8 buttons. See Button object.
actionIdsbodyarrayoptionalIds of actions the app's manifest declares, used when you send no buttons.
followUpbodyobjectoptionalOne action to run if the banner goes unanswered. See Follow-up object.

Fields that add speech (explained in the voice reference):

NameInTypeRequiredDescription
speakbodyboolean or objectoptionaltrue speaks the title and body. An object sets the text, voice, speed and language.
audiobodystringoptionalA recorded voice message to play: a file path, an https URL or a data: URI.
presentationbodystringoptionalbanner, voice (speech only, no banner) or both. Default banner.

Fields for the built-in layouts, used only when no template applies:

NameInTypeRequiredDescription
layoutbodystringoptionalimageLeft, imageRight, hero or compact. Default imageLeft.
accentColorbodystringoptionalA hex colour such as #34C759.
showSubtitlebodybooleanoptionalfalse hides the subtitle.
showBodybodybooleanoptionalfalse hides the body.
showTimestampbodybooleanoptionalfalse hides the time.
maxBodyLinesbodyintegeroptionalThe number of body lines shown before the text is cut.
Example requestshell
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"}]
  }'
Example responseJSON
{"ok": true, "id": "bid-42"}

Response fields

FieldTypeDescription
okbooleanAlways true on success.
idstringThe notification id: yours, or the one Herald generated.

Errors

StatusWhen
400app is missing, or there is no title and no template that supplies one.
400A speak value is invalid: invalid voice, invalid lang, or a speed outside 0.5 to 2.0.
413A field is over its limit. The message names the field. See Limits.
429The 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 inside metadata.
  • An unknown template name is not an error while the notification has its own title. Herald shows it with the built-in layout.
  • An image is 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.

FieldTypeRequiredDescription
labelstringrequiredThe text on the button.
stylestringoptionaldefault, destructive or cancel.
urlstringoptionalOpens this link.
commandstringoptionalRuns this shell command. The user must allow commands for the app first.
scriptstringoptionalRuns 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.
shortcutstringoptionalRuns this Apple Shortcut. The same permission applies.
inputstringoptionalWith shortcut or script: the text handed over, with {tokens} filled. Default: the notification as JSON.
callbackobjectoptionalPosts to your callback URL. Fields: url and payload, both optional.
openAppobjectoptionalBrings an app to the front. Fields: bundleId or path; empty means the sender.
replyobjectoptionalShows a text field in the banner. Fields: placeholder, callback, voice.
JSON
{
  "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"}}
  ]
}
JSON
{
  "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.

FieldTypeRequiredDescription
afternumber or stringrequiredSeconds from 5 to 604800, or "90s", "10m", "2h". Not needed when enabled is false.
actionRefstringoptionalThe id, or label, of one of this notification's buttons.
actionobjectoptionalAn action written inline, as a button with an id and a label.
enabledbooleanoptionalfalse 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.

JSON
{
  "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 timeout that is not longer than after means 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#

FieldTypeRequiredDescription
titlestringoptionalThe reminder's title. Default: the notification's title.
duestringoptionalWhen it is due, as an ISO 8601 date such as 2026-10-02T09:00:00-04:00.

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

NameInTypeRequiredDescription
appbodystringrequiredThe id of the app that is speaking.
textbodystringrequiredWhat to say. At most 2000 characters are spoken.
voicebodystringoptionalA voice id such as af_bella. Default: the app's voice, else the engine default.
speedbodynumberoptionalFrom 0.5 to 2.0. Default 1.0.
langbodystringoptionalA language code such as en-us.
idbodystringoptionalYour id for the History entry.
Example requestshell
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}'
Example responseJSON
{"ok": true, "id": "7C1D0E52-3F0B-4B8E-9A55-0F3A1C0D2E11"}

Errors

StatusWhen
400app or text is missing or empty.
400voice, 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

NameInTypeRequiredDescription
appbodystringrequiredThe app id the notification was sent with.
idbodystringrequiredThe notification id.
Example requestshell
curl -s -X POST "$HERALD/v1/dismiss" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"app": "example.bidbot", "id": "bid-42"}'
Example responseJSON
{"ok": true}

Errors

StatusWhen
400app 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

NameInTypeRequiredDescription
appbodystringoptionalClose only this app's banners. Omit it to close every banner of every app.
groupbodystringoptionalClose only the stack with this group. Requires app.
Example requestshell
curl -s -X POST "$HERALD/v1/dismissAll" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"app": "example.bidbot", "group": "acme"}'
Example responseJSON
{"ok": true}

Errors

StatusWhen
400group 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

NameInTypeRequiredDescription
appbodystringrequiredThe app id the notification was sent with.
idbodystringrequiredThe notification id.
minutesbodynumberrequiredHow long to hide it. More than 0 and at most 43200 (30 days). Fractions are allowed.
Example requestshell
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}'
Example responseJSON
{"ok": true, "until": "2026-10-02T13:15:00.000Z"}

Response fields

FieldTypeDescription
okbooleanAlways true on success.
untilstringWhen the banner comes back, as an ISO 8601 date.

Errors

StatusWhen
400app, id or minutes is missing, or minutes is out of range.
400The notification was already dismissed.
404No notification with this app and id exists.

Notes

  • Sending a new notification with the same id cancels 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

NameInTypeRequiredDescription
appbodystringrequiredThe app id the notification was sent with.
idbodystringrequiredThe notification id.
Example requestshell
curl -s -X POST "$HERALD/v1/unsnooze" \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"app": "example.bidbot", "id": "bid-42"}'
Example responseJSON
{"ok": true}

Errors

StatusWhen
400app 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.

Example requestshell
curl -s -X POST "$HERALD/v1/compose" -H "Authorization: Bearer $TOKEN"
Example responseJSON
{"ok": true}

Edit this page on GitHub

Esc
Getting started
Guides