Contents
Docs

API reference

Every endpoint, parameter and response of the PushPiper API. It's Pushover's API, served from https://api.pushpiper.com/1/.

Overview

PushPiper speaks the Pushover API: the same endpoints, parameters, JSON responses and key formats. Code and libraries written for Pushover work once they point at PushPiper and use a PushPiper app token and user key.

SettingValue
Base URLhttps://api.pushpiper.com/1/ (also served at https://pushpiper.com/1/)
Request bodiesapplication/x-www-form-urlencoded, multipart/form-data or application/json
ResponsesJSON. Every path ends in .json.
AuthenticationYour app token in the token parameter

Every response carries status (1 on success, 0 on failure) and a unique request id. Include the request id when you write to support.

Keys and tokens

Credentials are 30 case-sensitive characters from [A-Za-z0-9], like Pushover's. The first letter tells you what kind of key you hold.

KeyStarts withUsed for
App tokenaAuthenticates API calls for one app
User keyuDelivers to one person's devices
Group keygDelivers to every member of a group
Subscribed user keysDelivers to someone who subscribed to your app
Team tokentAuthenticates the Teams API
ReceiptrTracks one emergency message

User, group and subscribed keys are interchangeable in the user parameter. Your code doesn't need to know which one it holds.

Send a message

POST/1/messages.json

Send token, user and message. Everything else is optional.

curl -s \
  --form-string "token=APP_TOKEN" \
  --form-string "user=USER_KEY" \
  --form-string "message=Backup finished in 42 seconds." \
  https://api.pushpiper.com/1/messages.json

An accepted message returns HTTP 200:

Response
{
  "status": 1,
  "request": "01K73TZ9PB205722QDN3QK4H0R"
}

Parameters

ParameterDescription
token requiredYour app token.
user requiredA user, group or subscribed key, or up to 50 keys separated by commas (no spaces).
message requiredThe message body, up to 1024 characters.
titleUp to 250 characters. Defaults to your app's name.
deviceDevice names to deliver to, separated by commas. Each name is up to 25 characters of [A-Za-z0-9_-].
priority-2, -1, 0 (default), 1 or 2. See priorities.
soundOne of the built-in sounds.
urlA supplementary URL, up to 512 characters. javascript:, vbscript: and data: URLs are refused.
url_titleLink text for url, up to 100 characters. Without it, the URL itself is shown.
html1 to render the HTML subset.
monospace1 to render the message in a monospace font. Can't be combined with html.
timestampA Unix timestamp to show instead of the time we received the message.
ttlSeconds until the message is deleted from devices. A positive number, ignored at priority 2.
retryPriority 2 only: seconds between repeats, at least 30.
expirePriority 2 only: seconds to keep repeating, at most 10800.
callbackPriority 2 only: a URL we POST to when someone acknowledges the message.
tagsPriority 2 only: tags separated by commas, for cancel by tag.
attachmentOne file of up to 5,242,880 bytes, sent as multipart/form-data. See attachments.
attachment_base64The same file as a Base64 string, for clients without multipart support.
attachment_typeThe MIME type of the attachment, such as image/jpeg. Required with attachment_base64.
encrypted1 when message, title, url and url_title are end-to-end encrypted.

With encrypted=1, limits apply to your plaintext, before encryption.

Recipients and devices

  • Send to one key, or to up to 50 keys at once as a comma-separated list. A group holds up to 500 members, and one request reaches at most 1,000 users once groups are expanded.
  • Each user a request reaches counts as one message against your monthly quota, however many of its keys and groups include them.
  • device limits delivery to the devices with those names. When none of them exists, every device gets the message, so nothing is lost.
  • With several keys, or a group that isn't owned by a team, device is ignored and group members keep the device set in the group. A single team-owned group honours device and fails when no member device matches.

Priorities

PriorityNameWhat happens
-2LowestNo notification. The message waits in the inbox.
-1LowA silent notification.
0NormalA notification with sound. Silent during the device's quiet hours.
1HighA high-urgency notification that ignores quiet hours.
2EmergencyLike high, and repeats until someone acknowledges it or it expires.

Emergency messages

Priority 2 needs retry and expire. We resend the message every retry seconds (at least 30) until someone acknowledges it or expire seconds pass (at most 10800, three hours). Repeats stop after 50, so retry=30 with expire=10800 ends after 25 minutes.

cURL
curl -s \
  --form-string "token=APP_TOKEN" \
  --form-string "user=USER_KEY" \
  --form-string "message=Database is down" \
  --form-string "priority=2" \
  --form-string "retry=60" \
  --form-string "expire=3600" \
  --form-string "callback=https://example.com/pushpiper/ack" \
  --form-string "tags=db,eu-west" \
  https://api.pushpiper.com/1/messages.json

