Dashboard
DocsPayment Request Live Updates | BotSubscription

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").

FieldTypeNotes
update_typestring"created" or "completed".
channelstringAlways "payment-requests".
project_idstringThe owning project.
payment_requestobjectFull payment request snapshot — see fields below.

payment_request mirrors the REST API representation. Key fields:

FieldTypeNotes
payment_request_idUUIDBotSubscription identifier.
provider_payment_idstring | nullUpstream provider identifier (e.g. Stripe Payment Intent).
statusstringOne of pending, authorized, completed, failed, cancelled, expired.
request_typestringbalance or invoice.
amountstringThe NET decimal in the request currency (e.g. "19.99").
gross_amountstring (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.
currencystringISO 4217 code or crypto symbol (e.g. "USD", "BTC").
subscription_idUUID | nullSet when the request is tied to a subscription.
payment_request_dataobjectProvider-specific details — crypto address, QR code, currency conversion, documents.
failure_reason / payment_error_codestring | nullPopulated on terminal failures.
settled_at / updated_at / created_atstring | 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"
  }
}
Note

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.

FieldTypeNotes
reasonstringCurrently "payment_request_resolved".
channelstringAlways "payment-requests".
payment_requestobjectSame 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"
  }
}
Tip

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.


  1. On ready, subscribe by payment_request_id (preferred) or provider_payment_id.
  2. Render the live UI from the payment_request snapshot in each payment-request.updated event — treat the payload as a full overwrite.
  3. On subscription.closed, stop listening (or send unsubscribe) and remove the entity from your in-flight tracking.
  4. If the socket reconnects before completion, resubscribe with the same identifier after the next ready event.

Next steps

Last updated: