Skip to content

Send alerts

Every channel has one or more send URLs. To send an alert, POST a JSON body to a send URL. Copy the full URL from the app; it looks like this:

https://<api-host>/v1/send/<token>

The token in the URL is the only credential. Anyone who has the URL can send to the channel, so keep it out of public repositories and logs. A send URL can only send: it cannot read alerts or change the channel.

Terminal window
curl -X POST "$SEND_URL" \
-H 'Content-Type: application/json' \
-d '{"alert": {"title": "Build failed", "body": "3 tests failed on main"}}'

A successful request returns 201 Created. The alert is stored before the response is sent, and delivery to devices happens in the background.

Field Type Description
alert.title string Required. The alert title.
alert.body string Optional details shown under the title.
level string urgent, high, normal, or silent. Defaults to normal. See Alert levels and reminders.
require_ack boolean Keep the alert pending until someone acknowledges it. See Acknowledgement and receipts.
receipt boolean Return a receipt you can poll, even if acknowledgement is not required.
callback boolean Call the channel’s acknowledgement callback when the alert is acknowledged.
emergency object Reminder timing for urgent alerts: retry_seconds and expire_seconds.
url string A link to open from the alert.
event.type string What happened, for example build.failed.
event.subject string What it happened to, for example repo:payments:main.
event.fingerprint string A stable identity for the underlying event, used to recognize repeats. Defaults to the subject and type.
dedupe object How repeats are handled; see Repeated events.
thread_key string Groups related alerts. Shown in the alert’s details.
payload object Any JSON you want to keep with the alert. It is stored and returned by the API, not displayed.
expires_at string An RFC 3339 time after which the alert may be deleted. Defaults to 21 days after it was sent.
idempotency_key string Retrying a request with the same key returns the original alert instead of creating a second one. You can also send it as an Idempotency-Key header.

Channels that require end-to-end encryption take an encrypted envelope instead of alert; see Encrypted alerts.

{
"message": {
"id": "msg_4f1c…",
"receipt_id": "rcp_9a02…",
"level": "urgent",
"require_ack": true,
"status": "open",
"dedupe_count": 1
},
"decision": { "reason": "new_message" }
}

The response includes the stored alert as message, abridged here. receipt_id is present when the alert requires acknowledgement or you asked for a receipt. decision.reason says what happened to the request:

Reason Meaning
new_message A new alert was created.
duplicate_updated A repeat updated an open alert; see below.
duplicate_recorded A repeat was counted without changing the alert.
duplicate_dropped A repeat was ignored.
idempotent_replay The idempotency_key was already used; the original alert is returned.

decision.effective_policy shows the repeat handling that applied, with the same field names as the request’s dedupe object.

A job that fails every minute should not fill your inbox. When a request has an event and an open alert with the same fingerprint was last seen within the window, the request updates that alert instead of creating a new one: the title, body, link, and level are replaced, and the occurrence count goes up. The app shows it as, for example, “3 occurrences”. Repeats do not notify again unless the alert requires acknowledgement.

Requests without any event field are never treated as repeats.

Field Default Description
dedupe.mode coalesce none always creates a new alert.
dedupe.window_seconds 600 How long after the last repeat a new one still matches the open alert.
dedupe.accept_policy update_visible update_visible updates the alert; update_and_notify also notifies again; record_only counts the repeat without changing the alert; drop ignores it.
dedupe.renotify_after_seconds 0 When a repeat may notify again, the minimum time since the last notification.

Once an alert is acknowledged or resolved, the next request with its fingerprint starts a new alert.

Lengths count characters, so a Chinese character, a Latin letter, and an emoji each count as one. Leading and trailing whitespace is ignored.

Field Limit
Request body 64 KB
alert.title 100 characters, one line
alert.body 1,000 characters; line breaks and tabs are allowed
alert.title + alert.body 1,000 characters together
url 512 bytes; an absolute http or https URL with non-ASCII characters percent-encoded
thread_key, idempotency_key 128 characters
event.type 64 characters
event.subject, event.fingerprint 200 characters
payload 8 KB of JSON, nested at most 8 levels

A request over a limit is rejected with alert_too_long; nothing is shortened silently. Long bodies are shortened in the push notification itself, but the full text is always in the inbox. All limits are collected in Limits.

  • Each send URL accepts 60 requests per minute by default. Over the limit, requests get 429 limit_exceeded with a Retry-After header, and do not count as sent.
  • If a send URL keeps sending far past its limit — at least 100 rejected requests in a minute, or ten times its limit if that is higher — it is suspended and answers 401 invalid_send_url. A runaway script cannot be stopped from the server side otherwise, and a leaked URL is never silently reactivated. Fix the sender, then regenerate the URL in the app.
  • Each network address can make 600 requests per minute across all endpoints. Over that, requests get 429 rate_limited.

Validation errors are checked first and do not count against the send URL’s limit.

Errors have a JSON body with a stable code:

{ "error": { "code": "alert_too_long", "message": "…", "field": "alert.title", "limit": 100, "actual": 128 } }
Status Code Meaning
400 invalid_json The body is not valid JSON or not valid UTF-8.
401 invalid_send_url The send URL is wrong, revoked, suspended, or its channel was deleted.
413 request_too_large The request body is larger than 64 KB.
422 invalid_target alert.title is missing, a field has an invalid value or forbidden characters, or the request is otherwise invalid; field names the field.
422 alert_too_long A field is over its limit. The error includes field, limit, and actual.
422 invalid_alert_level level is not one of the four levels.
422 invalid_emergency The emergency timing is out of range.
422 invalid_callback callback was requested on an alert that cannot be acknowledged.
422 callback_not_configured callback was requested, but the channel has no enabled callback.
422 encryption_required The channel accepts only end-to-end encrypted alerts.
422 invalid_encrypted_message The encrypted envelope is incomplete or malformed.
429 limit_exceeded The send URL’s rate limit was reached. Retry after the number of seconds in Retry-After.
429 rate_limited Too many requests from your address. Retry after the number of seconds in Retry-After.

Every error code across the API is listed in Errors.