← PushPig
API reference
Every endpoint, parameter, scope and error code of the PushPig REST API.
PushPig API
Base URL: https://pushpig.de
Authentication
The API supports two authentication methods:
1. Session cookie (browser)
After login the server sets a pp_session cookie automatically. Every request from the browser is authenticated with it.
2. API key (bearer token)
For scripts, automations and external integrations. The key goes into the Authorization header:
Authorization: Bearer pp_<key>
API keys are created in the settings under API keys. A key is shown only once, right after you create it, so store it immediately.
Scopes
Every key carries scopes that define which actions it may perform:
| Scope |
Meaning |
push:send |
Send push messages (to every channel you can write to) |
push:send:<channel_id> |
Send to one specific channel only |
channels:read |
Read channels and events |
channels:read:<channel_id> |
Read one specific channel only |
channels:create |
Create new channels |
channels:update |
Edit channels (own channels only) |
channels:update:<channel_id> |
Edit one specific channel only |
channels:delete |
Delete channels (own channels only) |
channels:delete:<channel_id> |
Delete one specific channel only |
At least one scope is required when creating a key. A key without scopes has no access at all — every request is rejected with HTTP 403.
A scope without a channel id covers every id-qualified variant, but not the other way round:
push:send allows sending to any channel you can write to.
push:send:42 allows sending to channel 42 only. It covers neither channel 99 nor "all channels".
The same applies to channels:read, channels:update and channels:delete.
Scopes never widen your permissions: editing and deleting stay restricted to the channel owner, sending requires write access, and the plan limits (free: 2 channels) apply over the API as well.
Sending push messages
POST /api/push/send
Sends a message to a channel. Every subscriber of that channel receives it in real time.
Authentication: session cookie or bearer token (scope push:send)
Request:
{
"channel": "general",
"title": "Optional title",
"body": "Message text",
"send_at": "2026-12-24T18:00:00",
"urgent": false,
"actions": [
{ "title": "Open", "url": "https://example.com" }
],
"payload": { "custom": "data" }
}
| Field |
Type |
Required |
Description |
channel |
string |
✓* |
Slug of the channel |
channel_id |
integer |
✓* |
Alternative to channel |
body |
string |
✓ |
Message text (max. 4096 characters) |
title |
string |
– |
Optional subject (max. 255 characters) |
actions |
array |
– |
Up to 2 action buttons with title + url |
send_at |
string |
– |
ISO 8601 timestamp for delayed delivery (at least 30 s in the future) |
urgent |
boolean |
– |
If true, the push bypasses the recipients' quiet hours |
payload |
object |
– |
Arbitrary JSON payload |
* Either channel or channel_id is required.
Response (200):
{
"success": true,
"event_id": 42,
"fcm_sent": 3,
"webpush_sent": 1,
"email_sent": 2,
"deferred": 0
}
deferred is the number of recipients whose quiet hours are currently active; the push is delivered to them automatically once the window ends.
Examples:
curl -X POST https://pushpig.de/api/push/send \
-H "Authorization: Bearer pp_yourkey" \
-H "Content-Type: application/json" \
-d '{"channel": "server", "body": "Server is back up!"}'
await fetch("https://pushpig.de/api/push/send", {
method: "POST",
headers: {
"Authorization": "Bearer pp_yourkey",
"Content-Type": "application/json"
},
body: JSON.stringify({ channel: "general", title: "Alert", body: "Disk almost full!" })
});
import requests
requests.post(
"https://pushpig.de/api/push/send",
headers={"Authorization": "Bearer pp_yourkey"},
json={"channel": "general", "title": "Alert", "body": "Disk almost full!"}
)
<?php
$ch = curl_init("https://pushpig.de/api/push/send");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer pp_yourkey",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"channel" => "general",
"title" => "Alert",
"body" => "Disk almost full!",
]),
]);
curl_exec($ch);
rest_command:
pushpig_notify:
url: https://pushpig.de/api/push/send
method: POST
headers:
Authorization: "Bearer pp_yourkey"
Content-Type: application/json
payload: '{"channel": "system", "title": "{{ title }}", "body": "{{ message }}"}'
POST /api/push/test
Sends a test push to your own devices only (the current user's FCM tokens and web push subscriptions). Handy for verifying your own push setup.
Authentication: session cookie
Request:
{ "channel_id": 1 }
Response (200):
{
"success": true,
"fcm_sent": 1,
"webpush_sent": 1,
"total": 2
}
Templates
Save recurring messages as templates.
GET /api/push/templates/list
Returns every template visible to the user (their own plus channel-shared ones they can write to).
Response (200):
{
"templates": [
{
"id": 1,
"name": "Deploy done",
"title": "Deploy {version}",
"body": "{env} is live.",
"channel_id": null,
"channel_slug": null,
"is_owner": true,
"author_name": "alice",
"updated_at": "2026-05-15 20:00:00"
}
]
}
POST /api/push/templates/create
Request:
{
"name": "Deploy done",
"title": "Deploy {version}",
"body": "{env} is live.",
"channel_id": null,
"actions": []
}
Omit channel_id or set it to null for a personal template. With a channel_id it is shared with everyone who can write to that channel.
POST /api/push/templates/update
Body: { "id": 1, "name": "...", "body": "...", ... }
POST /api/push/templates/delete
Body: { "id": 1 }
Bulk send
Mass delivery with per-recipient variables.
POST /api/channels/bulk/create
Request:
{
"channel_id": 1,
"title_template": "Hi {name}!",
"body_template": "You won {prize}.",
"csv": "username,name,prize\nalice,Alice,100 EUR\nbob,Bob,50 EUR"
}
The username column is mandatory. Every other column becomes a placeholder. Recipients who have not subscribed to the channel are skipped.
Response (201):
{
"success": true,
"job_id": 42,
"total": 2,
"resolved": 2,
"skipped": 0
}
The job is processed asynchronously in the background. Check its status:
GET /api/channels/bulk/status?job_id=42
GET /api/channels/bulk/list?channel_id=1
POST /api/channels/bulk/cancel – { "job_id": 42 }
GET /api/channels/bulk/recipients?job_id=42&status=skipped
Limits: max. 5000 rows per CSV, max. 5 bulk jobs per user per hour.
Quiet hours
POST /api/channels/quiet-hours
Sets the current user's do-not-disturb window for a channel.
Request:
{
"channel_id": 1,
"quiet_start": "22:00",
"quiet_end": "07:00"
}
Both null removes the quiet hours. Format HH:MM. If quiet_end < quiet_start, the window spans midnight (e.g. 22:00 → 07:00).
The time zone is read from the user profile.
POST /api/auth/profile/timezone
Sets the user's IANA time zone:
{ "timezone": "Europe/Berlin" }
null falls back to the server default.
Webhooks
Channel owners can register external URLs that are called on specific events.
Supported events
| Event |
When |
push.sent |
Push delivered to at least one recipient |
push.failed |
Push reached no recipient (no subscribers / all tokens expired) |
channel.subscribed |
New subscriber in the channel |
channel.unsubscribed |
Subscriber opted out |
bulk.completed |
Bulk job finished |
Verifying the signature
Every webhook request carries these headers:
Content-Type: application/json
X-PushPig-Event: push.sent
X-PushPig-Delivery: 8a4f9b21c0d3...
X-PushPig-Idempotency-Key: 8a4f9b21c0d3...
X-PushPig-Timestamp: 1747333200
X-PushPig-Signature: sha256=<HMAC-SHA256("<timestamp>.<raw_body>", webhook_secret)>
What is signed is "<X-PushPig-Timestamp>.<raw_body>" (Stripe style), not the body
alone. The timestamp is fresh for every send attempt.
Replay protection: receivers should reject requests whose X-PushPig-Timestamp
deviates more than 300 s from their own clock, so an intercepted request cannot be
replayed at will later. Genuine retries carry a new, valid timestamp and still pass.
Idempotency: X-PushPig-Delivery (= X-PushPig-Idempotency-Key) is identical
across every retry of the same event. Receivers should store this value and discard
deliveries they already processed, to rule out double processing.
Receiver-side verification (bash):
# $TS = X-PushPig-Timestamp, $BODY = raw request body, $SIG = X-PushPig-Signature
# 1) check freshness (replay protection)
now=$(date +%s)
(( now - TS < 300 && TS - now < 300 )) || exit 1
# 2) recompute the signature over "<timestamp>.<body>"
expected=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $2}')
[[ "sha256=$expected" == "$SIG" ]] || exit 1
Example payload
{
"event": "push.sent",
"channel_id": 42,
"timestamp": "2026-05-15T20:00:00+00:00",
"data": {
"event_id": 1234,
"channel_slug": "deploys",
"title": "Deploy succeeded",
"body": "Version 2.4.1 is live.",
"fcm_sent": 3,
"webpush_sent": 1,
"email_sent": 0
}
}
Retry policy
Failed deliveries (HTTP ≠ 2xx or timeout) are retried up to 5 times with a backoff of 30 s → 5 min → 30 min → 1 h. HTTP timeout: 8 s. Every retry carries the same X-PushPig-Delivery ID (idempotency) but a fresh X-PushPig-Timestamp with a matching signature.
Management endpoints
POST /api/channels/webhooks/create – { channel_id, url, events: [...] }, returns the secret once
GET /api/channels/webhooks/list?channel_id=1
POST /api/channels/webhooks/delete – { id }
POST /api/channels/webhooks/test – { id }, synchronous test call
Channels
GET /api/channels
Lists every channel the user is subscribed to. For each channel the response also carries that user's quiet_start/quiet_end.
Authentication: session cookie or bearer token (scope channels:read)
Response (200):
{
"channels": [
{
"id": 1,
"name": "General",
"slug": "general",
"description": "General notifications",
"is_public": true,
"is_owner": false,
"can_write": false,
"subscribed": true,
"email_notify": false,
"push_notify": true,
"quiet_start": "22:00",
"quiet_end": "07:00",
"subscriber_count": 3,
"unread_count": 0
}
]
}
GET /api/channels/events?channel_id=1
Lists the channel's most recent events. Each event carries the delivery counters fcm_sent, webpush_sent, email_sent.
Authentication: session cookie or bearer token (scope channels:read or channels:read:<channel_id>)
POST /api/channels/create
Creates a channel. The caller automatically becomes its owner and subscriber. The slug is derived from the name and must be unique across the system.
Authentication: session cookie or bearer token (scope channels:create, confirmed email address required)
Request:
{ "name": "My channel", "description": "...", "is_public": true }
| Field |
Type |
Required |
Meaning |
name |
string |
yes |
Display name, max. 100 characters |
description |
string |
no |
Description |
is_public |
bool |
no |
Defaults to true, publicly discoverable |
Response (201):
{ "success": true, "channel": { "id": 7, "name": "My channel", "slug": "my-channel" } }
If the plan's channel limit is reached, the endpoint responds with HTTP 402.
POST /api/channels/update
Changes a channel. Owner only. Only the fields you send are changed, at least one is required.
Authentication: session cookie or bearer token (scope channels:update or channels:update:<channel_id>)
Request:
{ "id": 7, "name": "New name", "description": "...", "is_public": false }
| Field |
Type |
Required |
Meaning |
id |
int |
yes |
Channel id |
name |
string |
no |
New name, max. 100 characters |
description |
string|null |
no |
New description, max. 500 characters, null clears it |
is_public |
bool |
no |
Visibility |
The slug stays unchanged on rename, so existing integrations keep working.
Response (200):
{ "success": true }
POST /api/channels/delete
Permanently deletes a channel including its subscriptions and events. Owner only, and not reversible.
Authentication: session cookie or bearer token (scope channels:delete or channels:delete:<channel_id>)
Request:
{ "id": 7 }
Response (200):
{ "success": true, "message": "Channel deleted." }
POST /api/channels/subscribe
Subscribes or unsubscribes:
{ "channel_id": 1 }
To unsubscribe, add _method: "DELETE".
Invite links
POST /api/channels/invite-link/create – { channel_id }, creates a single-use token (valid 7 days) and returns the invite URL
GET /api/channels/invite-link/list?channel_id=1 – open links
POST /api/channels/invite-link/revoke – { id }, revoke a link
POST /api/channels/join – { token }, redeem an invite
SSE – real-time receiving
GET /api/sse/stream
Opens a Server-Sent Events stream. The browser receives push messages in real time for as long as the connection stays open.
Authentication: session cookie
This endpoint is meant for browser clients. External integrations only need to send via /api/push/send.
Event format:
data: {"type":"message","channel":"general","title":"Hello","body":"Test message","created_at":"2026-04-24T10:00:00Z"}
Errors
Every error is returned as JSON:
{ "error": "Error description" }
| HTTP code |
Meaning |
| 400 |
Bad request (missing/invalid parameters) |
| 401 |
Not authenticated |
| 403 |
Not permitted (e.g. missing scope or no write access) |
| 404 |
Resource not found |
| 405 |
HTTP method not allowed |
| 409 |
Conflict (e.g. invite already redeemed) |
| 410 |
Gone (invite or token expired) |
| 429 |
Rate limit exceeded |
| 500 |
Server error |
Help & API ·
Pricing ·
Contact ·
Imprint ·
Privacy