Realtime API
Receive live channel entry changes over a websocket: request a one-time grant, subscribe to RealtimeApiChannel, renew leases, handle added/changed/removed/resync events, and filter with live queries.
The Realtime API pushes channel entry changes to connected clients over a websocket. Events are hints: they tell you something changed, and the REST API stays the source of truth. A client subscribes, applies events as they arrive, and re-reads over REST whenever the server asks it to resync.
Realtime is opt-in per site. Nimbu support enables it for your site (the "Enable Realtime API"
site setting). Until then, grant requests return 403 Realtime API is disabled. The grant endpoint is in the API reference as
POST /realtime/grants.
There is no official JavaScript SDK client for realtime yet. Any ActionCable-compatible client works with the protocol below.
1. Request a grant
A grant is a short-lived, one-time credential for opening the websocket. It is the browser-safe way to connect: your page never needs to put a long-lived token in a websocket URL.
curl -X POST "https://api.nimbu.io/realtime/grants" \
-H "Authorization: Bearer $TOKEN" \
-H "X-Nimbu-Site: $SITE_ID"{
"grant": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expires_at": "2026-09-25T10:02:00Z"
}The endpoint returns 201 and accepts the same credentials as the rest of the API:
| Identity | How to request |
|---|---|
| OAuth bearer token | Authorization: Bearer …, plus X-Nimbu-Site for user-scoped tokens. |
| Customer session | X-Nimbu-Session-Token (or Session-Token) and the site. |
| Anonymous | No credentials; pass the site as X-Nimbu-Site or {"site_id": "…"} in the body. |
Grants expire after two minutes and are consumed on first use. Request a fresh grant for every (re)connect. Treat a grant like a token: keep it out of logs, analytics, and error reports.
| Status | Meaning |
|---|---|
401 | Credentials were sent but are invalid. |
403 | Realtime is disabled for the site, or the OAuth token cannot read channels. |
422 | No site could be resolved (site_id is required). |
429 | Too many anonymous grant requests from the same client (limit: 60 per minute). |
2. Connect
Open the websocket with the grant in the query string:
wss://<your-site-host>/ws?grant=<grant>The connection speaks the standard ActionCable protocol. A rejected or reused grant closes the connection; request a new grant and reconnect.
3. Subscribe
Subscribe to RealtimeApiChannel. The subscription parameters live in the ActionCable identifier:
{
"command": "subscribe",
"identifier": "{\"channel\":\"RealtimeApiChannel\",\"resource\":\"channel_entries\",\"parent_id\":\"news\"}"
}| Parameter | Notes |
|---|---|
resource | Required. Only channel_entries is supported. |
parent_id | Channel id or slug, same as {channel_id} in REST. Recommended: watch one channel. |
target_id | Optional entry id. Only events for that entry are delivered. |
query | Optional live query filter. Requires parent_id. |
firehose | true to watch every channel, including sensitive ones. Master site tokens with channel and sensitive-channel read scopes only. |
Without parent_id, anonymous and customer-session clients watch all non-sensitive channels of the
site; entry ACLs still apply to every event. A connection can hold up to 25 subscriptions.
Who receives what
Every event is checked against the subscriber's own identity before it is sent:
- Entry ACLs are applied per event, as in Channel access control.
- Unpublished entries are only delivered to master tokens.
- Sensitive channels are never delivered to anonymous or customer-session connections. OAuth
tokens need
read_sensitive_channels(orwrite_sensitive_channels), and a user without sensitive-channel permission on the site is refused even with the scope. Master site tokens also qualify. - OAuth tokens need
read_channels(orwrite_channels) to subscribe at all.
Control messages
The server answers subscribe, renew, and unsubscribe with control messages. They are protocol state, not optional metadata.
subscribed confirms the watch:
{
"type": "subscribed",
"subscription_id": "…",
"resource": "channel_entries",
"firehose": false,
"query": { "priority.gte": 3, "status": "open" },
"seq_at_create": 18234,
"resync": true,
"lease": { "expires_at": 1790330520.0, "renew_after": 1790330220.0 }
}resync: true means: do an authoritative REST read of your scope now. Events that happened while the
subscription was being set up are not replayed.
Leases. A subscription is a lease of up to 10 minutes. expires_at and renew_after are Unix
seconds. Renew at renew_after (the midpoint) by performing the renew action:
{
"command": "message",
"identifier": "{\"channel\":\"RealtimeApiChannel\",\"resource\":\"channel_entries\",\"parent_id\":\"news\"}",
"data": "{\"action\":\"renew\"}"
}renewed carries one entry in results. A successful renewal repeats the subscription and lease
fields with resync: true. { "status": "inactive", "resync": true } means the watch ended (for
example access was revoked, or a live query no longer compiles): resync over REST and subscribe
again.
unsubscribed answers the unsubscribe action with { "removed": true }, or
{ "status": "inactive" } when the watch was already gone.
subscription_error rejects a subscribe with a stable code:
| Code | Meaning |
|---|---|
not_available | Unknown resource, channel not found, or no access. Deliberately does not say which. |
invalid_query | The live query is malformed or uses an unsupported operator. |
limit_reached | This connection already holds 25 subscriptions. |
site_limit_reached | The site is at its realtime capacity. |
Events
{
"event_id": "5b0c8f7e-3d2a-4a51-9f59-0c6f2a0f8e11",
"event": "changed",
"resource": "channel_entries",
"parent_id": "66f0a1b2c3d4e5f607182900",
"id": "66f0a1b2c3d4e5f607182940",
"type": "channel_entries.updated",
"object": { "id": "66f0a1b2c3d4e5f607182940", "title": "Summer sale starts Monday", "status": "open" },
"changeset": { "status": "draft" },
"occurred_at": "2026-09-25T10:00:03.412Z",
"revision": 18240
}event | Meaning |
|---|---|
added | The entry became visible to this subscription: created, or updated into your filter/ACL. |
changed | The entry was visible before and after an update. |
removed | The entry is no longer visible: deleted, or updated out of your filter/ACL. No object. |
resync | Re-read your scope over REST. No object. Sent for example when a watched channel is deleted. |
type is channel_entries.created, .updated, .deleted, or .resync, describing the write that
happened. event describes what it means for your subscription: an update can arrive as
added or removed.
objectis the entry as the REST API would return it to an authorized reader, in the site's default locale. Relations are ids. ACL/owner metadata is left out.changeset(onchanged) lists the fields that changed with their old values.- Delivery is at-least-once. Deduplicate by
event_id. revisionis site-wide. Subscriptions legitimately skip revisions for events outside their watch, so a gap is not proof of a lost event. A cautious client may treat a gap as a reason to resync.
Resync over REST after a reconnect, an inactive renewal, a lease expiry, or any resync signal.
Live queries
A channel-scoped subscription can pass a query with the same filters as the REST entries list:
field=value, field.op=value, and a where expression. Only matching transitions are delivered
(added when an entry starts matching, removed when it stops).
{
"channel": "RealtimeApiChannel",
"resource": "channel_entries",
"parent_id": "tickets",
"query": { "status": "open", "priority.gte": 3 }
}query requires parent_id and is not allowed with firehose. The subscribed and renewed
controls echo the normalized query; resync over REST with the same filter.
Supported: equality, ne, gt, gte, lt, lte, in, nin, all, contains,
not_contains, exists, start, end, matches, select/multi-select by option name, relations
by id, dates, times, booleans, _status, slug, and where expressions with and, or, and
not.
Rejected with invalid_query, including inside a where expression:
search(full-text) andregex(usecontains,start, orend)- geo operators (
near,geoWithin,geoIntersects) inverse_of_*relations,_acl/_owner, and$-prefixed keys- sort, pagination, and projection keys (
sort,page,per_page,limit,fields, …) - more than 20 keys, or a query larger than 4 KB
- a negated operator (
ne,nin,not_contains,!=) on a dotted sub-field path such ascolor.title.ne; the positive forms work - more than one operator on the same dotted sub-field path (for example
items.score.gtwithitems.score.lt)
Localized fields and select option names match against the site default locale only. Encrypted fields never match. A query only narrows delivery; ACL, publication, and channel access are checked first.
Client checklist
POST /realtime/grants, then connect to/ws?grant=….- Subscribe; on
subscribed, load your scope over REST. - Apply events, deduplicating by
event_id. - Renew at
renew_after. - On disconnect,
inactive, orresync: get a new grant if needed, resubscribe, and re-read over REST.
Site Search
Search pages, products, channel entries, and collections in one call with GET /search, plus opt-in customer and order search. Covers parameters, scopes, ACL filtering, and the result shape.
Schema Management
Manage channel, blog, product, customer and checkout-profile fields declaratively: export the current schema, plan a change, apply it with a fingerprint, and record releases.