Real-time updates
How API clients can listen for data changes in realtime.
Protip
Searching for ways to update your sites or apps in real-time as your data changes? The Live Content API is a more flexible, efficient choice.
If you're looking to react to content as it changes, check out Sanity Functions or GROQ-powered webhooks.
The Sanity data store supports realtime updates, allowing API clients to listen for content changes. This is used for collaborative editing in our content studio, where your view of the document is updated as other people make changes. These updates are available to your own apps as well, and have a wide range of uses, such as:
- Alerting end-users of breaking news stories.
- Updating client state in a multiplayer game.
- Transmitting chat messages between users.
- Controlling IoT (Internet-of-Things) devices such as home automation systems.
Listeners use the Server-Sent Events protocol, by making an HTTPS request to:
https://<project-id>.api.sanity.io/v2026-05-21/data/listen/<dataset>?query=<GROQ-query>
The server will keep the connection open and stream events as they occur for any documents matching the provided GROQ query. Further parameters and details are listed in the listeners reference.
Gotcha
Listener queries do not support joins, since they operate on individual documents, and will ignore order-clauses and projections.
We recommend using one of our client libraries to listen for updates, which will automatically decode events into native data structures and handle stuff like automatic reconnects. Here's an example using our JavaScript library:
const query = '*[_type == "comment" && authorId != $ownerId]'
const params = {ownerId: 'myUserId'}
const subscription = client.listen(query, params)
.subscribe(update => {
const comment = update.result
console.log(`${comment.author} commented: ${comment.text}`)
})Events
Protip
Client libraries may hide or automatically handle certain events, refer to its documentation for details.
welcome
When the listener is set up and ready to serve mutations, you will receive the welcome event. It looks like this:
event: welcome
data: {"listenerName": "Ua6BR3GwQ14cnZXrgwCdsF"}You don't need to process this event, but it could be used to kick off other processing. If you are tracking changes to keep a document in sync on the client side, this is a good time to fetch the initial document using the doc endpoint. Fetching the document after the listener is ready ensures that you receive every subsequent mutation. If you fetch the initial document before setting up the listener, you may miss one or more mutations in the intervening time.
mutation
The most common event is the mutation event, which looks like this:
event: mutation id: lqgiok-skp-eja-k6z-9wrng7k5e#38123cba-286c-45a0-a6d1-3cc4dc43748a data: <JSON-payload on a single line>
The payload is a single line of JSON. The listener reference has a complete list of fields and descriptions, but some of the most useful fields are summarized below:
documentId: the ID of the modified documenttransition: type of event -update,appear, ordisappearidentity: the user making the changesmutations: an array of mutations as submitted to the mutate endpointresult: the complete document after the mutations are appliedpreviousRev: the document revision ID before the mutationresultRev: the document revision ID after the mutationtimestamp: time when the mutation was appliedvisibility: whether the change is visible to queries yet (query), or only to subsequent transactions (transaction).
Gotcha
Due to the distributed nature of the Sanity backend, mutation events may be sent out of order. A meticulous client would reassemble mutation events as an unbroken chain by comparing previousRev and resultRev, or use the most recent result document as determined by timestamp.
channelError
Errors during processing will appear as channelError events. These are typically caused by syntax errors in the query, and look like this:
event: channelError
message: {"message": <the error message>}disconnect
Normally, if you are disconnected from a listener endpoint you should just immediately reconnect. However, if you receive the disconnect event, you should disconnect and stay away. Typically this means you just got a channelError that is considered fatal (e.g. a syntax error) and reconnecting will just repeat the ordeal. The event looks like this:
event: disconnect
data: {"reason": <a string describing the reason>}Listeners in Sanity Studio
A single Studio session opens many listeners at once, not one. The preview system, Tasks, Releases, and each open document subscribe separately, so one browser tab produces a steady stream of requests to the listen endpoint.
These requests carry first-party request tags such as sanity.studio.preview.observe-document-set.listen, which is often the highest-volume tag in a project's request logs. See request tags.
Reconnect behavior
@sanity/client reconnects on its own after a transient connection failure, waiting one second before it retries. Dropped connections, 5xx responses, 408, and 429 all count as transient. A rejection it can identify as permanent, such as an expired token, is surfaced to your code as an error instead, so the client doesn't retry it in a loop.
Diagnose a spike of 401 responses
A Studio left open with stale credentials shows up in request logs as a large number of 401 responses on the listen endpoint, concentrated on a few IP addresses and often continuing outside working hours. The shape looks like an attack, but it is usually one client that can no longer authenticate, repeated across the many listeners that session holds open.
Before you treat that traffic as hostile, check for:
- Orphaned browser tabs left open on a Studio.
- Development instances still running against the project.
- Studios hosted in a desktop shell such as Electron.
Signing fully out and back in clears the stale credentials. Remember that a request tag is set by the client, so a sanity.studio tag on those requests is not evidence that the caller signed in. For what the failed requests mean for your bill, see plans and payments.