API

The API for sending notifications from services, CLIs, and agents, and getting a person's response.

Base URL: https://api.nodd.blipdrop.net

Authentication

Send Authorization: Bearer <key>.

Send keyUsed for every endpoint except GET /v1/actions/events
Read keyUsed only for GET /v1/actions/events
Reply tokenReads one message with GET /v1/messages/{id}

Errors use RFC 9457 application/problem+json. See Error codes.

Rate limits

Per key10 requests in 10 seconds. Beyond that, 429 rate_limited
Polling one message10 requests in 10 seconds
Per channel60 requests a minute. Beyond that, it pauses for 5 minutes with 403 channel_paused
Body size16 KB, or 64 KB for POST /v1/messages and 256 KB for inbound URLs
Monthly limit1,000 messages on Free, 50,000 deliveries per member on Team. Only high can go 10% over
Open requests3 on Free, 500 on Team

RateLimit-Remaining on the POST /v1/messages response is what remains in the current window.

POST /v1/messages

Sends a message. It answers 202 on acceptance, which does not guarantee delivery. Unknown fields are refused.

kindRequired. notice, progress, alert, approval, or choice
titleRequired. 1 to 120 characters
bodyUp to 1,000 characters, or 4,000 when format is markdown
levellow, normal (default), or high. These map to iOS Passive, Active, and Time Sensitive
fieldsUp to 6. label 24 characters, value 80 characters
linksUp to 2. label 24 characters, url https and 512 characters
optionschoice only. 2 to 4. id 32 characters, label 16 characters, style is default, primary, or destructive
confirmRequires Face ID to respond. Defaults to true for approval and false for choice
expires_inSeconds until the response deadline, 60 to 2,592,000. It cannot exceed the plan's delivery window (86,400 seconds on Free). If omitted on a message that needs a response, the delivery window is used
on_timeoutreject (default) or ignore. Returned as is in the action.expired event
formatplain (default) or markdown. markdown formats headings, lists, quotes, code, emphasis, and links in the app, and allows 4,000 characters. HTML, images, and tables are not rendered. Notifications show the text without markup
imagedata (Base64 JPEG or PNG, up to 40 KB) and alt (up to 120 characters). Sealed and deleted the same way as the body. Not available on progress
inputprompt (up to 40 characters) and required. Shows a text field on an approval or choice answered by one person, and returns up to 200 characters in response.note. Without it, the only text a responder can send is a rejection reason
secretpublic_key (a Base64 32-byte X25519 public key). Shows a masked field on an approval. The device seals the value to this key, and you read it with GET /v1/messages/{id}/secret. It cannot be combined with input, dedupe_key, confirm: false, or more than one approval
approvals_requiredPeople needed to settle an approval, 1 to 3. More than 1 needs Team. The same person counts once, and one rejection settles it
escalate_afterAfter this many seconds without a response, other people are told. 60 to 2,592,000, and less than the response deadline. Team only
escalate_toWho to tell. Same shape as to. Defaults to Managers and Responders. Needs escalate_after
alert_keyUp to 128 characters of letters, digits, and . _ : -. Used to resolve alerts
dedupe_keyUp to 128 characters. Open messages with the same key become one with a count. Grouped sends do not count toward limits
progressRequired with kind: progress. current, total (at least 1), label (40 characters), state (running, succeeded, failed)
tomembers (up to 20) and roles (manager, responder). Everyone still receives it, but only these people get it counted in their Action tab
sensitiveHides the body on the lock screen
deliveryimmediate (default) or digest. digest goes into an hourly summary and needs level: low
metadataString pairs, up to 1 KB. Returned as is in response events. Not encrypted to devices

format, image, input, and secret need Nodd 1.1 or later. Version 1.0 of the app shows no formatting or images, and can only reject a required input or a secret request.

With an Idempotency-Key header (up to 128 characters), the same key gets the first result for 24 hours, so a retry does not send twice.

The response has these fields.

idMessage ID, starting with msg_
statusqueued
reply_tokenA token that reads only this message
sent_atTime accepted
expires_atResponse deadline, present for messages that need a response or that set expires_in
dedupedtrue when grouped into an existing message by dedupe_key

GET /v1/messages/{id}

Returns the state and response. Bodies are not on the server, so they are not returned. While open, the reply has Retry-After: 3. The server does not hold the connection, so poll after that time.

id, kind, sent_at, expires_atValues from sending
sourceThe route recorded by the server: api, cli, github_action, mcp, slack, grafana, alertmanager, generic, datadog, sentry, or pagerduty
action_stateOnly on messages that need a response. See the table below
responseThe settled response: option_id, actor, assurance (none, biometric), responded_at, note, secret_digest, signature
approvals, approvals_requiredApprovals collected before settling, and the number needed
deliveryEstimated counts of confirmed, delayed, unconfirmed, and failed. unconfirmed does not mean undelivered
resolved_atWhen an alert was resolved

action_state values

openAwaiting a response
claimedA response is settled
deliveredThe settled response reached your callback URL
doneThe sender reported a result
expiredThe deadline and the 10 minute grace period passed
cancelledThe sender cancelled it
failedIntake failed, or the response could not reach your callback for 24 hours. In that case response is still present

GET, DELETE /v1/messages/{id}/secret

Returns the answer to secret, still sealed, as {"sealed": "…"}. The value is base64(enc || ciphertext). You can read it once the request is approved. Delete it with DELETE after reading. It is removed after 10 minutes either way.

It uses HPKE (DHKEM X25519 / HKDF-SHA256 / ChaCha20-Poly1305), with nod-secret-v1 as info and the message ID as additional data. The value is never in response, events, or callbacks. response.secret_digest is the SHA-256 of the Base64 text and is covered by the signature.

The public key passes through the server, so this does not protect against a compromised server.

PATCH /v1/messages/{id}

Updates a kind: progress message. It needs title or progress. Updates reach devices encrypted, like bodies, and are not kept on the server. running updates are silent, and succeeded and failed notify. Later updates get 409 invalid_state. It answers 202.

POST /v1/messages/{id}/cancel

Withdraws an approval or choice before it is answered. It answers 204. After a response it gives 409 already_claimed.

POST /v1/messages/{id}/result

Reports what happened after acting on a response. The card changes to done and action_state becomes done. It answers 204.

statusRequired. succeeded or failed
summaryUp to 200 characters. Reaches devices encrypted

POST /v1/alerts/{alert_key}/resolve

Closes open alerts with this alert_key and removes them from everyone's Action tab. It answers 202 even with no match. A note of up to 200 characters is optional.

POST /v1/heartbeats/{name}

Reports that a scheduled job is alive. name has the same format as alert_key. If no report arrives within interval plus grace seconds, the channel is told at high. A later report announces recovery. Reports do not count toward limits. Each channel can have 20.

intervalRequired. 60 to 2,678,400 seconds
grace0 to 2,678,400 seconds. If omitted, 10% of interval and at least 60 seconds

It answers 200 with name and the next due_at. DELETE /v1/heartbeats/{name} stops monitoring and answers 204.

GET /v1/actions/events

An events API for services that cannot receive callbacks. Use a read key.

cursorThe previous next_cursor
limit1 to 100. Default 50

It returns events and next_cursor. See Callbacks and signatures for the event format. Events from the last 2 seconds wait for the next read, so reordering cannot skip them. Respect Retry-After: 3. Events are deleted after 7 days.

GET /v1/meta

Returns the service state without authentication. mode is normal, degraded, read_only, or intake_stopped. It also returns api_version and notice when there is one.