Skip to Content
Realtime events

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_at gives 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_id claim, 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:

EnvironmentWebSocket address
testwss://api.predixum.net/connection/websocket
livewss://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.

ChannelWhat it carries
tenant:<tenant_id>:operatorThe 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>:tickerOne 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" }
FieldValue
typeposition.settled or position.voided.
position_idThe id of the position.
market_slugThe slug of the market.
outcome_idThe outcome that the position holds.
statesettled_win, settled_loss or voided.
payoutThe 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_corrected and market.void_corrected carry {type, sequence, epoch} only. Read the market again with GET /v1/markets/{slug} after one.
  • market.price carries {type, sequence, epoch, outcomes}, where outcomes is 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 the market.trade events 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.trade carries {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.

Last updated on