The response adds a receipt. Use it to check the status or cancel the repeats.

Response
{
  "status": 1,
  "request": "01K73TZ9PB205722QDN3QK4H0R",
  "receipt": "rp9p37JFID44oNn0puvoqINYzuDq9R"
}

HTML and monospace

With html=1 a message can use <b>, <i>, <u>, <font color="..."> and <a href="...">. With monospace=1 it renders in a fixed-width font. Use one or the other. Notifications show plain text, and the formatting appears in the inbox.

Sounds

Set sound to one of the 23 built-in sounds below. Names we don't know, like Pushover custom sounds, are ignored. A subscriber's chosen sound beats the message's, which beats the device default. Browsers play the system sound for background notifications, so the chosen sound plays when PushPiper is open.

GET/1/sounds.json?token=APP_TOKEN

Response
{
  "status": 1,
  "sounds": {
    "pushover": "Piper (default)",
    "bike": "Bike",
    "bugle": "Bugle",
    "cashregister": "Cash Register",
    "classical": "Classical",
    "cosmic": "Cosmic",
    "falling": "Falling",
    "gamelan": "Gamelan",
    "incoming": "Incoming",
    "intermission": "Intermission",
    "magic": "Magic",
    "mechanical": "Mechanical",
    "pianobar": "Piano Bar",
    "siren": "Siren",
    "spacealarm": "Space Alarm",
    "tugboat": "Tug Boat",
    "alien": "Alien Alarm (long)",
    "climb": "Climb (long)",
    "persistent": "Persistent (long)",
    "echo": "Piper Echo (long)",
    "updown": "Up Down (long)",
    "vibrate": "Vibrate Only",
    "none": "None (silent)"
  },
  "request": "01K73TZ9PB205722QDN3QK4H0R"
}

Attachments

Attach one file of up to 5,242,880 bytes (5 MB) per message: an image, PDF, plain text, audio or MP4 video. Send it as the attachment field of a multipart request, or Base64-encode it into attachment_base64 together with attachment_type. Attachments are not end-to-end encrypted.

curl -s \
  --form-string "token=APP_TOKEN" \
  --form-string "user=USER_KEY" \
  --form-string "message=Someone is at the door" \
  -F "attachment=@doorbell.jpg;type=image/jpeg" \
  https://api.pushpiper.com/1/messages.json

Receipts and callbacks

GET/1/receipts/{receipt}.json?token=APP_TOKEN

Every emergency message returns a receipt you can query for 7 days. Poll no more than once every 5 seconds, or pass a callback and let us tell you.

cURL
curl -s "https://api.pushpiper.com/1/receipts/rp9p37JFID44oNn0puvoqINYzuDq9R.json?token=APP_TOKEN"
Response
{
  "status": 1,
  "acknowledged": 1,
  "acknowledged_at": 1791555060,
  "acknowledged_by": "uwe7hiuHJyLkzwwO8qonyEPzYjJbI4",
  "acknowledged_by_device": "pixel",
  "last_delivered_at": 1791555000,
  "expired": 0,
  "expires_at": 1791558600,
  "called_back": 1,
  "called_back_at": 1791555061,
  "request": "01K73TZ9PB205722QDN3QK4H0R"
}
FieldMeaning
acknowledged1 once someone acknowledged the message, else 0
acknowledged_atUnix timestamp of the acknowledgement, or 0
acknowledged_byUser key of the first person to acknowledge
acknowledged_by_deviceName of the device they acknowledged on
last_delivered_atUnix timestamp of the latest repeat, or 0
expired1 once expire has passed, else 0
expires_atUnix timestamp when the repeats stop
called_back1 once we called your callback URL, else 0
called_back_atUnix timestamp of that call, or 0

Callbacks

When the first person acknowledges, we POST receipt, acknowledged=1, acknowledged_at, acknowledged_by and acknowledged_by_device to your callback URL. Answer with any 2xx. Otherwise we retry after 1 minute, 5 minutes, 15 minutes, 1 hour and 6 hours.

Callback URLs must be public http or https URLs. We wait up to 10 seconds for an answer and don't follow redirects.

Cancel repeats

POST/1/receipts/{receipt}/cancel.json

POST/1/receipts/cancel_by_tag/{tag}.json

Send your token. Cancelling stops the repeats and the message stays in the inbox. Cancel by tag stops every active emergency message from your app that carries the tag.

