Skip to content
Nowlark
API reference menu

Replies

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.

Step-by-step guides#

Approve a GitHub deploy from your iPhone.