API reference menu
Getting started
OverviewAuthentication and rate limitExamplesSending
Send a notificationPriority and repeatLinks, images and buttonsSourcesScheduled sendsWebhook templatesReceipts and replies
Receipts and callbacksRepliesTeams
Teams, roles and endpointsChannels and escalationWebhooks and emailAlert email and timelineTeam formsPlans, audit log and transferCompany sign‑in and SCIMThe web consoleAccount
Identity and moving phonesDevices, keys and historyThe Upgrade PassCompatibility
ntfy's publish APIProwl's APIAppendix
Endpoint indexErrors and status codesLimitsPriorities and fieldsGlossaryDestinations: webhooks and email
A channel always reaches its members' phones. A destination adds somewhere else: a webhook the team runs (to open a ticket, post in chat, start a script) or an email address (for the people who will not install an app, which is most small businesses receiving their website's enquiries). Both are fed from the same places the phones are, and each channel may have 10.
curl -X POST https://nowlark.com/v1/channels/chn_…/destinations -H "Authorization: Bearer nla_…" \
-H 'Content-Type: application/json' \
-d '{"kind":"webhook","target":"https://hooks.yourcompany.com/nowlark","events":["thread.created","thread.acknowledged"]}'
Events: thread.created (an alert arrived), thread.acknowledged (someone opened it, or a form
submission was claimed), thread.escalated (a later tier was notified), thread.unopened (nobody
took it: still unopened a set time after the last tier was notified) and thread.resolved (forms: a
submission was marked replied or archived). Default is the first three, and destinations made before
the last two existed keep the three; name the others in events to get them. A test event is sent
by the test endpoint.
Nobody took it. thread.unopened is sent once if, when the wait is over, nobody on the channel
has opened, taken or answered the alert. The wait starts when the last escalation tier is notified,
or, on a channel with no escalation, when the alert is sent. With repeat, that is the last tier of
the last repeat. The default is 900 seconds (15 minutes),
longer than a person's own 5-minute email so that each of them has had their chance first; set it per
channel with PATCH /v1/channels/{id} {"unopened_after": 600} (300, 600, 900, 1800, 3600 or 7200,
or null for the default).
Delivery. Each event is a row in an outbox, one per (destination, thread, event) for ever, so a
send retried after a crash does not deliver twice. A failure is retried after 30 s, 5 min, 30 min, 2 h
and 12 h, then given up on. A destination whose deliveries fail 20 times in a row is switched off
(disabled in the list, with the reason) so a dead endpoint stops costing a retry schedule per
alert; …/enable switches it back on. The acknowledged event is read from the members' copies by a
sweeper, so every way of opening one reaches it. Delivered within one scheduler tick (15 seconds).
Webhook body and signature. JSON: event, id (the thread), team and channel (id, name),
at, and for thread.created the source, title, body, url, priority and tags; for
thread.acknowledged the earliest acknowledged_at and acknowledged_by, the names members chose
for the team (never an account id); for thread.escalated the tier and notified; for
thread.unopened the source, title and waited (seconds since the alert was sent, not since the last tier); for
thread.resolved the source, title, status and by. Headers:
X-Nowlark-Event, X-Nowlark-Delivery (use it to deduplicate) and
X-Nowlark-Signature: t=1789665612,v1=5257a869e7ec…
where v1 is the hex HMAC-SHA256, keyed with the destination's secret (whsec_…, shown once,
when it is created), of <t>.<raw body>. Recompute it, compare in constant time, and reject a t
more than a few minutes old. The URL is held to every rule that applies to a callback_url: https
only, public addresses only, no redirects, 5 s timeout.
Email. Nothing is sent to an address until it has followed a link sent to it: adding one sends a
verification email (3 per address and 20 per team a day), and until it is confirmed nothing else is
queued. The link opens a page with a button, because mail scanners open every link; only the button
confirms. Every email carries a one-click unsubscribe (List-Unsubscribe with List-Unsubscribe-Post),
and an address that uses it is remembered, by hash, across every channel and team: it cannot be
added again and an admin cannot switch it back on. An address receives at most 60 emails an hour and a
team 500 a day; what exceeds that waits and is sent later, it is not lost. The subject is one line
whatever a sender put in the title, and only a plain address is accepted (no name, no list, no line
break). Email needs the operator to configure a relay; without one, adding an email destination is
409 email_not_configured.
Bounces. What the relay answers at send time decides what happens. An address it says does not
exist (an enhanced 5.1.x status, or 550, 551 or 553 with no enhanced status) is a hard bounce: the delivery fails at once and
is never retried, the address is suppressed for every channel and team, and every destination for it is
switched off with the reason (address bounced (550)). A 4xx or a network failure is soft and is
retried on the usual schedule. Any other 5xx (blocked as spam, mailbox full) is a refusal about the
relay or the content, not the address: no retry and no suppression, but it counts toward the
destination's run of failures, so a relay that refuses everything switches the channel's email off. A
bounced or complained-about address cannot be switched back on by an admin.
Mail the relay accepted can still bounce later. Ask support to connect your relay's bounce reports.