cURL
curl -s --form-string "token=APP_TOKEN" \
  https://api.pushpiper.com/1/receipts/rp9p37JFID44oNn0puvoqINYzuDq9R/cancel.json

curl -s --form-string "token=APP_TOKEN" \
  https://api.pushpiper.com/1/receipts/cancel_by_tag/eu-west.json

Validate a key

POST/1/users/validate.json

Check a user or group key before you store it. Send token, user and optionally device. A key with at least one active device (or the named device) returns status: 1 and the device names. group is 1 for a group key.

cURL
curl -s \
  --form-string "token=APP_TOKEN" \
  --form-string "user=USER_KEY" \
  https://api.pushpiper.com/1/users/validate.json
Response
{
  "status": 1,
  "group": 0,
  "devices": [
    "pixel",
    "work-laptop"
  ],
  "licenses": [],
  "request": "01K73TZ9PB205722QDN3QK4H0R"
}

licenses is always empty because PushPiper has no per-platform licenses. For subscribed keys devices is empty as well, so publishers never learn about a subscriber's devices.

Message limits

GET/1/apps/limits.json?token=APP_TOKEN

Quotas belong to your account and every app on it shares them: 10,000 messages a month on Free, 50,000 on Pro and 25,000 per team. Each message pack adds 10,000. Quotas reset on the 1st of the month at 00:00 UTC.

Response
{
  "status": 1,
  "limit": 10000,
  "remaining": 7496,
  "reset": 1793491200,
  "request": "01K73TZ9PB205722QDN3QK4H0R"
}

Every send response carries the same numbers as headers:

Headers
X-Limit-App-Limit: 10000
X-Limit-App-Remaining: 7496
X-Limit-App-Reset: 1793491200

Once the quota is used up, sends return HTTP 429 until the reset, or until you upgrade or add a message pack.

Groups

A group puts up to 500 members behind one key, and an account can have up to 100 groups. Manage groups in the app or with these endpoints. Every call takes the token of an app on the account that owns the group. Group names are unique per account and up to 100 characters, without line breaks or tabs.

EndpointParametersResult
POST /1/groups.jsonnameCreates a group and returns its key as group
GET /1/groups.jsonnoneLists your groups as groups: [{group, name}]
GET /1/groups/{group}.jsonnoneReturns name and users: [{user, device, memo, disabled}]
POST /1/groups/{group}/add_user.jsonuser, device, memoAdds a member. A user can be added once per device. memo is up to 200 characters, without line breaks or tabs.
POST /1/groups/{group}/remove_user.jsonuser, deviceRemoves a member
POST /1/groups/{group}/disable_user.jsonuser, devicePauses delivery to a member
POST /1/groups/{group}/enable_user.jsonuser, deviceResumes delivery to a member
POST /1/groups/{group}/rename.jsonnameRenames the group

Without device, remove_user, disable_user and enable_user act on every membership of that user.

cURL
curl -s \
  --form-string "token=APP_TOKEN" \
  --form-string "name=On-call" \
  https://api.pushpiper.com/1/groups.json

curl -s \
  --form-string "token=APP_TOKEN" \
  --form-string "user=USER_KEY" \
  --form-string "memo=Sam, weekends" \
  https://api.pushpiper.com/1/groups/gPCVx8nEzqidMVw8BxfNWiDn2qwHvq/add_user.json
Response
{
  "status": 1,
  "group": "gPCVx8nEzqidMVw8BxfNWiDn2qwHvq",
  "request": "01K73TZ9PB205722QDN3QK4H0R"
}

Each team has a Team Delivery Group that always matches its members. It's read-only in the API and the app.

Subscriptions

Subscriptions let people sign up for your app's notifications without handing you their user key. Turn them on for an app and you get a subscribe URL:

Subscribe URL
https://pushpiper.com/subscribe/Doorbell-6LbSiA4aGDvPWS8A
  • Group subscriptions add each subscriber to a group you pick. Send to the group key to reach everyone.
  • Web subscriptions send the visitor back to your site with a subscribed user key. Add a success URL and optionally a failure URL, both URL-encoded and starting with the base URL you registered.

After subscribing, we redirect to success with pushover_user_key=<key> added. Store the key and send to it like any user key. Unsubscribing redirects with an empty pushover_user_key, plus pushover_unsubscribed=1 and pushover_unsubscribed_user_key=<key>. Revoked keys and keys of other apps return user: "invalid".

Migrate existing users

POST/1/subscriptions/migrate.json

Turns user keys you already have into subscribed keys, without a confirmation step. Send token, subscription (your subscription code), user, and optionally device_name and sound. When the user already subscribes, you get the existing key back.

