API docs
Pushr sends notifications and Live Activities to the iPhones of everyone in a workspace. Everything is JSON over HTTPS at
https://api.pushr.live.
Quick start
- Install the Pushr app on your iPhone, sign in and allow notifications.
- Open the dashboard, go to API keys and create a key. Copy it: it is shown once.
- Send a notification:
export PUSHR_API_KEY=psk_...
curl https://api.pushr.live/v1/channels/demo/notifications \
-H "Authorization: Bearer $PUSHR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title": "Hello from Pushr", "body": "It works."}'
Your iPhone shows the notification. The Playground sends the same pushes from the browser, without a key.
Authentication
Every call sends an API key as a bearer token: Authorization: Bearer psk_.... A key belongs to one workspace and
publishes to that workspace only. Owners and admins create and revoke keys in the dashboard; the dashboard shows the last time
each key was used. A missing, wrong or revoked key gets 401.
Keep keys out of source control. In CI, store the key as a secret and read it from an environment variable.
Channels
Every push goes to a channel, named in the URL: /v1/channels/{channel}/.... Pushr creates a channel the first time
you publish to it. A name is 1 to 64 characters: lowercase letters, digits, ., _ and -,
starting with a letter or digit. For example ci, deploys or backups.nightly.
Each member can mute a channel in the app or the dashboard. Muted members get no pushes from it, but its events still show in History.
Notifications
POST /v1/channels/{channel}/notifications alerts every device of every member of the workspace.
| Field | Type | Rules | Description |
|---|---|---|---|
title required | string | 1 to 64 characters | Bold first line. |
subtitle optional | string | up to 64 characters | Second line. |
body required | string | up to 512 characters | Message text. May be empty. |
url optional | string | up to 512 characters; URL | http(s) link opened on tap. |
symbol optional | string | up to 64 characters | SF Symbol name, e.g. hammer.fill. |
accent optional | string | matches ^#[0-9a-fA-F]{6}$ | Tint color as #RRGGBB. |
image optional | string | matches ^ast_[0-9a-f]{24}$ | Id of an image uploaded to this workspace, shown in the expanded notification. |
sound optional | boolean | default true | Play the default sound. |
level optional | string | one of passive, active, time-sensitive; default "active" | passive stays silent, time-sensitive breaks through Focus. |
curl https://api.pushr.live/v1/channels/deploys/notifications \
-H "Authorization: Bearer $PUSHR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title": "Deploy finished", "subtitle": "api v2.14.0", "body": "Rolled out to all regions in 4 min.", "url": "https://github.com/acme/api/releases", "symbol": "checkmark.circle.fill", "accent": "#34C759", "level": "active"}'
The answer is 201 with {"id": "...", "devices": 2}, where devices counts the iPhones that took the push.
Live Activities
A Live Activity sits on the Lock Screen and in the Dynamic Island, and changes as you send new states. You start, update and end it with two calls.
Start and update
PUT /v1/channels/{channel}/activities/{external_id} with {"template", "state", "alert"?}. The first call
starts the activity on every member's iPhone and returns 201 with {"id", "event": "start", "devices"}.
Later calls with the same external_id update it and return 200 with {"id", "event": "update"}.
- Send the full state every time; Pushr does not merge it with the previous one.
templateis fixed at start. An update with a different template gets409.alert({"title", "body"}, title up to 64 characters, body up to 256) lights up the screen. On start it defaults to the state's title and subtitle; on update there is none unless you send one.
curl -X PUT https://api.pushr.live/v1/channels/ci/activities/build-128 \
-H "Authorization: Bearer $PUSHR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"template": "progress", "state": {"title": "Build #128", "progress": 0.4, "value": "40%"}, "alert": {"title": "Build #128 started", "body": "main"}}'
End
POST /v1/channels/{channel}/activities/{external_id}/end ends the activity. The body is optional:
state sets a final state (same template), and dismissAt (an ISO date) says when iOS removes it from the
Lock Screen. Without dismissAt, iOS keeps an ended activity for up to four hours; a past date removes it at once.
Ending an activity that is not running gets 404.
curl -X POST https://api.pushr.live/v1/channels/ci/activities/build-128/end \
-H "Authorization: Bearer $PUSHR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"state": {"title": "Build #128", "progress": 1, "value": "Done"}, "dismissAt": "2026-10-09T18:30:00Z"}'
external_id rules
- You choose it: 1 to 128 characters, letters, digits,
.,_,:and-. A build number or job id works well. - It names one running activity per channel. The same id in two channels is two activities.
- Once an activity ends, the same id starts a new one.
- iOS ends a Live Activity after 8 hours; Pushr marks it ended then too.
A whole payload, as sent to Apple, must fit in 4 KB. A bigger one gets 413 with {"error": "payload too large", "bytes": 4210} and nothing is sent.
Templates
The template picks the layout. Every template takes the shared fields title, subtitle,
symbol, accent, url and image, plus its own. Unknown fields get
400.
progress
A progress bar with short trailing text. Leave progress out for an indeterminate bar.
| Field | Type | Rules | Description |
|---|---|---|---|
title required | string | 1 to 64 characters | Main line. |
subtitle optional | string | up to 128 characters | Second line. |
symbol optional | string | up to 64 characters | SF Symbol name, e.g. hammer.fill. |
accent optional | string | matches ^#[0-9a-fA-F]{6}$ | Tint color as #RRGGBB. |
url optional | string | up to 512 characters; URL | http(s) link opened on tap. |
image optional | string | matches ^ast_[0-9a-f]{24}$ | Id of an image uploaded to this workspace. |
progress optional | number | 0 to 1 | Share done. Leave it out for an indeterminate bar. |
value optional | string | up to 16 characters | Short trailing text, e.g. 62% or 3/5. |
curl -X PUT https://api.pushr.live/v1/channels/backups/activities/nightly-2026-10-09 \
-H "Authorization: Bearer $PUSHR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"template": "progress", "state": {"title": "Nightly backup", "subtitle": "db-1 to S3", "symbol": "externaldrive.fill", "accent": "#007AFF", "progress": 0.62, "value": "62%"}}'
steps
A list of named steps, each with a status. Fits CI pipelines and multi-stage jobs.
| Field | Type | Rules | Description |
|---|---|---|---|
title required | string | 1 to 64 characters | Main line. |
subtitle optional | string | up to 128 characters | Second line. |
symbol optional | string | up to 64 characters | SF Symbol name, e.g. hammer.fill. |
accent optional | string | matches ^#[0-9a-fA-F]{6}$ | Tint color as #RRGGBB. |
url optional | string | up to 512 characters; URL | http(s) link opened on tap. |
image optional | string | matches ^ast_[0-9a-f]{24}$ | Id of an image uploaded to this workspace. |
steps required | array of objects | 1 to 8 items | Steps in order. |
steps[].name required | string | 1 to 32 characters | Step label. |
steps[].status required | string | one of pending, running, success, failure, skipped | |
value optional | string | up to 16 characters | Short trailing text, e.g. 2/4. |
curl -X PUT https://api.pushr.live/v1/channels/ci/activities/pipeline-128 \
-H "Authorization: Bearer $PUSHR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"template": "steps", "state": {"title": "CI pipeline", "subtitle": "main · #128", "symbol": "hammer.fill", "url": "https://github.com/acme/api/actions", "value": "3/4", "steps": [
{"name": "install", "status": "success"},
{"name": "test", "status": "success"},
{"name": "build", "status": "success"},
{"name": "deploy", "status": "running"}]}}'
status
A title, a badge and up to four label and value pairs. Fits incidents, matches and anything with a few numbers.
| Field | Type | Rules | Description |
|---|---|---|---|
title required | string | 1 to 64 characters | Main line. |
subtitle optional | string | up to 128 characters | Second line. |
symbol optional | string | up to 64 characters | SF Symbol name, e.g. hammer.fill. |
accent optional | string | matches ^#[0-9a-fA-F]{6}$ | Tint color as #RRGGBB. |
url optional | string | up to 512 characters; URL | http(s) link opened on tap. |
image optional | string | matches ^ast_[0-9a-f]{24}$ | Id of an image uploaded to this workspace. |
badge optional | string | 1 to 16 characters | Short label next to the title, e.g. LIVE. |
fields optional | array of objects | up to 4 items | Label and value pairs. |
fields[].label required | string | 1 to 16 characters | |
fields[].value required | string | up to 24 characters |
curl -X PUT https://api.pushr.live/v1/channels/incidents/activities/inc-512 \
-H "Authorization: Bearer $PUSHR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"template": "status", "state": {"title": "API latency high", "subtitle": "eu-west", "symbol": "exclamationmark.triangle.fill", "accent": "#FF9500", "badge": "SEV2", "fields": [
{"label": "p95", "value": "870 ms"},
{"label": "Errors", "value": "2.1%"},
{"label": "On call", "value": "Sam"}]}}'
countdown
A timer to endsAt that iOS keeps running without further pushes. With startsAt it also shows a progress bar.
| Field | Type | Rules | Description |
|---|---|---|---|
title required | string | 1 to 64 characters | Main line. |
subtitle optional | string | up to 128 characters | Second line. |
symbol optional | string | up to 64 characters | SF Symbol name, e.g. hammer.fill. |
accent optional | string | matches ^#[0-9a-fA-F]{6}$ | Tint color as #RRGGBB. |
url optional | string | up to 512 characters; URL | http(s) link opened on tap. |
image optional | string | matches ^ast_[0-9a-f]{24}$ | Id of an image uploaded to this workspace. |
endsAt required | string | ISO 8601 date and time with offset | When the timer reaches zero. |
startsAt optional | string | ISO 8601 date and time with offset | Adds a progress bar from this time. |
curl -X PUT https://api.pushr.live/v1/channels/releases/activities/freeze-v2-15 \
-H "Authorization: Bearer $PUSHR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"template": "countdown", "state": {"title": "Code freeze", "subtitle": "Release v2.15", "symbol": "snowflake", "startsAt": "2026-10-09T09:00:00Z", "endsAt": "2026-10-09T17:00:00Z"}}'
Images
Upload a PNG or JPEG once, then show it in notifications and Live Activities with its id in the image field.
POST /v1/assets takes the raw file as the body, at most 256 KB and 512 × 512 px. It returns 201 with
{"id": "ast_...", "width", "height", "bytes"}, or 200 with the same id if the workspace already has the
file. Pushr then copies new images to members' iPhones in the background, so upload before you need them.
curl https://api.pushr.live/v1/assets \
-H "Authorization: Bearer $PUSHR_API_KEY" \
-H "Content-Type: image/png" \
--data-binary @logo.png
An id from another workspace gets 400. Owners and admins can also upload and delete images in the dashboard.
Rate limits
Limits count per workspace, so more keys do not buy more sends.
| Calls | Limit |
|---|---|
| Live Activity start, update and end | 120 a minute |
| Notifications | 30 a minute |
| Image uploads | 10 a minute |
Past the limit you get 429 with {"error": "rate limited"} and Retry-After: 60.
Errors
Errors are JSON with an error message. Bad fields also list zod issues, each with a path and a message:
{"error": "invalid request", "issues": [{"code": "too_big", "path": ["title"], "message": "Too big: expected string to have <=64 characters"}]}
| Status | Meaning |
|---|---|
400 | Invalid JSON, a field breaks a rule, or an unknown image id. |
401 | Missing, wrong or revoked API key. |
404 | Nothing to end: no running activity with that id. |
409 | The update uses another template than the running activity, or it ended while you updated it (retry). |
411 | An image upload without Content-Length. |
413 | Payload over 4 KB, or image over 256 KB. |
429 | Rate limited; wait for Retry-After seconds. |
500 | Our fault. Retry later. |
Teams
Each account has a personal workspace that only you see. Create a team workspace in the dashboard to share pushes: everything sent with that workspace's keys reaches every member's iPhone. Invite people with a link or an 8-character code.
- Owners do everything, including deleting the team and changing roles.
- Admins manage API keys, images and invites.
- Members get the pushes, see History and mute channels.
When someone leaves or is removed, their iPhone ends the team's Live Activities.