Skip to content

Payment Events

For asynchronous payment flows (QR codes, manual crypto transfers, OTP, back-office-processed withdrawals), the user doesn't leave your website. To update your UI the moment a transaction's state changes, subscribe to payment events.

The contract

javascript
const unsubscribe = sdk.wallet.onPaymentUpdate((event) => {
  console.log(`Transaction ${event.transactionId}: ${event.status}`);
  if (event.status === 'COMPLETED') {
    // stop spinner, show success, refresh balances
  } else if (event.status === 'FAILED') {
    // show failure; balance unchanged
  }
});

When it fires: every real state change of a payment intent or withdrawal; QR confirmations, redirect/3DS outcomes, back-office approval or rejection, expiries. This includes outcomes of your own verifyDeposit calls, so one listener covers every path.

When it fires: the normative rule:

text
GIVEN a payment intent or withdrawal request exists
WHEN its status CHANGES on the platform (QR confirmation, redirect/3DS outcome,
     OTP outcome, manual-verification confirmation, back-office approval or
     rejection, expiry, cancellation)
THEN one PaymentEvent fires carrying the new status, and the channel delivers
     it to every open subscription.

GIVEN a call to initiateDeposit or requestWithdrawal
WHEN the intent is CREATED
THEN NO event fires; the created intent is the call's own return value;
     events exist for changes AFTER creation, never for the creation itself.

When it never fires: transport failures. Events reflect platform state changes only; timeouts and network errors surface as typed errors on the call that failed; never as events.

Event channel

Events ride the platform's shared real-time channel; its connection lifecycle, ticket authentication, at-least-once delivery with the 24-hour replay window, and eventId dedupe rules are defined once in the Real-Time Channel reference. Wallet-specific channel behavior:

  • Sandbox: in-process emitter; identical listener API, same envelopes.
  • Reconciliation: beyond the 24-hour replay window, reconcile a non-terminal intent (REQUIRES_QR_SCAN / REQUIRES_OTP / PENDING_VERIFICATION / a PENDING withdrawal) by fetching current state; verifyDeposit(txId) for the intent's status, getTransactions for withdrawal status.

Deduplicate by (transactionId, status); treat a repeated identical event as one (delivery is at-least-once).

PaymentEvent

typescript
interface PaymentEvent {
  eventId: string;              // dedupe key; also on the envelope
  transactionId: string;        // matches PaymentIntent.transactionId / Transaction.id
  walletId: string;
  type: 'DEPOSIT' | 'WITHDRAWAL';// which flow changed
  status: PaymentIntentStatus | 'PENDING' | 'REJECTED'; // deposits: full intent status union (see Dynamic Deposits); withdrawals: PENDING / COMPLETED / REJECTED (see Withdrawals)
  amount: number;
  currency: string;
}

Correct usage

javascript
// 1. ALWAYS subscribe BEFORE initiating; otherwise you can miss the event
const unsubscribe = sdk.wallet.onPaymentUpdate(onEvent);

// 2. Initiate the deposit (walletId + matching currency required)
const intent = await sdk.wallet.initiateDeposit({
  walletId: 'wallet_usd',
  methodId: 'upi_qr',
  amount: 20.00,
  currency: 'USD'
});

if (intent.status === 'REQUIRES_QR_SCAN') {
  showQRCodeOnScreen(intent.qrCode);
}

// 3. Cleanup; call unsubscribe on unmount (React useEffect cleanup, Vue onUnmounted)

Best practices

  1. Subscribe first, initiate second; never miss the event.
  2. Deduplicate (transactionId, status) pairs; delivery is at-least-once.
  3. Unsubscribe on unmount to prevent duplicate listeners and memory leaks.
  4. On COMPLETED, refresh balances with getWallets() for absolute accuracy.