cURL
curl -s \
  --form-string "token=APP_TOKEN" \
  --form-string "subscription=Doorbell-6LbSiA4aGDvPWS8A" \
  --form-string "user=USER_KEY" \
  https://api.pushpiper.com/1/subscriptions/migrate.json
Response
{
  "status": 1,
  "subscribed_user_key": "sws17LPwYBZoF2uKoc0uT7AjLTauT3",
  "request": "01K73TZ9PB205722QDN3QK4H0R"
}

Teams

Manage team members with the team token from your team page. App tokens are refused with team token is invalid.

EndpointParametersResult
GET /1/teams.jsontokenThe team name and its users, each with their devices
POST /1/teams/add_user.jsontoken, email, name, admin, group, instant, passwordInvites someone by email. group also adds them to that group. Someone already in the team is left as they are.
POST /1/teams/revoke_invitation.jsontoken, emailCancels a pending invitation
POST /1/teams/remove_user.jsontoken, emailRemoves a member. The last administrator can't be removed.

Invitations always arrive as a join link that's valid for 7 days, and members sign in with their own account. password is accepted and ignored, and instant sends the same join link. An address gets one invitation email a minute and a team 1,000 a day; past either, add_user.json answers 429 with Retry-After.

GET /1/teams.json
{
  "status": 1,
  "name": "Acme Ops",
  "users": [
    {
      "id": "01J9ZQ4M7X2K8T5R3W6N1P0VBC",
      "name": "Sam Rivera",
      "email": "sam@example.com",
      "administrator": true,
      "devices": [
        {
          "name": "pixel",
          "os": "Android",
          "os_version": "16",
          "enabled": true,
          "administratively_disabled": false
        }
      ]
    }
  ],
  "request": "01K73TZ9PB205722QDN3QK4H0R"
}

When a team's subscription is inactive, sends from its apps and add_user.json return team subscription is inactive.

End-to-end encryption

Send encrypted=1 with message, title, url and url_title encrypted under a 256-bit key that only your devices know. We store and deliver ciphertext, and the PushPiper app decrypts it on your devices. The format is the same as Pushover's, and limits apply to your plaintext, before encryption.

Read the encryption guide, with shell, Node.js and Python recipes.

Email gateway

Every user gets a private address like eswgicvoru448657jqep@pushpiper.app. Mail sent to it becomes a message: the subject is the title (250 characters) and the plain-text body is the message (1024 characters). Aliases can have their own priority, from -2 to 1, and sound.

Mail that only has HTML is turned into text, and quoted replies and signatures are left out. The first PNG, JPEG, GIF or WebP attachment up to 5 MB comes with the message, so camera snapshots work like they do on Pushover. When nothing is attached, the first image in the body is used instead. Images under 1 KB, like tracking pixels, are skipped.

The email's Date header becomes the message time when it's no more than 7 days old. Each address takes mail up to 10 MB and up to 30 emails a minute.

Mail bounces back to the sender when the address is unknown or disabled, the account is out of messages, the email is too large or the address gets too many emails.

Emailed messages are never end-to-end encrypted, and they count against your personal quota.

Errors

A refused request returns status: 0, an errors array you can show to people, and the name of the parameter that failed:

Response
{
  "user": "invalid",
  "errors": [
    "user identifier is invalid"
  ],
  "status": 0,
  "request": "01K73TZ9PB205722QDN3QK4H0R"
}
HTTP statusMeaningWhat to do
200Accepted, status is 1Nothing. It's on its way.
400Invalid input: a bad token or key, or a missing or too long parameterFix the request. Sending it again won't help.
429The monthly quota is used upWait for the reset, upgrade or add a message pack.
5xx or no answerA temporary problem on our sideRetry after at least 5 seconds, with backoff.

Rate limits

The monthly quota is the limit that matters. To damp floods, each app token can also make up to 600 requests a minute, and each IP address gets 30 failed authentications a minute. Reuse connections (HTTP keep-alive) when you send many messages in a row.

Differences from Pushover

  • Responses are JSON only. Paths ending in .xml aren't supported.
  • Quotas reset on the 1st at 00:00 UTC instead of US Central time.
  • Accounts hold up to 100 groups and groups up to 500 members. One request reaches at most 1,000 users once groups are expanded.
  • url refuses javascript:, vbscript: and data: URLs.
  • users/validate returns no devices for subscribed keys, and licenses is always empty.
  • Teams add_user ignores password, and instant sends a join link instead of an instant login.
  • Attachments can be PDF, plain text, audio or MP4 video as well as images.