REST API reference
Base URL: //api.pushnotificationservice.com/v1. All responses are JSON.
The API is versioned in the path. A machine-readable
OpenAPI 3.1 description of everything on this page
is also available.
Authentication
Create an API key in the dashboard under API keys. The key is shown once. Store it securely. Authenticate with a bearer token:
Authorization: Bearer pns_xxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Keys are pns_ followed by a 12-character prefix and a 40-character secret.
Keys are scoped to your account: you can only act on sites your account owns. Requests are
rate-limited to 120/min per key. Unauthenticated requests get 401.
Send a notification
POST /v1/sites/{siteId}/notifications
| Field | Type | Notes |
|---|---|---|
title | string | Required. Max 120 chars. |
body | string | Required. Max 500 chars. |
url | string | Click destination. Tracked via signed redirect. |
icon, image | string | Optional HTTPS image URLs. |
tags | string[] | Target subscribers with any of these tags (max 10). Mutually exclusive with subscribers. |
subscribers | string[] | Target specific subscriber IDs (max 1000). |
platforms | string[] | Restrict delivery to web, ios, and/or android. Mutually exclusive with device_tokens. |
device_tokens | string[] | Target specific device token IDs (max 1000). Mutually exclusive with platforms. |
scheduled_for | ISO 8601 | Future time to send. Omit to send immediately. |
Omitting both tags and subscribers targets all active subscribers.
Omitting platforms targets every platform a subscriber is on — web, iOS, and Android.
Omit these fields entirely to get that behavior — an empty array (e.g. "tags": [])
is rejected with a 422 rather than treated the same as omitting the field.
Quota is checked once, when you create (or restore) a notification — never again after
that. A scheduled notification, or one that's already sending, always completes against
its full live audience, even if more subscribers join in the meantime: we don't cancel or
truncate a send you've already started because your account grew. Only creating a new
notification can be rejected with a 429 for insufficient quota, and only true
deliveries count against your monthly quota — a failed send costs nothing.
curl -X POST \
//api.pushnotificationservice.com/v1/sites/SITE_ID/notifications \
-H "Authorization: Bearer pns_live_..." \
-H "Content-Type: application/json" \
-d '{
"title": "Flash sale",
"body": "30% off for the next hour.",
"url": "https://example.com/sale",
"tags": ["offers"]
}'
{
"data": {
"id": "9b1f...",
"title": "Flash sale",
"status": "queued",
"counts": { "targeted": 0, "delivered": 0, "displayed": 0, "failed": 0, "clicked": 0 }
}
}
Delivery is asynchronous: the send returns queued (or scheduled)
and the counts fill in as workers deliver. Poll the campaign endpoint below for live counts.
delivered counts push-service acceptance; displayed counts
notifications the service worker actually showed on a device.
Other notification endpoints
| Method & path | Description |
|---|---|
GET /v1/sites/{siteId}/notifications | List campaigns (newest first, 25 per page, ?page=). |
GET /v1/sites/{siteId}/notifications/{id} | Fetch one campaign with live delivery counts. |
Subscribers
| Method & path | Description |
|---|---|
GET /v1/sites/{siteId}/subscribers | List subscribers. Filters: ?status=active, ?tag=news, ?per_page= (default 50, max 200). |
POST /v1/sites/{siteId}/subscribers/{id}/tags | Add/remove tags: {"add":["vip"],"remove":["trial"]} (max 20 each). |
DELETE /v1/sites/{siteId}/subscribers/{id} | Unsubscribe a subscriber. |
GET /v1/sites/{siteId}/tags | List tags with subscriber counts. |
Mobile device tokens
Native iOS and Android apps register their own push token so campaigns can target them
alongside (or instead of) web subscribers. These endpoints are public — no API key —
the same way the SDK's web subscription endpoints are, since a native app has no
Origin header for CORS to check against. Each site needs its APNs and/or FCM
credentials configured once in the dashboard before sends actually deliver; registration
itself is accepted either way, so a token is never lost to a timing gap.
| Method & path | Description |
|---|---|
POST /v1/sites/{siteId}/device-tokens | Register (or refresh) a device token: {"platform":"ios","token":"..."}. Idempotent; pass old_token to migrate a rotated token in place. |
POST /v1/sites/{siteId}/device-tokens/unregister | Opt out: {"platform":"ios","token":"..."}. Idempotent and quiet about unknown tokens. |
curl -X POST \
//api.pushnotificationservice.com/v1/sites/SITE_ID/device-tokens \
-H "Content-Type: application/json" \
-d '{
"platform": "ios",
"token": "the 64-character hex APNs device token"
}'
platform is ios or android. iOS tokens are 64-200
hex characters; Android (FCM) tokens are long base64url-ish strings. An optional
bundle_id field, if sent, must match the site's configured APNs bundle ID or
the request is rejected — a lightweight check against token stuffing, since there's no
Origin header to rely on here.
tags cannot target device tokens (there's no per-device tagging yet) — a
tag-scoped send only ever reaches web subscribers, and combining tags with
platforms: ["ios"]/["android"] is rejected as a 422.
The image field on a notification displays automatically on Android; on iOS
it requires the app to ship its own Notification Service Extension (a standard APNs
mutable-content requirement).
Signed links in notifications
Every delivered notification carries signed URLs generated per campaign or subscriber. You never build these yourself, but you'll see them in traffic and logs:
| Method & path | Description |
|---|---|
GET /c/{campaignId} | Click-through redirect. Counts the click, then 302s to the campaign's url (or the site's domain). No cookies, no per-user history. |
GET /d/{campaignId} | Display receipt pinged by the service worker after it shows the notification. Campaign-scoped, carries no subscriber identity. |
GET /unsubscribe/{subscriberId} | One-click unsubscribe page. Works without JavaScript or CORS. |
All of them verify a URL signature and reject tampered links with 403. The
browser-facing subscription endpoints used by the SDK snippet
(/v1/sites/{siteId}/config, /v1/sites/{siteId}/subscriptions…)
are internal to the snippet and documented on the
snippet page.
Errors & limits
| Status | Meaning |
|---|---|
401 | Missing or invalid API key. |
403 | Account suspended. |
404 | Site or resource not found, or not owned by your account. |
422 | Validation error (see errors in the body). |
429 | Rate limit hit, or monthly push quota exceeded. |