Pushr

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

  1. Install the Pushr app on your iPhone, sign in and allow notifications.
  2. Open the dashboard, go to API keys and create a key. Copy it: it is shown once.
  3. 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.

FieldTypeRulesDescription
title requiredstring1 to 64 charactersBold first line.
subtitle optionalstringup to 64 charactersSecond line.
body requiredstringup to 512 charactersMessage text. May be empty.
url optionalstringup to 512 characters; URLhttp(s) link opened on tap.
symbol optionalstringup to 64 charactersSF Symbol name, e.g. hammer.fill.
accent optionalstringmatches ^#[0-9a-fA-F]{6}$Tint color as #RRGGBB.
image optionalstringmatches ^ast_[0-9a-f]{24}$Id of an image uploaded to this workspace, shown in the expanded notification.
sound optionalbooleandefault truePlay the default sound.
level optionalstringone 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"}.

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

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.

FieldTypeRulesDescription
title requiredstring1 to 64 charactersMain line.
subtitle optionalstringup to 128 charactersSecond line.
symbol optionalstringup to 64 charactersSF Symbol name, e.g. hammer.fill.
accent optionalstringmatches ^#[0-9a-fA-F]{6}$Tint color as #RRGGBB.
url optionalstringup to 512 characters; URLhttp(s) link opened on tap.
image optionalstringmatches ^ast_[0-9a-f]{24}$Id of an image uploaded to this workspace.
progress optionalnumber0 to 1Share done. Leave it out for an indeterminate bar.
value optionalstringup to 16 charactersShort 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.

FieldTypeRulesDescription
title requiredstring1 to 64 charactersMain line.
subtitle optionalstringup to 128 charactersSecond line.
symbol optionalstringup to 64 charactersSF Symbol name, e.g. hammer.fill.
accent optionalstringmatches ^#[0-9a-fA-F]{6}$Tint color as #RRGGBB.
url optionalstringup to 512 characters; URLhttp(s) link opened on tap.
image optionalstringmatches ^ast_[0-9a-f]{24}$Id of an image uploaded to this workspace.
steps requiredarray of objects1 to 8 itemsSteps in order.
steps[].name requiredstring1 to 32 charactersStep label.
steps[].status requiredstringone of pending, running, success, failure, skipped
value optionalstringup to 16 charactersShort 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.

FieldTypeRulesDescription
title requiredstring1 to 64 charactersMain line.
subtitle optionalstringup to 128 charactersSecond line.
symbol optionalstringup to 64 charactersSF Symbol name, e.g. hammer.fill.
accent optionalstringmatches ^#[0-9a-fA-F]{6}$Tint color as #RRGGBB.
url optionalstringup to 512 characters; URLhttp(s) link opened on tap.
image optionalstringmatches ^ast_[0-9a-f]{24}$Id of an image uploaded to this workspace.
badge optionalstring1 to 16 charactersShort label next to the title, e.g. LIVE.
fields optionalarray of objectsup to 4 itemsLabel and value pairs.
fields[].label requiredstring1 to 16 characters
fields[].value requiredstringup 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.

FieldTypeRulesDescription
title requiredstring1 to 64 charactersMain line.
subtitle optionalstringup to 128 charactersSecond line.
symbol optionalstringup to 64 charactersSF Symbol name, e.g. hammer.fill.
accent optionalstringmatches ^#[0-9a-fA-F]{6}$Tint color as #RRGGBB.
url optionalstringup to 512 characters; URLhttp(s) link opened on tap.
image optionalstringmatches ^ast_[0-9a-f]{24}$Id of an image uploaded to this workspace.
endsAt requiredstringISO 8601 date and time with offsetWhen the timer reaches zero.
startsAt optionalstringISO 8601 date and time with offsetAdds 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.

CallsLimit
Live Activity start, update and end120 a minute
Notifications30 a minute
Image uploads10 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"}]}
StatusMeaning
400Invalid JSON, a field breaks a rule, or an unknown image id.
401Missing, wrong or revoked API key.
404Nothing to end: no running activity with that id.
409The update uses another template than the running activity, or it ended while you updated it (retry).
411An image upload without Content-Length.
413Payload over 4 KB, or image over 256 KB.
429Rate limited; wait for Retry-After seconds.
500Our 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.

When someone leaves or is removed, their iPhone ends the team's Live Activities.