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 key | Used for every endpoint except GET /v1/actions/events |
|---|---|
| Read key | Used only for GET /v1/actions/events |
| Reply token | Reads one message with GET /v1/messages/{id} |
Errors use RFC 9457 application/problem+json. See Error codes.
Rate limits
| Per key | 10 requests in 10 seconds. Beyond that, 429 rate_limited |
|---|---|
| Polling one message | 10 requests in 10 seconds |
| Per channel | 60 requests a minute. Beyond that, it pauses for 5 minutes with 403 channel_paused |
| Body size | 16 KB, or 64 KB for POST /v1/messages and 256 KB for inbound URLs |
| Monthly limit | 1,000 messages on Free, 50,000 deliveries per member on Team. Only high can go 10% over |
| Open requests | 3 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.
kind | Required. notice, progress, alert, approval, or choice |
|---|---|
title | Required. 1 to 120 characters |
body | Up to 1,000 characters, or 4,000 when format is markdown |
level | low, normal (default), or high. These map to iOS Passive, Active, and Time Sensitive |
fields | Up to 6. label 24 characters, value 80 characters |
links | Up to 2. label 24 characters, url https and 512 characters |
options | choice only. 2 to 4. id 32 characters, label 16 characters, style is default, primary, or destructive |
confirm | Requires Face ID to respond. Defaults to true for approval and false for choice |
expires_in | Seconds 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_timeout | reject (default) or ignore. Returned as is in the action.expired event |
format | plain (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 |
image | data (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 |
input | prompt (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 |
secret | public_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_required | People needed to settle an approval, 1 to 3. More than 1 needs Team. The same person counts once, and one rejection settles it |
escalate_after | After this many seconds without a response, other people are told. 60 to 2,592,000, and less than the response deadline. Team only |
escalate_to | Who to tell. Same shape as to. Defaults to Managers and Responders. Needs escalate_after |
alert_key | Up to 128 characters of letters, digits, and . _ : -. Used to resolve alerts |
dedupe_key | Up to 128 characters. Open messages with the same key become one with a count. Grouped sends do not count toward limits |
progress | Required with kind: progress. current, total (at least 1), label (40 characters), state (running, succeeded, failed) |
to | members (up to 20) and roles (manager, responder). Everyone still receives it, but only these people get it counted in their Action tab |
sensitive | Hides the body on the lock screen |
delivery | immediate (default) or digest. digest goes into an hourly summary and needs level: low |
metadata | String 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.
id | Message ID, starting with msg_ |
|---|---|
status | queued |
reply_token | A token that reads only this message |
sent_at | Time accepted |
expires_at | Response deadline, present for messages that need a response or that set expires_in |
deduped | true 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_at | Values from sending |
|---|---|
source | The route recorded by the server: api, cli, github_action, mcp, slack, grafana, alertmanager, generic, datadog, sentry, or pagerduty |
action_state | Only on messages that need a response. See the table below |
response | The settled response: option_id, actor, assurance (none, biometric), responded_at, note, secret_digest, signature |
approvals, approvals_required | Approvals collected before settling, and the number needed |
delivery | Estimated counts of confirmed, delayed, unconfirmed, and failed. unconfirmed does not mean undelivered |
resolved_at | When an alert was resolved |
action_state values
open | Awaiting a response |
|---|---|
claimed | A response is settled |
delivered | The settled response reached your callback URL |
done | The sender reported a result |
expired | The deadline and the 10 minute grace period passed |
cancelled | The sender cancelled it |
failed | Intake 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.
status | Required. succeeded or failed |
|---|---|
summary | Up 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.
interval | Required. 60 to 2,678,400 seconds |
|---|---|
grace | 0 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.
cursor | The previous next_cursor |
|---|---|
limit | 1 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.