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 fieldsGlossaryReplies
A notification can ask a question the person answers with one tap, on the iPhone (the Mac app cannot show or answer one yet), and the answer comes back to the sender. This is how a script asks "Deploy v2.4 to production?" and acts on the answer.
{"source":"Deploy","title":"Deploy v2.4 to production?","body":"All checks passed",
"replies":"approve_deny","callback_url":"https://yourcompany.com/nowlark","callback_secret":"…16+ chars…",
"reply_within":3600}
The buttons are a fixed vocabulary, for the reason actions are: iOS draws only buttons the app
registered before the notification arrived. The reply sets, and the verbs a tap sends:
replies |
Buttons (verb) |
|---|---|
approve_deny |
Approve ( approve), Deny (deny) |
yes_no |
Yes ( yes), No (no) |
approve_deny_later |
Approve, Deny, Later ( later) |
yes_no_later |
Yes, No, Later |
approve and yes need the phone to be unlocked (the app registers them as authentication-required);
deny, no and later do not. That is enforced by the app, not by the server, which cannot see the
lock screen. later is an answer, not a snooze: it closes the question like any other, so a later approve gets 409. One reply set per notification. It replaces actions rather than sitting beside them
(400 if both are sent). A team channel send takes replies too: see below.
An answer the sender has not heard yet is kept. The signed callback is sent from the row that holds the answer, so until it is delivered (or its retries are used up) that row is not removed by a later send with the same replace_key, nor by the person deleting the notification from the history; either then takes effect once the sender has been told. A question that has closed also stops repeating.
Every post to a callback URL that came with a callback_secret is signed (X-Nowlark-Signature), the acknowledgement as well as the answer, so a sender that checks signatures has no kind of post to its URL that anyone could forge. Opening a question without answering it sends acknowledged, which is not an answer: act only on replied.
The answer is recorded once. The app posts it to POST /v1/notifications/{id}/reply with the
account key and {"reply":"approve"}. The first answer wins, decided by a single conditional update,
so two devices tapping at once cannot both win. The same answer sent again (an offline retry) is 200
with "already":true; a different one is 409 with the answer that stands. After reply_within the
answer is refused (410 expired). A reply also counts as seeing the notification: it is read and stops
repeating. A reply to a notification with no reply set is 400, a verb outside the set is 400, and
another account's notification is 404.
The sender learns it two ways. GET /v1/receipts/{id} carries reply_state (null when no reply
set was asked for, else waiting, answered or expired), reply and replied_at. And if
callback_url was given, Nowlark calls it with POST:
{"event":"replied","id":"ntf_…","source":"Deploy","reply":"approve","replied_at":1789665612.4}
signed like a webhook: X-Nowlark-Signature: t=<unix>,v1=<hex>, HMAC-SHA256 over <t>.<body> keyed
with callback_secret. Reject a timestamp more than a few minutes old. The URL is held to every rule
of a callback_url (https, public addresses, no redirects, one 5 second deadline) and is tried on the
same schedule. The same URL also receives the usual acknowledged event, since a reply is also a
read. Confirm with the receipt, which needs your API key; the callback alone is a hint.
On a team channel. Every member gets the question; the first member to answer settles it for
the thread. That is one conditional update on the thread, so two members tapping together cannot both
win: the other gets 409 with the answer that stands (200 with "already":true if they tapped the
same verb). The winner's name (the one they chose for the team) is on the thread receipt as
replied_by, beside reply_state, reply and replied_at. Every other member's copy takes the
standing answer in their history and stops repeating, and a later escalation tier is not paged.
reply_within is counted from the send, for the whole thread: an escalation tier sent later gets what is left (and is skipped once it has closed), never a fresh window. The receipt names the winner by the name they chose for the team, even after their own copy is gone. callback_url and callback_secret are still refused on a
channel send (400); the receipt is the way back, as for any channel send.