REST API overview
Base URL, authentication, endpoints, errors, pagination, idempotency and limits of the NotificationStream REST API.
The REST API is what the SDKs use under the hood. Use it directly from any language, or
use the Node SDK. Every endpoint is listed below and
described in full by the OpenAPI 3.1 document (download:
openapi.json, also served live by the API at
https://api.notificationstream.com/v1/openapi.json).
Load it into any OpenAPI tool or client generator for request and response schemas.
Base URL
https://api.notificationstream.com/v1All paths start with /v1. Requests and responses are JSON (UTF-8). All timestamps are
epoch milliseconds (UTC).
Two kinds of endpoints
| Server endpoints | Client endpoints | |
|---|---|---|
| Paths | /v1/notifications, /v1/project-users, /v1/segments, /v1/project | /v1/client/feed, /v1/client/actions, /v1/client/profile, /v1/client/realtime |
| Called from | Your backend | Browsers and mobile apps (the SDKs) |
| Auth | Authorization: Bearer sk_… | X-Publishable-Key: pk_… + Authorization: Bearer <user token> |
| CORS | None (not callable from browsers) | Access-Control-Allow-Origin: * (or your allowed origins), preflights cached |
| Billing | Writes need an active subscription (402); reads keep working | Need an active subscription (402) |
| Monthly requests | Not counted | Counted; over the limit, feed polls get 429 |
Server authentication
curl https://api.notificationstream.com/v1/project \
-H "Authorization: Bearer sk_…"GET /v1/project is a handy way to check a key: it returns the project's id, name, default
locale and publishable key. A missing, invalid or revoked key gets 401 unauthorized.
Client authentication
Every client request sends two headers, and there are no sessions:
curl https://api.notificationstream.com/v1/client/feed \
-H "X-Publishable-Key: pk_…" \
-H "Authorization: Bearer <user token>"- The user token is an ES256 JWT your backend signs with one of the project's signing
keys:
subis your user id,expat most 7 days ahead, andkidoptionally names the key. See Identity verification. - The API acts for the token's
sub. A user's first request creates the project user. - On
401,error.details.reasonsays which check failed (expired,signature, …). Get a fresh token from your backend and retry once. - Bodies are JSON (
Content-Type: application/json; they're parsed as JSON whatever the content type). Browsers preflight these requests and cache the answer per URL. See Polling, realtime and performance.
Endpoints
Request and response schemas for every operation are in the OpenAPI document.
Notifications
Send, schedule, edit and cancel notifications from your backend.
| Method | Path | Purpose |
|---|---|---|
POST | /v1/notifications | Send a notification |
GET | /v1/notifications | List notifications |
GET | /v1/notifications/{id} | Get a notification |
PATCH | /v1/notifications/{id} | Update a draft or scheduled notification |
DELETE | /v1/notifications/{id} | Delete a notification |
POST | /v1/notifications/{id}/cancel | Cancel a notification |
Project users
End users of your app, keyed by your own user id. Profiles, attributes and tags power targeting and templates.
| Method | Path | Purpose |
|---|---|---|
PUT | /v1/project-users/{userId} | Create or update a project user |
GET | /v1/project-users/{userId} | Get a project user |
DELETE | /v1/project-users/{userId} | Delete a project user |
GET | /v1/project-users | List project users |
POST | /v1/project-users/{userId}/tags | Add or remove tags |
GET | /v1/project-users/{userId}/feed | Get a project user's feed |
Segments
Saved rule-based audiences over project user fields, attributes and tags.
| Method | Path | Purpose |
|---|---|---|
GET | /v1/segments | List segments |
POST | /v1/segments | Create a segment |
POST | /v1/segments/preview | Preview segment conditions |
GET | /v1/segments/{id} | Get a segment |
PATCH | /v1/segments/{id} | Update a segment |
DELETE | /v1/segments/{id} | Delete a segment |
Project
| Method | Path | Purpose |
|---|---|---|
GET | /v1/project | Get the project a secret key belongs to |
Client
Used by the SDKs from browsers and mobile apps, with the publishable key and a user token instead of a secret key (see Client authentication). You normally don't call these yourself.
| Method | Path | Purpose |
|---|---|---|
GET | /v1/client/feed | Get the user's feed, counts and the project's client config |
POST | /v1/client/actions | Mark notifications read, unread, archived or clicked |
POST | /v1/client/profile | Sync the user's profile fields and attributes (not tags) |
GET | /v1/client/realtime | WebSocket that says when the user's feed changed (Growth plan and up) |
Realtime
/v1/client/realtime is a WebSocket. Browsers can't set headers on a WebSocket, so the
credentials travel as subprotocols, and the server selects notificationstream.v1:
Sec-WebSocket-Protocol: notificationstream.v1, pk.<publishable key>, bearer.<user token>- The server sends JSON text frames
{"type":"changed","at":<epoch ms>,"spread":<ms>}. Fetch the feed at a random point withinspreadms, withX-Changed-At: <at>. Nothing is replayed, so fetch once after every (re)connect. - Send
pingevery 60 seconds; the server answerspong. Any other frame closes the connection with1003. - The connection runs the same checks as the other client endpoints. Because browsers hide
the HTTP status of a failed upgrade, a refused connection opens and closes right away
with a code:
4401(key or token; get a fresh token and reconnect),4402(no active subscription),4403(origin not allowed, or the plan doesn't include realtime) or4429(monthly request limit reached; the reason israte_limited:<seconds>to wait). Other closes are normal: reconnect with backoff. - Opening a connection counts as one request; messages don't, the feed fetches they trigger do.
Errors
Every error response has the same shape:
{
"error": {
"code": "bad_request",
"message": "Invalid request",
"details": [{ "path": "contents.en.title", "message": "Too small: expected string to have >=1 characters", "code": "too_small" }]
}
}code is stable and machine-readable; message is for humans and may change. For
validation errors, details lists each issue with a dotted path.
Every response carries an X-Request-Id header (Cloudflare's ray id when available).
Include it when you contact support.
| Status | Code | Typical causes |
|---|---|---|
| 400 | bad_request | Validation failed (including data over 8 KB), body isn't valid JSON, unknown segmentId, expiresAt not after publishAt, invalid cursor, missing ids for a per-notification action |
| 401 | unauthorized | Missing, invalid or revoked secret key; unknown publishable key; missing, invalid or expired user token (details.reason); a project without signing keys |
| 402 | payment_required | Server write or client call without an active, trialing or past-due subscription |
| 403 | forbidden | Browser origin not in the project's allowed origins, or the plan doesn't include the feature (see plan features) |
| 404 | not_found | Resource doesn't exist in this project, or unknown route |
| 409 | conflict | Editing a notification that is published, expired or canceled |
| 429 | rate_limited | GET /v1/client/feed after the organization used up its monthly requests. Retry after Retry-After seconds. Server endpoints are not rate-limited |
| 500 | internal_error | Unexpected server error. Safe to retry idempotent requests |
Pagination
List endpoints return a page and a cursor:
{ "items": [ … ], "nextCursor": "eyJjIjoxNzkyNTQwODAwMDAwLCJpIjoibnRmXzRtUXA5c1gyIn0" }Pass nextCursor back as cursor to get the next page, until it is null. Cursors are
opaque: they encode the position of the last item, so creating or deleting items between
requests doesn't reset or skip pages. A malformed cursor returns 400 bad_request.
A feed page can hold fewer than limit items, even none, while nextCursor is set: a
status or category filter that skips most notifications stops looking after a while.
Keep paging until nextCursor is null.
| Endpoint | Order | limit |
|---|---|---|
GET /v1/notifications | newest created first | 1 to 100, default 50 |
GET /v1/project-users | newest created first | 1 to 100, default 50 |
GET /v1/client/feed, GET /v1/project-users/{userId}/feed | newest published first | 1 to 50, default 20 |
GET /v1/segments | by name, all at once | not paginated |
Idempotency
POST /v1/notifications accepts an Idempotency-Key header (or an idempotencyKey body
field, which takes precedence). If a notification with the same key already exists in the
project, the API returns that notification (201) instead of creating a second one. Use a
key derived from the event that triggered the notification, such as
invoice-1042-ready, so retries after timeouts never notify users twice.
- Keys are 1 to 255 characters and unique per project.
- A retry returns the original notification even if the body differs.
- The key is kept as long as the notification exists.
PUT, tag changes and cancel are safe to repeat. A repeated DELETE returns 404, and
repeating a PATCH that published a draft returns 409; in both cases the first call took
effect. POST /v1/segments has no idempotency key: repeating it creates a second segment.
Caching headers
| Response | Headers |
|---|---|
| Feeds | Weak ETag, Cache-Control: private, no-cache; If-None-Match gives 304 |
| Actions, profile | Cache-Control: no-store |
/v1/openapi.json | Cache-Control: public, max-age=300 |
Limits
Field and request limits are listed in Billing and limits.
The main ones: 1,000 user ids per notification, 50 locales, 8 KB of data, 100 attributes
and 100 tags per project user, 50 conditions per segment, 100 ids per client action.
Note
Broadcasts (all, tags and segment audiences) and changes to them can take up to
about 30 seconds to show up in feeds. A users audience shows up on the user's next
fetch. See
How quickly do notifications appear?