Skip to main content
The stream is the recommended event channel: your backend holds one outbound WebSocket to Gleap’s realtime cluster and receives every event for your project on it — no inbound endpoint to expose, no per-delivery signature verification, no retry windows, and lower latency than webhooks (which queue and retry per delivery). The stream speaks the Pusher wire protocol, so any mature off-the-shelf Pusher client (Node, Java, Go, .NET) works unmodified. Webhooks remain fully supported when your infrastructure policy prefers inbound HTTP.

Connect

The API host depends on the data region of your project:
The examples on this page use the EU host. If your project lives in another region, replace https://api.gleap.io with your region’s host. API tokens are per project and only valid in the project’s region.
Read the connection parameters at startup — never hardcode them:
Point your client’s channel authorization at authEndpoint (with your token as the Authorization header) and subscribe to channel. Include two custom auth params:
  • consumerId names the connecting instance ([A-Za-z0-9_-]{1,64}).
  • typingEvents: true opts this stream into agent.typing.* (high volume; default off).
Also configure the client’s user authentication against userAuthEndpoint (Pusher “signin”) — that is what allows the server to cleanly terminate a superseded connection during takeover. Event data arrives as a JSON string of the same envelope webhooks receive — JSON.parse it (Pusher clients usually do this for you). Payloads larger than the socket limit arrive as a slim pointer (data.truncated: true with the conversation/message id) — fetch the full object over REST.

Exactly one consumer

Gleap allows one active consumerId per project. When a connection authorizes with a different consumerId, it takes over: the previous consumer’s connections are closed and a s2s.takeover control event is published on the channel. Connections that share the same consumerId are the same consumer: none of them is taken over, and each receives every event. Use this to run the stream on several instances at once (active-active), so losing one instance loses no events. Deduplicate by the envelope id (for example SET NX in Redis with a short TTL) or make processing idempotent. typingEvents is a project-wide setting and the most recent auth wins, so use the same value on every instance. Rolling deployments need no coordination either way: with the same consumerId old and new pods overlap and both receive events; with a new consumerId the new pod takes over and the old one is dropped.

Detecting a takeover

s2s.takeover goes out at the same moment as the disconnect, so it is best effort: it may arrive just before the close or not at all. It is a control event, not wrapped in the envelope:
Every subscriber sees it, so compare consumerId with your own and ignore it when it matches. A taken-over connection is closed with code 4009 and reason You got disconnected by the app., which only reaches connections that completed user authentication. You were taken over when you receive s2s.takeover with a foreign consumerId, or a close 4009 with that reason. Do not reconnect automatically in that case: two instances with different ids would keep evicting each other. Reconnect on every other close.

Disconnects

Every close initiated by the realtime server first sends a pusher:error message ({ "code", "message" }) and then a close frame with the same code. Log both. There is no maximum connection lifetime. A failed channel authorization (for example a revoked token) does not close the socket; it arrives as pusher:subscription_error. In general the Pusher ranges apply: 4000–4099 fix the cause before reconnecting, 4100–4199 reconnect with backoff, 4200–4299 reconnect immediately.

Resync on every (re)connect — mandatory

A dropped connection loses the events published during the gap; there is no replay. After every connect — first connect, reconnect, and after a takeover — run per-conversation catch-up for anything you track:
  1. GET /v3/s2s/contacts/{userId}/conversations for contacts with open activity, and/or
  2. GET /v3/s2s/conversations/{id}/messages?after=<last message id you have> per conversation.
This is the same self-healing model the Gleap widget uses, and it also covers webhook consumers after their retry window.