WebSocket events

Guides

WebSocket events

The BroSettlement WebSocket stream notifies an organization about durable account, wallet, deposit, and transaction changes so clients can refresh affected REST resources without relying on continuous polling.

Connect

Staging endpoint:

text
wss://brosettlement-staging-api.brolabel.io/v1/ws

The API key requires the websockets:read scope. Sign the handshake with the API key's Ed25519 private key:

text
WS_CONNECT
/v1/ws
TIMESTAMP
NONCE

Use the same API key ID, Unix timestamp in seconds, nonce, and padded standard Base64 signature required by the current integration environment. The REST six-line canonical value is not valid for a WebSocket connection.

Delivery model

  • Events are scoped to the API key's organization.
  • A durable business event has a globally unique ID that remains stable when redelivered.
  • Delivery is at least once: clients must expect duplicates.
  • occurredAt is the durable state-transition time, not the delivery time.
  • correlationId connects REST requests, transactions, chain activity, and support logs.
  • Event schemas begin at version 1 and evolve additively.
  • Unknown fields must be ignored.
  • Private keys, raw signatures, seed material, user secrets, and internal MPC payloads must never appear in an event.

Event envelopes

The MVP-compatible envelope uses type, event, and eventId:

json
{
  "type": "EVENT",
  "event": "transaction.updated",
  "eventId": "evt_...",
  "version": 1,
  "orgId": "org-id",
  "occurredAt": "2026-07-07T13:38:33.000Z",
  "correlationId": "request-or-flow-id",
  "data": {}
}

The durable topic-stream envelope uses op, subject, id, and sequence:

json
{
  "op": "event",
  "subId": "s1",
  "id": "evt_...",
  "sequence": 987654,
  "subject": "transaction.updated",
  "occurredAt": "2026-07-07T13:38:33.000Z",
  "orgId": "org-id",
  "correlationId": "request-or-flow-id",
  "version": 1,
  "data": {}
}

Support both envelopes during migration when your environment can emit either form. Normalize eventId/id and event/subject before processing.

Required event families

EventMeaning
system.connectedAuthentication succeeded and the stream is ready
system.errorAuthentication, policy, topic, or payload error
account.createdA ledger account was durably created
wallet.createdA wallet and address were created and are readable
wallet.creation_failedAccepted wallet provisioning failed asynchronously
wallet.status_changedA wallet moved between lifecycle states
transaction.createdA durable transaction record or intent was created
transaction.updatedClient-visible transaction state or on-chain detail changed
transaction.confirmedThe transaction reached successful finality
transaction.failedProcessing failed before successful on-chain completion
transaction.failed_on_chainBlockchain execution failed or reverted
deposit.observedAn incoming transfer was detected
deposit.confirmedThe deposit met the confirmation policy
deposit.creditedThe confirmed deposit was credited to the operating ledger

Read-only REST calls do not emit a business event merely because they were called.

Account events

account.created follows a successful POST /api/v1/ledger/accounts.

json
{
  "accountId": "account-id",
  "orgId": "org-id",
  "name": "Customer 123",
  "externalId": "customer-123",
  "createdAt": "2026-07-07T13:00:00.000Z",
  "requestId": "http-request-id",
  "idempotencyKey": null
}

A synchronous validation or authentication rejection that creates no account does not produce account.created.

Wallet events

wallet.created identifies the durable wallet and address:

json
{
  "walletId": "wallet-id",
  "accountId": "account-id",
  "orgId": "org-id",
  "chain": "tron:nile",
  "address": "T...",
  "status": "ACTIVE",
  "createdAt": "2026-07-07T13:00:00.000Z",
  "requestId": "http-request-id",
  "idempotencyKey": "client-operation-id"
}

If an asynchronously accepted wallet fails later, emit wallet.creation_failed with accountId, chain, status, and a machine-readable errorCode. Use wallet.status_changed for transitions such as PENDING to ACTIVE, DISABLED, ARCHIVED, or FAILED.

A synchronous 4xx rejection that creates no durable wallet does not require a business event. The REST error is the source of truth.

Transaction events

Use one transaction.* family for withdrawals and internal transfers. Do not split the lifecycle into withdrawal.*, transfer.*, or internal_transfer.* families.

The payload distinguishes the movement:

  • isInternalTransfer: false and transferKind: "EXTERNAL_WITHDRAWAL" for an external withdrawal;
  • isInternalTransfer: true and transferKind: "INTERNAL_TRANSFER" for a same-organization movement.

