HeraldClient (Swift)
Herald is controlled over a local HTTP API, and three small clients wrap it so your code does not build requests by hand: a Swift package for macOS apps, a Python module and a Node module. Each one finds the running Herald, adds the bearer token, sends the request and raises a clear error when Herald is not running. For shell scripts there is also the command line tool, and for agents the MCP server.
All three clients read the port and token from Herald's support folder, ~/Library/Application Support/Herald/, the same
two files the API describes. They talk to 127.0.0.1 only. A client
function is a thin wrapper: the request and reply fields are documented once, in the endpoint it names.
| Client | Where | Needs | Covers |
|---|---|---|---|
Swift HeraldClient | The HeraldClient library product of this package. | macOS 13 or later. No dependencies. | Notifications, apps, History, stacks, quiet hours, manifests, templates, previews, and every other route. |
| Python | clients/ | Python 3 and its standard library. | Notifications, apps and History. |
| Node | clients/ | Node 18 or later. No dependencies. | Notifications, apps and History. |
The Python and Node clients cover what most integrations need: send a notification, dismiss it, read History. For a manifest, a template or a preview from those languages, call the HTTP route yourself, as Registering a manifest shows.
Swift#
HeraldClient is a Swift package library with no dependencies. It has typed models for notifications, buttons,
manifests and templates, an async client, and a small server that receives button callbacks. Use it from a macOS app
or a command line tool.
Add it
Add the package to your Package.swift and depend on the HeraldClient product:
dependencies: [
.package(url: "https://github.com/ivg-design/herald.git", branch: "main"),
],
targets: [
.target(name: "MyApp", dependencies: [
.product(name: "HeraldClient", package: "herald"),
]),
]
In an Xcode project, use File > Add Package Dependencies with the same address.
import HeraldClient
let herald = HeraldClient.shared
guard herald.isAvailable else { return } // a cheap check; false when Herald is not running
let id = try await herald.notify(HeraldNotification(app: "example.bidbot", title: "Bid accepted"))
notify returns the notification id. HeraldClient.shared reads the default support folder. isAvailable is true
when the token and port files exist and the health endpoint answers within a second.
Realistic example
Register the app with a callback address, send a notification with buttons, and receive the button presses:
import HeraldClient
let callbacks = HeraldCallbackServer(handler: { event in
// event.action is the label of the button; event.payload is the JSON you attached to it.
print("\(event.notificationId): \(event.action) \(String(describing: event.payload))")
})
try callbacks.start()
let herald = HeraldClient.shared
try await herald.register(HeraldAppRegistration(
app: "example.bidbot", appName: "BidBot",
callbackURL: callbacks.callbackURL,
defaults: HeraldAppDefaults(sound: "Glass")))
let id = try await herald.notify(HeraldNotification(
app: "example.bidbot", id: "bid-42", title: "Counter-offer from Acme",
body: "They offer $3,900. Accept?",
buttons: [
HeraldButton(label: "Accept", callback: HeraldCallback(payload: .object(["decision": .string("accept")]))),
HeraldButton(label: "Decline", style: "destructive",
callback: HeraldCallback(payload: .object(["decision": .string("decline")]))),
],
snooze: true))
let recent = try await herald.history(app: "example.bidbot", limit: 10)
try await herald.dismiss(app: "example.bidbot", id: id)
callbacks.stop()
HeraldCallbackServer listens on a loopback port that the system picks, so callbacks.callbackURL is only known after start(). See Actions for the callback request.
How Herald reads the answer to a callback:
- Any
2xxanswer means "the action happened", and Herald closes the banner. - When your action can fail, create the server with
HeraldCallbackServer(statusHandler:)and return200only after the action succeeded. - A
4xxis final.408,429and5xxmake Herald retry once. - Herald waits at most 5 seconds for the answer.
Second instance
HeraldClient(supportDirectory:port:token:) points a client at another Herald, such as the
second instance used for testing. The HERALD_SUPPORT_DIR
environment variable has the same effect on HeraldClient.shared.
Errors
Calls throw HeraldError.
| Case | When |
|---|---|
.notRunning | There is no token or port file, or nothing answers on the port. |
.unauthorized | Herald answered 401: the token is wrong. |
. | Herald answered with another error. message is the error text of the reply. |
.invalidResponse | The reply was not what the client expected. |
Functions
| Function | What it does | Endpoint |
|---|---|---|
isAvailable | Tells whether Herald answers, without throwing. | GET /v1/health |
health() | Returns ok, the app version and its process id. | GET /v1/health |
notify(_:) | Shows a notification and returns its id. | POST /v1/notify |
notify(payload:) | Sends a JSON payload exactly as given, so manifest fields stay at the top level. | POST /v1/notify |
speak(_:) | Says text aloud with no banner. | POST /v1/speak |
register(_:) | Registers or updates an app. | POST /v1/register |
dismiss(app:id:) | Closes one banner. | POST /v1/dismiss |
dismissAll(app:) | Closes every banner of an app. | POST / |
dismissAll(app: | Closes the banners of one stack. | POST / |
stacks(app:) | Lists the stacks on screen. | GET /v1/stacks |
history(app: | Returns stored notifications, newest first. | GET /v1/history |
quietHours() | Returns the quiet-hours schedule and what is silenced now. | GET / |
setQuietHours(_:) | Changes the schedule or starts a one-off quiet period. | PUT / |
apps() | Lists registered apps. | GET /v1/apps |
manifests() | Lists every manifest. | GET /v1/manifests |
manifest(app:) | Returns one app's manifest, or nil when it has none. | GET /v1/manifest |
putManifest(_:) | Saves an app's manifest. | PUT /v1/manifest |
deleteManifest(app: | Deletes an app's manifest. | DELETE / |
templates(app:) | Lists saved templates. | GET /v1/templates |
template(app: | Returns one template, including the built-in builtin.* ones. | GET /v1/templates |
putTemplate(_:) | Saves a template. | PUT /v1/templates |
deleteTemplate(app: | Deletes a template. | DELETE / |
components() | Returns the component schema. | GET /v1/components |
shortcuts() | Lists the installed Apple Shortcuts. | GET /v1/shortcuts |
preview(_:) | Draws a template offscreen and returns the PNG bytes. | POST /v1/preview |
designerSnapshot(app: | Draws the Designer window offscreen and returns the PNG bytes. | GET / |
call(_:_: | Sends any authenticated request and returns the JSON reply. Use it for the routes with no typed function. | Any route in the API reference. |
HeraldCallbackServer is the one part that is not a request: it is a listener for the callbacks Herald sends. See the
callback request.