Messages & Actions
This page covers the shared live-updates protocol once your client is connected: naming conventions, the envelope on named broadcast events, the actions you send, the system messages you receive, and the error codes the gateway emits.
If you are integrating a Telegram subscription bot or Discord subscription bot client, expect to spend most of your time on this page — everything except the handshake and per-channel payloads lives here.
Case conventions
The live-updates surface follows one consistent set of naming rules, aligned with the rest of the BotSubscription platform. The shape is simple: JSON object keys are snake_case. Wire identifiers — channels and broadcast event names — are kebab-case, mirroring REST URL path segments and outbound webhook event types so a client that already speaks one of those surfaces sees the same conventions here.
| Wire element | Convention | Example |
|---|---|---|
| Payload field names (every direction) | snake_case | connection_id, payment_request_id, event_id, update_type |
| Handshake auth field | snake_case | auth.project_id |
Channel values (in subscribe/unsubscribe + the channel field) | kebab-case | "payment-requests", "targets", "payment-methods" |
Broadcast event names (the string passed to socket.on(...)) | dot-separated kebab — identical to outbound webhook event types | payment-request.updated, target.added, payment-method.added |
System messages cover lifecycle and acknowledgements (ready, subscribed, error, pong, …) and arrive via socket.on("message", handler). Broadcast events carry channel payloads and arrive as named Socket.IO events under their dot-separated names.
Broadcast event envelope
Every named Socket.IO broadcast event documented in the channel pages carries two extra fields alongside its channel-specific body:
| Field | Type | Use |
|---|---|---|
event_id | UUID v4 (string) | Unique per server emission. If your client briefly disconnects mid-broadcast and the same emission is redelivered, it carries the same event_id — a usable client-side dedup key. Not stable across true reconnects: a fresh emission after reconnect gets a new event_id. |
emitted_at | integer (ms since epoch, UTC) | Server wall-clock at emit. Suitable for "approximately when" displays. Not monotonic across nodes — clocks may drift, so don't rely on it for causal ordering. |
System messages (the message event) do not carry these fields — they apply to broadcasts only.
For applying state correctly, use the natural keys in each payload (payment_request_id, target.target_id, payment_method_id) plus the payload's own updated_at / created_at for last-write-wins. Treat each broadcast payload as a full snapshot, not a diff.
Client actions
All client payloads are JSON objects with an action field. Send them with socket.send(payload) or the equivalent socket.emit("message", payload).
subscribe
Start receiving updates for a channel.
| Field | Required | Notes |
|---|---|---|
action | yes | "subscribe" |
channel | yes | One of payment-requests, targets, payment-methods. |
payment_request_id / provider_payment_id | conditional | Only for payment-requests. Exactly one must be present. |
Example — subscribe to a specific payment request:
{
"action": "subscribe",
"channel": "payment-requests",
"payment_request_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
}Example — subscribe to all target events for the project:
{ "action": "subscribe", "channel": "targets" }The server responds via the message event with either a subscribed confirmation or an error.
unsubscribe
Same shape as subscribe, with action: "unsubscribe". The response is unsubscribed on success.
ping
Optional application-level keep-alive. Use it when your client may stay otherwise quiet longer than the 9-minute idle timeout.
{ "action": "ping", "timestamp": 1700000000000 }timestamp is optional (a client-supplied epoch in ms). The server replies with pong and echoes the value as received_timestamp.
System messages
Delivered via socket.on("message", handler) in snake_case.
ready
Sent once, immediately after the handshake succeeds.
{
"event": "ready",
"connection_id": "uuid",
"channels": ["payment-requests", "targets", "payment-methods"]
}Use it as the signal to send your initial subscribe actions.
subscribed / unsubscribed
Acknowledges a subscribe / unsubscribe action.
{
"event": "subscribed",
"channel": "payment-requests",
"project_id": "uuid",
"payment_request_id": "uuid | null",
"provider_payment_id": "string | null"
}The identifier fields echo the ones you sent. They are null for fields that don't apply to the channel.
pong
Reply to your ping action.
{
"event": "pong",
"timestamp": 1700000100000,
"received_timestamp": 1700000000000
}timestamp is the server time at reply. received_timestamp echoes the value you sent (null if you didn't include one).
error
Sent when the gateway can't process a frame.
{
"event": "error",
"code": "invalid_payload",
"message": "Human-readable description",
"meta": {
"channel": "payment-requests",
"action": "subscribe"
}
}code— a machine-readable string from the error codes table below.message— a human-readable description suitable for logs.meta.channel/meta.action— populated whenever the error was raised in a channel-aware code path: any subscribe/unsubscribe error, plusunsupported_channelandunsupported_action. They let you route the error back to the specific in-flight request that triggered it when multiple actions are pending concurrently. Errors raised before channel resolution (invalid_payload) do not carry these fields.meta.errors— forinvalid_payload, an array of field-level validation issues.
Error codes
| Code | When it fires |
|---|---|
invalid_payload | The frame is not valid JSON, or it failed schema validation. meta.errors carries field-level details. |
unsupported_channel | The channel value is not one the gateway knows. |
unsupported_action | The action value is not one the gateway knows. |
forbidden | You are not authorized to subscribe to the requested resource (for example, a payment request belonging to another project). |
subscription_failed | The channel's subscribe handler returned an error. |
unsubscribe_failed | The channel's unsubscribe handler returned an error. |
When several subscribe / unsubscribe actions are in flight on the same socket, use meta.channel and meta.action to correlate each error with the action that produced it.
Reconnection
Subscriptions are session-local — the gateway does not persist them. When Socket.IO reconnects:
- Wait for the fresh
readymessage. - Resubscribe to every channel / identifier your UI still cares about.
event_idis not stable across the reconnect, so don't rely on it for cross-session dedup.- Apply incoming broadcasts as full-snapshot overwrites keyed by the entity ID. Use payload timestamps (
updated_at,created_at) to keep state monotonic if late frames arrive after a fresher one. - When a
subscription.closedevent arrives on thepayment-requestschannel, clean up the local subscription instead of resubscribing — the entity has reached a terminal state.
Next steps
- Track a single payment in Payment Request live updates.
- Mirror Discord and Telegram targets live in Target live updates.
- Reflect saved cards and wallets in Payment Method live updates.