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 fieldsGlossaryErrors and status codes
The error shape, and each HTTP status the API returns, with the line that explains it.
Errors#
Every error is JSON with a machine-readable error and a human message.
| Status | error |
Means |
|---|---|---|
400 |
invalid_request |
Missing source, no title and no body, a non-http URL, priority out of range, an image_id this account did not upload or that has expired |
401 |
auth_required |
No key sent |
401 |
invalid_key |
Unknown or revoked key |
401 |
wrong_credential |
You sent an account key ( nla_) where an API key (nlk_) belongs |
402 |
upgrade_required |
The send used a feature this account has not unlocked, e.g. image |
409 |
image_store_full |
This account already holds 50 MB of uploaded images |
413 |
image_too_large |
An uploaded image is over 5 MB |
415 |
unsupported_image |
An uploaded file is not a PNG or a JPEG |
429 |
rate_limited |
1000/hour exceeded |
429 |
too_many_scheduled |
200 scheduled sends are already waiting |
403 |
wrong_credential |
A channel key was used for something other than sending to its channel |
Errors from the account endpoints arrive wrapped as {"detail": {"error": …, "message": …}},
which is FastAPI's shape. The bundled clients unwrap both.
Status codes#
Each row is the line in which the status appears, and the section it comes from.
| Status | Where it is said |
|---|---|
200 | The same answer sent again (an offline retry) is 200 with "already":true; a different one is 409 with the answer that stands.In Replies |
200 | 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).In Replies |
200 | Two members at the same moment get one 200 and one 409 already_claimed naming who has it.In Team forms |
400 | |
400 | A literal non-public address is a 400 when sent; a name that resolves privately later is simply never called.In Receipts and callbacks |
400 | |
400 | 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.In Replies |
400 | callback_url and callback_secret are still refused on a channel send (400); the receipt is the way back, as for any channel send.In Replies |
400invalid_request | A link is something a phone opens on a tap, so the scheme is not a remote sender's to choose: anything else is refused 400 invalid_request.In Links, images and buttons: Links |
400invalid_request | An id another account uploaded, or one that has expired, is refused 400 invalid_request, and so is a send with both image and image_id.In Links, images and buttons: Images |
400 | callback_url is refused (400), not ignored: read the receipt instead.In Channels and escalation: Sending to a channel |
400unknown_template | An unknown name is a 400 unknown_template.In Team forms |
400 | Finding the company: POST / (no credential; 30 lookups an hour from one IP address) returns {"org": "<sign-in name>", when the email's domain is one a team has verified (Email domains, below) and that team has company sign‑in set up, and {"org": null} for any other well-formed address, so the answer does not tell anyone which domains have a team (a malformed one is 400, too many lookups 429).In Company sign‑in and SCIM: Company sign‑in (Okta and other OpenID Connect providers) |
400 | PUT /: One of history_choices: 30 for everyone, 90, 180 or 365 with the Upgrade Pass (402 upgrade_required without it, 400 for a length not on offer).In Identity and moving phones: Identity |
400 | PUT /: Choosing a tone is part of the Upgrade Pass (402 upgrade_required); a name not on offer is 400.In Identity and moving phones: Identity |
400 | PUT /: every is one of repeat_every_choices (0 is off), for one of repeat_for_choices; anything else is 400.In Identity and moving phones: Identity |
401 | 401 if the key is unknown or revoked.In Authentication and rate limit: GET /v1/key/check |
401 | Errors are SCIM errors; a bad token is one 401 for every reason; a duplicate userName is a 409 uniqueness.In Company sign‑in and SCIM: Automatic provisioning (SCIM 2.0) |
402upgrade_required | image: Part of the Upgrade Pass; without one the send is refused 402 upgrade_required.In Send a notification: POST /v1/notify |
402 | Refused now, not later. A bad field, a feature the account has not unlocked ( 402), or too many waiting (429 too_many_scheduled, 200 per account) is the response to the call that asked, never a silent failure at the time it was due.In Scheduled sends: Scheduled sends |
402upgrade_required | |
402upgrade_required | PUT /: One of history_choices: 30 for everyone, 90, 180 or 365 with the Upgrade Pass (402 upgrade_required without it, 400 for a length not on offer).In Identity and moving phones: Identity |
402upgrade_required | PUT /: Choosing a tone is part of the Upgrade Pass (402 upgrade_required); a name not on offer is 400.In Identity and moving phones: Identity |
402upgrade_required | POST /v1/keys: The first is free; more are part of the Upgrade Pass (402 upgrade_required).In Devices, keys and history |
403forbidden | A team you are not on answers 404, the same as one that does not exist, and a role too low answers 403 forbidden.In Teams, roles and endpoints |
404 | DELETE / cancels one that has not fired; once it has been picked up it is on its way and the answer is 404.In Scheduled sends: Scheduled sends |
404 | Any key on the account may ask, only for its own account's notifications; another account's id is 404, the same as one that does not exist.In Receipts and callbacks |
404 | 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.In Replies |
404 | A team you are not on answers 404, the same as one that does not exist, and a role too low answers 403 forbidden.In Teams, roles and endpoints |
404invalid_code | POST / {"code"}: Redeem an invite. 20 attempts an hour; every failure is one 404 invalid_code.In Teams, roles and endpoints |
404 | A suspended or unknown form is the same 404.In Team forms |
404 | Who sees it. Admins see every form; anyone else only forms announcing to a channel they are on, and a form they may not see is a 404, the same as one that does not exist.In Team forms |
404 | A 404 or 410 means the webhook was deleted on the service's side and is not retried.In Team forms: Post to Slack, Microsoft Teams, Discord or a webhook |
404invalid_handoff | A handoff is single use, expires after two minutes, and is refused ( 404 invalid_handoff) for any account but the one that began the sign‑in.In Company sign‑in and SCIM: Company sign‑in (Okta and other OpenID Connect providers) |
404 | An id belonging to someone else returns 404, never their row.In Identity and moving phones |
409 | later is an answer, not a snooze: it closes the question like any other, so a later approve gets 409.In Replies |
409 | The same answer sent again (an offline retry) is 200 with "already":true; a different one is 409 with the answer that stands.In Replies |
409 | 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).In Replies |
409too_many_sources | 409 too_many_sources past 500 names on one account.In Sources: POST /v1/sources |
409image_store_full | 50 MB of images per account. Past that, POST /v1/images returns 409 image_store_full.In Links, images and buttons: Images |
409sso_required | POST / {"code"}: 409 sso_required or 409 provisioned when the team adds people through its company sign‑in or its identity providerIn Teams, roles and endpoints |
409provisioned | POST / {"code"}: 409 sso_required or 409 provisioned when the team adds people through its company sign‑in or its identity providerIn Teams, roles and endpoints |
409escalation_needs_any | Needs a channel whose ack_policy is any (409 escalation_needs_any otherwise): on all every member must open it, so there is no "nobody has" to escalate on.In Channels and escalation: Escalation |
409email_not_configured | Email needs the operator to configure a relay; without one, adding an email destination is 409 email_not_configured.In Webhooks and email |
409already_claimed | Two members at the same moment get one 200 and one 409 already_claimed naming who has it.In Team forms |
409already_exists | POST /: 422 bad_url for an address that is not that service's webhook, 409 already_exists for one already connected, 409 too_many_connections past 20 per team (any number of rules can share one)In Team forms: Post to Slack, Microsoft Teams, Discord or a webhook |
409too_many_connections | POST /: 422 bad_url for an address that is not that service's webhook, 409 already_exists for one already connected, 409 too_many_connections past 20 per team (any number of rules can share one)In Team forms: Post to Slack, Microsoft Teams, Discord or a webhook |
409connection_in_use | DELETE /: 409 connection_in_use, naming the rules, while a rule posts to itIn Team forms: Post to Slack, Microsoft Teams, Discord or a webhook |
409hours_in_use | PUT /: 409 hours_in_use, naming the rules, for a time zone alone while a rule tests business_hoursIn Team forms: Business hours |
409hours_in_use | DELETE /: 409 hours_in_use, naming the rules, while any rule tests business_hours, time_of_day or weekday, since they would move to UTCIn Team forms: Business hours |
409stale_order | PUT /: A list that misses or repeats one is 409 stale_order (someone may have just added a rule): read the list again and resendIn Team forms: Business hours |
409too_many_rules | A team has up to 50 rules ( 409 too_many_rules past that).In Team forms: Business hours |
409no_test | While no test runs, the three admin routes answer 409 no_test, and starting a second one answers 409 test_running.In Team forms: Two versions of a form |
409test_running | While no test runs, the three admin routes answer 409 no_test, and starting a second one answers 409 test_running.In Team forms: Two versions of a form |
409no_google_secret | If GA4's validation server reports a problem with the event, the answer is "sent":false with Google's description in message; without a saved secret it is 409 no_google_secret.In Team forms: Google Analytics and Tag Manager |
409plan_active | 409 plan_active while the team already has a live subscription.In Plans, audit log and transfer: Plans and billing |
409trial_unavailable | 409 trial_unavailable when the account has had its trial, the team already has a plan, or the owner's Upgrade Pass already covers the team.In Plans, audit log and transfer: Plans and billing |
409 | Refused with 409: plan_active while a merchant subscription is live (it belongs to whoever pays it: cancel, hand over, subscribe again) and new_owner_cannot_carry_it while billing is not open and the new owner's own Upgrade Pass would not let the team send.In Plans, audit log and transfer: Ownership transfer |
409 | Without app=1, the same address signs a browser in to the console and links the company account to the phone that made the code, so the phone is the member and gets the alerts; a company account already linked to another Nowlark account is refused (409).In Company sign‑in and SCIM: Company sign‑in (Okta and other OpenID Connect providers) |
409record_not_found | 409 record_not_found until it is (DNS changes can take a few minutes).In Company sign‑in and SCIM: Email domains |
409domain_taken | A domain is verified for one team only: once one team has verified it, another gets 409 domain_taken.In Company sign‑in and SCIM: Email domains |
409uniqueness | Errors are SCIM errors; a bad token is one 401 for every reason; a duplicate userName is a 409 uniqueness.In Company sign‑in and SCIM: Automatic provisioning (SCIM 2.0) |
409plan_active | DELETE /v1/me: 409 plan_active while a team it owns has a live subscription with the merchant (the team would go and the charges would not): cancel that first.In Identity and moving phones: Identity |
410expired | |
410 | A 404 or 410 means the webhook was deleted on the service's side and is not retried.In Team forms: Post to Slack, Microsoft Teams, Discord or a webhook |
413 | A body over 256 KB is 413.In Webhook templates |
413image_too_large | Over 5 MB is 413 image_too_large.In Links, images and buttons: Images |
429too_many_scheduled | Refused now, not later. A bad field, a feature the account has not unlocked ( 402), or too many waiting (429 too_many_scheduled, 200 per account) is the response to the call that asked, never a silent failure at the time it was due.In Scheduled sends: Scheduled sends |
429 | Exceeding it returns 429 with {"error": "rate_limited"}.In Authentication and rate limit: Rate limit |
429 | Finding the company: POST / (no credential; 30 lookups an hour from one IP address) returns {"org": "<sign-in name>", when the email's domain is one a team has verified (Email domains, below) and that team has company sign‑in set up, and {"org": null} for any other well-formed address, so the answer does not tell anyone which domains have a team (a malformed one is 400, too many lookups 429).In Company sign‑in and SCIM: Company sign‑in (Okta and other OpenID Connect providers) |
500 | |
501 | Groups answers an empty list and refuses writes (501): roles still come from the groups in the sign‑in.In Company sign‑in and SCIM: Automatic provisioning (SCIM 2.0) |
Prowl's status codes#
Answered on Prowl's own API. Prowl's, not HTTP's obvious ones. A client written against Prowl branches on exactly these:
| Code | Meaning |
|---|---|
200 |
Accepted |
400 |
Bad request: no application, no event or description, a non-http url, priority out of range |
401 |
Unknown or missing key. One bad key in a comma-separated list refuses the whole call and sends nothing |
406 |
The key's 1000/hour is spent. Not 429: Prowl used 406 and clients back off on it |
500 |
Something unexpected |
ntfy's errors#
Answered on ntfy's publish API. Errors are ntfy's JSON ({"code": 40101, "http": 401, "error": "…"}), and a key that does not
work is always 401, which is what a tool's "Test" button reports as needing authorization.