Python & Node clients
Python#
clients/python/herald.py is one file that uses only the Python standard library. Copy it next to your script, or put
its folder on PYTHONPATH.
from herald import Herald
Herald().notify("example.bidbot", "Bid accepted")
notify(app, title, **fields) returns the notification id. Every extra keyword becomes a field of the notification.
Realistic example
Register the app, send a notification with buttons, update it, and read History. The second notify reuses the id, so
Herald replaces the banner instead of stacking a new one.
from herald import Herald, HeraldError, HeraldUnavailable
h = Herald()
try:
if h.is_available():
h.register("example.bidbot", appName="BidBot",
callbackURL="http://127.0.0.1:5123/herald", defaults={"sound": "Glass"})
h.notify("example.bidbot", "Counter-offer from Acme",
id="bid-42", body="They offer $3,900. Accept?", sound="Glass", snooze=True,
buttons=[{"label": "Accept", "callback": {"payload": {"decision": "accept"}}},
{"label": "Open", "url": "https://example.com/bids/42"}])
h.notify("example.bidbot", "Counter-offer from Acme", id="bid-42",
body="They now offer $4,100. Accept?")
for item in h.history("example.bidbot", limit=5):
print(item["id"], item["notification"]["title"])
h.dismiss("example.bidbot", "bid-42")
except HeraldUnavailable:
print("Herald is not running")
except HeraldError as e:
print("Herald refused the request:", e.status, e.message)
A callback button posts to the callbackURL you registered. A receiver needs only the standard library. Herald sends a JSON object and treats a 2xx answer as success. The fields of the object are in the callback request.
A receiver looks like this:
import json
from http.server import BaseHTTPRequestHandler, HTTPServer
class Callback(BaseHTTPRequestHandler):
def do_POST(self):
event = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
print(event["notificationId"], event["action"], event.get("payload"))
self.send_response(200)
self.end_headers()
HTTPServer(("127.0.0.1", 5123), Callback).serve_forever()
Second instance
The module reads the default support folder. For another Herald, pass the port and token:
Herald(port=48700, token=open("/tmp/herald-test/token").read().strip()).
Errors
Two exceptions matter:
HeraldUnavailablemeans Herald is not running. It is a subclass ofHeraldError.HeraldErrorcarriesstatusandmessagefor an error answer.
The client waits 10 seconds by default. Herald(timeout=...) changes it.
Functions
| Function | What it does | Endpoint |
|---|---|---|
is_available() | Returns True when Herald answers. | GET /v1/health |
health() | Returns ok, the version and the process id. | GET /v1/health |
notify(app, | Shows a notification and returns its id. | POST /v1/notify |
register(app, | Registers or updates an app. | POST /v1/register |
dismiss(app, id) | Closes one banner. | POST /v1/dismiss |
dismiss_all(app) | Closes every banner of an app. | POST / |
history(app, | Returns the stored notifications as a list. | GET /v1/history |
clear_history(app) | Deletes an app's History. | DELETE /v1/history |
apps() | Lists registered apps. | GET /v1/apps |
Node#
clients/node/herald.js is one file with no dependencies. It needs Node 18 or later, for the built-in fetch. It is a
CommonJS module that an ES module can also import.
const { Herald } = require('./herald');
await new Herald().notify('example.bidbot', 'Bid accepted');
notify(app, title, fields) returns the notification id. fields is an object of any other notification fields.
const { Herald, HeraldError, HeraldUnavailable } = require('./herald');
const h = new Herald();
try {
if (await h.isAvailable()) {
await h.register('example.bidbot', {
appName: 'BidBot', callbackURL: 'http://127.0.0.1:5123/herald', defaults: { sound: 'Glass' },
});
await h.notify('example.bidbot', 'Counter-offer from Acme', {
id: 'bid-42', body: 'They offer $3,900. Accept?', sound: 'Glass', snooze: true,
buttons: [
{ label: 'Accept', callback: { payload: { decision: 'accept' } } },
{ label: 'Open', url: 'https://example.com/bids/42' },
],
});
for (const item of await h.history('example.bidbot', 5)) console.log(item.id, item.notification.title);
await h.dismiss('example.bidbot', 'bid-42');
}
} catch (e) {
if (e instanceof HeraldUnavailable) console.error('Herald is not running');
else if (e instanceof HeraldError) console.error('Herald refused the request:', e.status, e.message);
else throw e;
}
Second instance
The module reads the default support folder. For another Herald, pass the port and token:
new Herald({ port: 48700, token: '...' }). The options are port, token and timeoutMs (default 10000).
Errors
Two errors matter:
HeraldUnavailablemeans Herald is not running. It extendsHeraldError.HeraldErrorcarriesstatusfor an error answer.
isAvailable() returns false when Herald answers with an error, and throws HeraldUnavailable when it does not answer at all.
Functions
| Function | What it does | Endpoint |
|---|---|---|
isAvailable() | Resolves true when Herald answers. | GET /v1/health |
health() | Resolves ok, the version and the process id. | GET /v1/health |
notify(app, | Shows a notification and resolves its id. | POST /v1/notify |
register(app, | 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 / |
history(app, | Resolves the stored notifications as an array. | GET /v1/history |
clearHistory(app) | Deletes an app's History. | DELETE /v1/history |
apps() | Resolves the registered apps. | GET /v1/apps |
Registering a manifest#
A manifest declares the fields your app sends, the actions it offers and any assets, so the Designer can show them with
sample values and a template can bind them. Register it once when your app starts. Saving a manifest for an app replaces
the one before it. Then send the fields at the top level of each notification. The format is in the
manifest reference; the route is
PUT /v1/manifest.
The Python and Node clients have no manifest function, so the examples call the route with the client's own address and
token. Swift has putManifest(_:).
Swift#
import HeraldClient
let manifest = try HeraldJSON.decoder().decode(HeraldManifest.self, from: Data("""
{"app": "example.bidbot", "appName": "BidBot", "version": 1,
"fields": [{"key": "title", "type": "text", "required": true, "sample": "Bid accepted"},
{"key": "amount", "type": "text", "sample": "$4,200"},
{"key": "url", "type": "url"}],
"actions": [{"id": "archive", "label": "Archive", "kind": "callback", "style": "destructive"}]}
""".utf8))
try await HeraldClient.shared.putManifest(manifest)
// Manifest fields go at the top level of the payload, the way an app sends them.
_ = try await HeraldClient.shared.notify(payload: .object([
"app": .string("example.bidbot"), "title": .string("Bid accepted"),
"amount": .string("$4,200"), "url": .string("https://example.com/bids/42"),
]))
Python#
import json
import urllib.request
from herald import Herald
h = Herald()
manifest = {
"app": "example.bidbot", "appName": "BidBot", "version": 1,
"fields": [{"key": "title", "type": "text", "required": True, "sample": "Bid accepted"},
{"key": "amount", "type": "text", "sample": "$4,200"},
{"key": "url", "type": "url"}],
"actions": [{"id": "archive", "label": "Archive", "kind": "callback", "style": "destructive"}],
}
request = urllib.request.Request(
h.base_url + "/v1/manifest", method="PUT", data=json.dumps(manifest).encode(),
headers={"Authorization": "Bearer " + h.token, "Content-Type": "application/json"})
urllib.request.urlopen(request).read()
h.notify("example.bidbot", "Bid accepted", amount="$4,200", url="https://example.com/bids/42")
Node#
const { Herald } = require('./herald');
const h = new Herald();
await fetch(h.baseUrl + '/v1/manifest', {
method: 'PUT',
headers: { Authorization: `Bearer ${h.token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({
app: 'example.bidbot', appName: 'BidBot', version: 1,
fields: [{ key: 'title', type: 'text', required: true, sample: 'Bid accepted' },
{ key: 'amount', type: 'text', sample: '$4,200' }],
actions: [{ id: 'archive', label: 'Archive', kind: 'callback' }],
}),
});
await h.notify('example.bidbot', 'Bid accepted', { amount: '$4,200' });
Shell#
D="$HOME/Library/Application Support/Herald"
curl -s -X PUT "http://127.0.0.1:$(cat "$D/port")/v1/manifest" \
-H "Authorization: Bearer $(cat "$D/token")" -H "Content-Type: application/json" \
-d @manifest.json
The command line tool has no command that saves a manifest. It can delete one, with
herald manifest delete. To read manifests back, use
GET /v1/manifests.