Payment Request Live Updates
Subscribe to the payment-requests channel to receive live status changes for a single payment request. The typical use case is a checkout page or invoice viewer that should update the instant a crypto payment settles or a card authorization clears — without polling the REST API.
Subscribing
Subscribe by either the BotSubscription payment_request_id or the upstream provider_payment_id. Exactly one must be present:
{
"action": "subscribe",
"channel": "payment-requests",
"payment_request_id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
}{
"action": "subscribe",
"channel": "payment-requests",
"provider_payment_id": "stripe_pi_abc123"
}The server replies with subscribed on success. unsubscribe uses the same payload with action: "unsubscribe".
Events
This channel emits two broadcast events. Both arrive as named Socket.IO events with snake_case field names, and both carry the standard broadcast envelope (event_id, emitted_at).
payment-request.updated
Fires when the payment request is first created (update_type: "created") or reaches status: "completed" (update_type: "completed").
| Field | Type | Notes |
|---|---|---|
update_type | string | "created" or "completed". |
channel | string | Always "payment-requests". |
project_id | string | The owning project. |
payment_request | object | Full payment request snapshot — see fields below. |
payment_request mirrors the REST API representation. Key fields:
| Field | Type | Notes |
|---|---|---|
payment_request_id | UUID | BotSubscription identifier. |
provider_payment_id | string | null | Upstream provider identifier (e.g. Stripe Payment Intent). |
status | string | One of pending, authorized, completed, failed, cancelled, expired. |
request_type | string | balance or invoice. |
amount | string | The NET decimal in the request currency (e.g. "19.99"). |
gross_amount | string (conditional) | The provider charge total (NET + VAT). Present only when a stored provider charge total exists (provider-charged central rows); it may equal amount when VAT is 0, and is omitted (never null) for internal-balance, local-project, and legacy rows. Coalesce gross_amount ?? amount for "what was charged"; do not infer VAT from its presence. |
currency | string | ISO 4217 code or crypto symbol (e.g. "USD", "BTC"). |
subscription_id | UUID | null | Set when the request is tied to a subscription. |
payment_request_data | object | Provider-specific details — crypto address, QR code, currency conversion, documents. |
failure_reason / payment_error_code | string | null | Populated on terminal failures. |
settled_at / updated_at / created_at | string | null (ISO 8601 UTC) | Timestamps. settled_at is null until settlement. Every timestamp in the broadcast — top-level and nested — is ISO 8601 UTC. |
Example payload:
{
"event_id": "9b724ac8-f0e1-4b56-8d7a-2c9c0d11b2f1",
"emitted_at": 1779836400000,
"update_type": "completed",
"channel": "payment-requests",
"project_id": "93425026-6bb8-4f81-a75d-63f538e1a123",
"payment_request": {
"payment_request_id": "7a356073-61e8-466d-8c17-f58c7042a975",
"provider_payment_id": "pi_3PqXyz0CZ0xYz",
"status": "completed",
"request_type": "invoice",
"amount": "19.99",
"gross_amount": "23.99",
"currency": "USD",
"subscription_id": "31f5d4b3-2a3c-4f8b-9e2d-7c5b6a1d8e3f",
"payment_request_data": {
"display_hint": "hybrid",
"save_payment_method": true,
"email": "[email protected]",
"documents": [
{
"document_id": "11111111-1111-1111-1111-111111111111",
"kind": "receipt",
"filename": "receipt-2026-05-25.pdf",
"file_size": 12345,
"sequence_number": 142,
"created_at": "2026-05-25T10:00:00.000Z"
}
]
},
"failure_reason": null,
"payment_error_code": null,
"settled_at": "2026-05-25T09:59:59.812Z",
"updated_at": "2026-05-25T09:59:59.900Z",
"created_at": "2026-05-25T09:55:00.000Z"
}
}When this event does not fire. payment-request.updated is emitted only for the created and completed transitions. Payments that end as failed, cancelled, or expired emit subscription.closed instead — read on.
subscription.closed
Fires when the payment request reaches any terminal status (completed, failed, cancelled, expired). Use it as the signal to clean up your local subscription.
| Field | Type | Notes |
|---|---|---|
reason | string | Currently "payment_request_resolved". |
channel | string | Always "payment-requests". |
payment_request | object | Same shape as in payment-request.updated — the final state snapshot. |
Example payload:
{
"event_id": "5e2a1b0d-7c61-4d83-9f10-aa00b2c3d4e5",
"emitted_at": 1779836400500,
"reason": "payment_request_resolved",
"channel": "payment-requests",
"payment_request": {
"payment_request_id": "7a356073-61e8-466d-8c17-f58c7042a975",
"status": "completed"
}
}A payment that completes triggers both payment-request.updated (update_type: "completed") and subscription.closed. If your UI reacts to payment-request.updated, the matching subscription.closed is your cue to stop listening and tear down the subscription cleanly.
Recommended client pattern
- On
ready, subscribe bypayment_request_id(preferred) orprovider_payment_id. - Render the live UI from the
payment_requestsnapshot in eachpayment-request.updatedevent — treat the payload as a full overwrite. - On
subscription.closed, stop listening (or sendunsubscribe) and remove the entity from your in-flight tracking. - If the socket reconnects before completion, resubscribe with the same identifier after the next
readyevent.
Next steps
- Mirror Discord and Telegram targets live in Target live updates.
- Reflect saved cards and wallets in Payment Method live updates.
- For durable backend processing of terminal events, register an outbound webhook.