Minimum lifecycle payload:

json
{
  "transactionId": "transaction-id",
  "transactionGroupId": null,
  "sourceTransactionId": null,
  "destinationTransactionId": null,
  "orgId": "org-id",
  "accountId": "account-id",
  "walletId": "wallet-id",
  "destinationAccountId": null,
  "destinationWalletId": null,
  "chain": "tron:nile",
  "asset": "TRX",
  "amountAtomic": "7000000",
  "amountDirection": "DEBIT",
  "isInternalTransfer": false,
  "transferKind": "EXTERNAL_WITHDRAWAL",
  "fromAddress": "T...",
  "toAddress": "T...",
  "txHash": null,
  "status": "PENDING",
  "sourceStatus": "PENDING",
  "destinationStatus": null,
  "failureCode": null,
  "failureMessage": null
}

transaction.updated covers reservations, MPC signing progress, broadcast, confirmation progress, on-chain details, and externally relevant failure information. Terminal events carry the final REST-visible status: CONFIRMED, FAILED, or FAILED_ON_CHAIN.

Keep amountAtomic as a base-10 string. Do not parse it through floating-point types.

Deposits

Deposits originate from blockchain observation rather than a client REST POST:

  1. deposit.observed — the incoming transfer was detected.
  2. transaction.created — the client-visible deposit transaction exists.
  3. deposit.confirmed — required confirmations were reached.
  4. transaction.confirmed — the transaction reached terminal success.
  5. deposit.credited — the operating ledger was credited.

An implementation may combine compatible lifecycle signals, but a confirmed deposit must produce at least one deposit or transaction event. Clients must not be forced to discover a durable confirmed deposit only through polling.

Deposit data must identify transactionId when available, walletId, accountId, chain, asset, amountAtomic, txHash, block information, and confirmation progress.

Synchronous rejection and durable failure

These cases have different event behavior:

Result of POST /api/v1/transactionsWebSocket expectation
Synchronous 4xx, no transaction ID, no durable record or reservationNo transaction.* business event; handle the REST error
Transaction ID or durable intent exists, then processing failsEmit transaction.created, then terminal transaction.failed
Blockchain execution fails or revertsEmit transaction.failed_on_chain

Once a transaction is visible through list/detail REST endpoints, every later terminal failure must also be visible through WebSocket. A failed reservation must be voided exactly once.

For an internal transfer with paired debit and credit rows, include sourceTransactionId, destinationTransactionId, and leg statuses. A failed source leg must not leave the destination leg permanently PENDING.

REST reconciliation

Treat an event as a signal to refresh authoritative state:

Event familyREST resources to refresh
account.*/api/v1/ledger/accounts/{accountId}
wallet.*/api/v1/wallets/{walletId} and account wallets
transaction.*/api/v1/transactions/{id}, related balances, and journal entries
deposit.*Transaction detail, wallet/account balances, and ledger entries

The transactions list and transaction detail must converge to the same status. Do not display a final customer balance solely from an unverified event payload.

Listener requirements

  1. Log received frames without logging API private keys or signing secrets.
  2. Parse JSON while preserving the original payload for investigation.
  3. Treat system.connected as transport state, not a business event.
  4. Deduplicate business events by stable eventId or id.
  5. Persist sequence or the last event ID when resume is available.
  6. Acknowledge durable events when the server requires explicit ack.
  7. Reconnect with bounded exponential backoff and jitter.
  8. Resume from the last committed position when supported.
  9. Refresh affected REST resources after transaction.*, wallet.*, and deposit events.
  10. Commit the resume position only after local processing succeeds.

Current staging compatibility

The event requirements document records these staging compatibility gaps:

  • some deployments may still emit legacy SYSTEM_CONNECTED instead of system.connected;
  • the current /v1/ws transport may not yet expose topics, ack, or resume;
  • event coverage for deposits and paired internal-transfer legs must be verified in the target environment.

Normalize the legacy connection event during migration, but do not invent missing business events client-side. Use REST reconciliation to recover state and confirm the enabled WebSocket contract before production launch.

Security

  • Reject or discard an event whose orgId does not match the authenticated organization.
  • Never place credentials or MPC signing material in logs.
  • Treat event data as untrusted input and validate required fields.
  • Preserve correlationId and REST requestId for support.
  • Use a dedicated least-privilege API key with websockets:read.