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/v1

All paths start with /v1. Requests and responses are JSON (UTF-8). All timestamps are epoch milliseconds (UTC).

Two kinds of endpoints

Server endpointsClient endpoints
Paths/v1/notifications, /v1/project-users, /v1/segments, /v1/project/v1/client/feed, /v1/client/actions, /v1/client/profile, /v1/client/realtime
Called fromYour backendBrowsers and mobile apps (the SDKs)
AuthAuthorization: Bearer sk_…X-Publishable-Key: pk_… + Authorization: Bearer <user token>
CORSNone (not callable from browsers)Access-Control-Allow-Origin: * (or your allowed origins), preflights cached
BillingWrites need an active subscription (402); reads keep workingNeed an active subscription (402)
Monthly requestsNot countedCounted; 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: sub is your user id, exp at most 7 days ahead, and kid optionally 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.reason says 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.

MethodPathPurpose
POST/v1/notificationsSend a notification
GET/v1/notificationsList 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}/cancelCancel a notification

Project users

End users of your app, keyed by your own user id. Profiles, attributes and tags power targeting and templates.

MethodPathPurpose
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-usersList project users
POST/v1/project-users/{userId}/tagsAdd or remove tags
GET/v1/project-users/{userId}/feedGet a project user's feed

Segments

Saved rule-based audiences over project user fields, attributes and tags.

MethodPathPurpose
GET/v1/segmentsList segments
POST/v1/segmentsCreate a segment
POST/v1/segments/previewPreview segment conditions
GET/v1/segments/{id}Get a segment
PATCH/v1/segments/{id}Update a segment
DELETE/v1/segments/{id}Delete a segment

Project

MethodPathPurpose
GET/v1/projectGet 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.

MethodPathPurpose
GET/v1/client/feedGet the user's feed, counts and the project's client config
POST/v1/client/actionsMark notifications read, unread, archived or clicked
POST/v1/client/profileSync the user's profile fields and attributes (not tags)
GET/v1/client/realtimeWebSocket 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 within spread ms, with X-Changed-At: <at>. Nothing is replayed, so fetch once after every (re)connect.
  • Send ping every 60 seconds; the server answers pong. Any other frame closes the connection with 1003.
  • 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) or 4429 (monthly request limit reached; the reason is rate_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.

StatusCodeTypical causes
400bad_requestValidation 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
401unauthorizedMissing, invalid or revoked secret key; unknown publishable key; missing, invalid or expired user token (details.reason); a project without signing keys
402payment_requiredServer write or client call without an active, trialing or past-due subscription
403forbiddenBrowser origin not in the project's allowed origins, or the plan doesn't include the feature (see plan features)
404not_foundResource doesn't exist in this project, or unknown route
409conflictEditing a notification that is published, expired or canceled
429rate_limitedGET /v1/client/feed after the organization used up its monthly requests. Retry after Retry-After seconds. Server endpoints are not rate-limited
500internal_errorUnexpected 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.

EndpointOrderlimit
GET /v1/notificationsnewest created first1 to 100, default 50
GET /v1/project-usersnewest created first1 to 100, default 50
GET /v1/client/feed, GET /v1/project-users/{userId}/feednewest published first1 to 50, default 20
GET /v1/segmentsby name, all at oncenot 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

ResponseHeaders
FeedsWeak ETag, Cache-Control: private, no-cache; If-None-Match gives 304
Actions, profileCache-Control: no-store
/v1/openapi.jsonCache-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?