Appearance
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,getTransactionsfor 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
- Subscribe first, initiate second; never miss the event.
- Deduplicate
(transactionId, status)pairs; delivery is at-least-once. - Unsubscribe on unmount to prevent duplicate listeners and memory leaks.
- On
COMPLETED, refresh balances withgetWallets()for absolute accuracy.