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.
| Setting | Value |
|---|---|
| Base URL | https://api.pushpiper.com/1/ (also served at https://pushpiper.com/1/) |
| Request bodies | application/x-www-form-urlencoded, multipart/form-data or application/json |
| Responses | JSON. Every path ends in .json. |
| Authentication | Your 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.
| Key | Starts with | Used for |
|---|---|---|
| App token | a | Authenticates API calls for one app |
| User key | u | Delivers to one person's devices |
| Group key | g | Delivers to every member of a group |
| Subscribed user key | s | Delivers to someone who subscribed to your app |
| Team token | t | Authenticates the Teams API |
| Receipt | r | Tracks 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.jsonimport requests
response = requests.post(
"https://api.pushpiper.com/1/messages.json",
data={
"token": "APP_TOKEN",
"user": "USER_KEY",
"message": "Backup finished in 42 seconds.",
},
timeout=10,
)
print(response.status_code, response.json())const response = await fetch("https://api.pushpiper.com/1/messages.json", {
method: "POST",
body: new URLSearchParams({
token: "APP_TOKEN",
user: "USER_KEY",
message: "Backup finished in 42 seconds.",
}),
});
console.log(response.status, await response.json());package main
import (
"fmt"
"io"
"net/http"
"net/url"
)
func main() {
resp, err := http.PostForm("https://api.pushpiper.com/1/messages.json", url.Values{
"token": {"APP_TOKEN"},
"user": {"USER_KEY"},
"message": {"Backup finished in 42 seconds."},
})
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(resp.Status, string(body))
}<?php
$ch = curl_init("https://api.pushpiper.com/1/messages.json");
curl_setopt_array($ch, [
CURLOPT_POSTFIELDS => [
"token" => "APP_TOKEN",
"user" => "USER_KEY",
"message" => "Backup finished in 42 seconds.",
],
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $response, PHP_EOL;# Add to ~/.profile, then run: push "Backup finished"
push() {
curl -fsS --max-time 10 \
--form-string "token=${PUSHPIPER_TOKEN:?}" \
--form-string "user=${PUSHPIPER_USER:?}" \
--form-string "message=$*" \
https://api.pushpiper.com/1/messages.json > /dev/null
}
./backup.sh && push "Backup finished" || push "Backup failed"An accepted message returns HTTP 200:
{
"status": 1,
"request": "01K73TZ9PB205722QDN3QK4H0R"
}Parameters
| Parameter | Description |
|---|---|
token required | Your app token. |
user required | A user, group or subscribed key, or up to 50 keys separated by commas (no spaces). |
message required | The message body, up to 1024 characters. |
title | Up to 250 characters. Defaults to your app's name. |
device | Device 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. |
sound | One of the built-in sounds. |
url | A supplementary URL, up to 512 characters. javascript:, vbscript: and data: URLs are refused. |
url_title | Link text for url, up to 100 characters. Without it, the URL itself is shown. |
html | 1 to render the HTML subset. |
monospace | 1 to render the message in a monospace font. Can't be combined with html. |
timestamp | A Unix timestamp to show instead of the time we received the message. |
ttl | Seconds until the message is deleted from devices. A positive number, ignored at priority 2. |
retry | Priority 2 only: seconds between repeats, at least 30. |
expire | Priority 2 only: seconds to keep repeating, at most 10800. |
callback | Priority 2 only: a URL we POST to when someone acknowledges the message. |
tags | Priority 2 only: tags separated by commas, for cancel by tag. |
attachment | One file of up to 5,242,880 bytes, sent as multipart/form-data. See attachments. |
attachment_base64 | The same file as a Base64 string, for clients without multipart support. |
attachment_type | The MIME type of the attachment, such as image/jpeg. Required with attachment_base64. |
encrypted | 1 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.
devicelimits 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,
deviceis ignored and group members keep the device set in the group. A single team-owned group honoursdeviceand fails when no member device matches.
Priorities
| Priority | Name | What happens |
|---|---|---|
-2 | Lowest | No notification. The message waits in the inbox. |
-1 | Low | A silent notification. |
0 | Normal | A notification with sound. Silent during the device's quiet hours. |
1 | High | A high-urgency notification that ignores quiet hours. |
2 | Emergency | Like 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 -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.jsonThe response adds a receipt. Use it to check the status or cancel the repeats.
{
"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
{
"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.jsonbase64 < doorbell.jpg | tr -d '\n' > doorbell.b64
curl -s \
--data-urlencode "token=APP_TOKEN" \
--data-urlencode "user=USER_KEY" \
--data-urlencode "message=Someone is at the door" \
--data-urlencode "attachment_base64@doorbell.b64" \
--data-urlencode "attachment_type=image/jpeg" \
https://api.pushpiper.com/1/messages.jsonReceipts 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 -s "https://api.pushpiper.com/1/receipts/rp9p37JFID44oNn0puvoqINYzuDq9R.json?token=APP_TOKEN"{
"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"
}| Field | Meaning |
|---|---|
acknowledged | 1 once someone acknowledged the message, else 0 |
acknowledged_at | Unix timestamp of the acknowledgement, or 0 |
acknowledged_by | User key of the first person to acknowledge |
acknowledged_by_device | Name of the device they acknowledged on |
last_delivered_at | Unix timestamp of the latest repeat, or 0 |
expired | 1 once expire has passed, else 0 |
expires_at | Unix timestamp when the repeats stop |
called_back | 1 once we called your callback URL, else 0 |
called_back_at | Unix 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 -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.jsonValidate 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 -s \
--form-string "token=APP_TOKEN" \
--form-string "user=USER_KEY" \
https://api.pushpiper.com/1/users/validate.json{
"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.
{
"status": 1,
"limit": 10000,
"remaining": 7496,
"reset": 1793491200,
"request": "01K73TZ9PB205722QDN3QK4H0R"
}Every send response carries the same numbers as headers:
X-Limit-App-Limit: 10000
X-Limit-App-Remaining: 7496
X-Limit-App-Reset: 1793491200Once 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.
| Endpoint | Parameters | Result |
|---|---|---|
POST /1/groups.json | name | Creates a group and returns its key as group |
GET /1/groups.json | none | Lists your groups as groups: [{group, name}] |
GET /1/groups/{group}.json | none | Returns name and users: [{user, device, memo, disabled}] |
POST /1/groups/{group}/add_user.json | user, device, memo | Adds 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.json | user, device | Removes a member |
POST /1/groups/{group}/disable_user.json | user, device | Pauses delivery to a member |
POST /1/groups/{group}/enable_user.json | user, device | Resumes delivery to a member |
POST /1/groups/{group}/rename.json | name | Renames the group |
Without device, remove_user, disable_user and enable_user act on every membership of that user.
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{
"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:
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
successURL and optionally afailureURL, 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 -s \
--form-string "token=APP_TOKEN" \
--form-string "subscription=Doorbell-6LbSiA4aGDvPWS8A" \
--form-string "user=USER_KEY" \
https://api.pushpiper.com/1/subscriptions/migrate.json{
"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.
| Endpoint | Parameters | Result |
|---|---|---|
GET /1/teams.json | token | The team name and its users, each with their devices |
POST /1/teams/add_user.json | token, email, name, admin, group, instant, password | Invites 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.json | token, email | Cancels a pending invitation |
POST /1/teams/remove_user.json | token, email | Removes 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.
{
"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:
{
"user": "invalid",
"errors": [
"user identifier is invalid"
],
"status": 0,
"request": "01K73TZ9PB205722QDN3QK4H0R"
}| HTTP status | Meaning | What to do |
|---|---|---|
200 | Accepted, status is 1 | Nothing. It's on its way. |
400 | Invalid input: a bad token or key, or a missing or too long parameter | Fix the request. Sending it again won't help. |
429 | The monthly quota is used up | Wait for the reset, upgrade or add a message pack. |
5xx or no answer | A temporary problem on our side | Retry 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
.xmlaren'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.
urlrefusesjavascript:,vbscript:anddata:URLs.users/validatereturns no devices for subscribed keys, andlicensesis always empty.- Teams
add_userignorespassword, andinstantsends a join link instead of an instant login. - Attachments can be PDF, plain text, audio or MP4 video as well as images.