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.

ClientWhereNeedsCovers
Swift HeraldClientThe 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.
Pythonclients/python/herald.pyPython 3 and its standard library.Notifications, apps and History.
Nodeclients/node/herald.jsNode 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:

Swift
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.

Minimal exampleSwift
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:

Swift
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 2xx answer means "the action happened", and Herald closes the banner.
  • When your action can fail, create the server with HeraldCallbackServer(statusHandler:) and return 200 only after the action succeeded.
  • A 4xx is final. 408, 429 and 5xx make 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.

CaseWhen
.notRunningThere is no token or port file, or nothing answers on the port.
.unauthorizedHerald answered 401: the token is wrong.
.server(status:message:)Herald answered with another error. message is the error text of the reply.
.invalidResponseThe reply was not what the client expected.

Functions

FunctionWhat it doesEndpoint
isAvailableTells 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 /v1/dismissAll
dismissAll(app:group:)Closes the banners of one stack.POST /v1/dismissAll
stacks(app:)Lists the stacks on screen.GET /v1/stacks
history(app:limit:)Returns stored notifications, newest first.GET /v1/history
quietHours()Returns the quiet-hours schedule and what is silenced now.GET /v1/settings/quiet-hours
setQuietHours(_:)Changes the schedule or starts a one-off quiet period.PUT /v1/settings/quiet-hours
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 /v1/manifest
templates(app:)Lists saved templates.GET /v1/templates
template(app:name:)Returns one template, including the built-in builtin.* ones.GET /v1/templates
putTemplate(_:)Saves a template.PUT /v1/templates
deleteTemplate(app:name:)Deletes a template.DELETE /v1/templates
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:template:select:width:height:)Draws the Designer window offscreen and returns the PNG bytes.GET /v1/designer/snapshot
call(_:_:query:body:timeout:)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.

Edit this page on GitHub

Esc
Getting started
Guides