Realtime events
Your backend can hold one realtime connection to hear about settlements as they happen. The connection is optional. Webhooks stay the guaranteed notice: a realtime event can be lost, and a webhook delivery is retried until you accept it. Use the connection for speed and the Webhook guide for the record.
The realtime server is Centrifugo . This page gives the token, the address, the channels and the event bodies.
Get a connection token
Your backend exchanges its tenant API key for a connection token with POST /v1/realtime/tokens. The key needs the write:positions scope. The request has no body, because the tenant comes from the key.
POST /v1/realtime/tokens
Authorization: Bearer {{api_key}}HTTP 201
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.example.signature",
"expires_at": "2026-09-27T11:00:00Z"
}- The token is valid for one hour.
expires_atgives the exact time. - There is no refresh call. Get a new token before
expires_at, and use it for the next connect. - Call this endpoint from your server only. A browser cannot call it, because CORS is not allowed. The widget has its own token and never uses this one.
- The token pins your tenant. Its JWT payload carries your tenant id in the
tenant_idclaim, which you need for the channel names below. - When we revoke your API key, you cannot get a new token, and the tokens you already hold stop working within the hour.
Connect
Open a WebSocket to this address. Test mode and live mode use the same one, and the token decides the mode:
| Environment | WebSocket address |
|---|---|
test | wss://api.predixum.net/connection/websocket |
live | wss://api.predixum.net/connection/websocket |
Use the Centrifugo JSON protocol. A client that uses the binary (Protobuf) protocol is refused. Send the token in the data field of the connect command, as {"token": "<token>"}. Do not put it in the token field of the command: we read the token from data only. If you use a Centrifugo client library, set the token through the option that fills the connect data.
{"id": 1, "connect": {"data": {"token": "<token>"}}}
{"id": 2, "subscribe": {"channel": "tenant:<tenant_id>:operator"}}A refused token gets the error code 101 (unauthorized). Get a new token and connect again. A refused subscribe gets the error code 103 (permission denied).
Each event arrives as a push on the channel. The event body is in pub.data:
{"push": {"channel": "tenant:<tenant_id>:operator", "pub": {"data": {"type": "position.settled", "...": "..."}}}}If the connection drops, reconnect with a growing delay and some random jitter. A reconnect after the token expired needs a new token.
Channels
The channels are subscribe-only. Your connection cannot publish. A token from one tenant cannot subscribe to the channels of another tenant.
| Channel | What it carries |
|---|---|
tenant:<tenant_id>:operator | The settlement events of your users’ positions. Only a connection with a token from POST /v1/realtime/tokens can subscribe. |
tenant:<tenant_id>:market:<slug> | The status and price events of one market. Write the slug in lowercase. |
tenant:<tenant_id>:ticker | One price summary for list pages, so that a list needs one subscription and not one per market. |
Operator channel events
The operator channel carries two event types: position.settled and position.voided. Each arrives once per position at most.
{
"type": "position.settled",
"position_id": "0192a0b1-7c2e-7d3f-8a4b-5c6d7e8f9a0c",
"market_slug": "btc-100k-2026",
"outcome_id": "0192a0b1-7c2e-7d3f-8a4b-5c6d7e8f9a0d",
"state": "settled_win",
"payout": "38.461538"
}| Field | Value |
|---|---|
type | position.settled or position.voided. |
position_id | The id of the position. |
market_slug | The slug of the market. |
outcome_id | The outcome that the position holds. |
state | settled_win, settled_loss or voided. |
payout | The amount paid into the virtual balance, as a decimal string. It is 0 for settled_loss, and the stake for voided. |
Nothing else reaches this channel. Settlement and void corrections, position.opened, position.closed, order.filled and report.ready come by webhook only.
An event that we publish while your connection is down is lost, and we do not replay it. We can also pause the realtime pipeline during an incident, and then no event arrives. The webhook delivery covers both cases. When you store a state or a payout, take it from the webhook or from GET /v1/positions/{id}.
Market and ticker events
These channels carry the same events that the widget reads. Use them if you draw your own market pages.
market.opened,market.suspended,market.resolved,market.voided,market.resolution_correctedandmarket.void_correctedcarry{type, sequence, epoch}only. Read the market again withGET /v1/markets/{slug}after one.market.pricecarries{type, sequence, epoch, outcomes}, whereoutcomesis a list of{outcome_id, price}.- A price push on the ticker is a JSON list. Each item is
{slug, sequence, epoch, outcomes}for one market whose price changed. The ticker also carries themarket.tradeevents of the markets of your tenant, as single objects, so check whether a push is a list or an object before you read it. market.tradecarries{type, market_slug, outcome_id, amount_bucket, side}for one anonymous trade. It has no sequence and no epoch.
sequence and epoch follow the rules of the Market Feed. If you see a gap in sequence or a new epoch, read GET /v1/markets/{slug}/snapshot and continue from there. The Market Feed guide explains both values and the polling fallback.