Appearance
Dynamic Deposits
The SDK uses a Dynamic Payment Orchestrator: payment methods are fetched from the platform at runtime, so gateways can be added or removed per region without touching your frontend.
1. Fetching available methods
javascript
const methods = await sdk.wallet.getPaymentMethods();
methods.forEach(method => {
console.log(`${method.name} (${method.category})`);
console.log(`Min: ${method.minAmount}, Max: ${method.maxAmount}`);
});The PaymentMethod structure is defined in Wallet Overview.
2. Initiating a deposit
javascript
const intent = await sdk.wallet.initiateDeposit({
walletId: 'wallet_usd', // required; which wallet to credit
methodId: 'upi_qr', // required; from getPaymentMethods()
amount: 50.00, // required; must be > 0, within minAmount/maxAmount
currency: 'USD', // required; MUST match the wallet's currency
idempotencyKey: 'deposit-abc-1' // recommended; guards against double-submit
});DepositPayload
| Field | Type | Required | Notes |
|---|---|---|---|
walletId | string | yes | Target wallet; must be ACTIVE |
methodId | string | yes | From getPaymentMethods() |
amount | number | yes | > 0, finite; within the method's minAmount–maxAmount |
currency | string | yes | Must equal the wallet's currency (WALLET_CURRENCY_MISMATCH otherwise) |
idempotencyKey | string | recommended | Client-generated unique key (e.g., UUID per user action) |
Double-submit safety: two identical submissions with the SAME key return the SAME transaction; the same key with a DIFFERENT payload rejects with WALLET_DUPLICATE_REQUEST.
3. PaymentIntent
typescript
type PaymentIntentStatus =
| 'REQUIRES_REDIRECT' // 3D Secure / hosted page; take the user to redirectUrl
| 'REQUIRES_QR_SCAN' // display qrCode to the user
| 'REQUIRES_OTP' // collect a one-time code, then call verifyDeposit(txId, code)
| 'PENDING_VERIFICATION' // manual transfer; display walletAddress, await confirmation
| 'COMPLETED' // funds credited (terminal)
| 'FAILED'; // expired, cancelled, or declined (terminal)
interface PaymentIntent {
transactionId: string; // opaque ID; store it; used for verifyDeposit + events
walletId: string;
methodId: string;
amount: number;
currency: string;
status: PaymentIntentStatus;
redirectUrl?: string; // present when status = REQUIRES_REDIRECT
qrCode?: string; // present when status = REQUIRES_QR_SCAN
otpHint?: string; // present when status = REQUIRES_OTP (e.g., 'sent to +12•••123')
walletAddress?: string; // present when status = PENDING_VERIFICATION (manual crypto/bank)
expiresAt: string; // ISO-8601; intent expiry (QR codes, OTP, verification windows)
}4. Status machine, by method category
The method's category drives the flow; the same SDK call produces different next-steps. Deposits are blocked during an active self-exclusion; see the RG enforcement contract.
| Status | Entry trigger | Allowed transitions | Notes |
|---|---|---|---|
REQUIRES_REDIRECT | initiateDeposit on a CARD / HOSTED_PAGE / MOBILE_WALLET method (not OTP-gated) | → COMPLETED (verified on return) · → FAILED (declined/expired) | User returns to your return-URL → verifyDeposit(txId) |
REQUIRES_QR_SCAN | initiateDeposit on a UPI / QR_CODE method (not OTP-gated) | → PENDING_VERIFICATION (some crypto flows) · → COMPLETED (confirmed) · → FAILED (expired) | Display qrCode; watch for events |
REQUIRES_OTP | initiateDeposit on a method with requiresOtp: true (any category) | → COMPLETED (correct code) · → FAILED (expired) | Collect code → verifyDeposit(txId, code) |
PENDING_VERIFICATION | initiateDeposit on a CRYPTO / BANK_TRANSFER method (not OTP-gated) | → COMPLETED (funds confirmed) · → FAILED (not received before expiresAt) | Display walletAddress |
COMPLETED | Funds credited | terminal | none |
FAILED | Declined, cancelled, or expiresAt passed | terminal | Balance unchanged |
One rule: the category (plus requiresOtp) determines the initial status. Every intent starts directly in its flow state; there is no separate "created" state:
| Category | Initial status (unless requiresOtp: true, which yields REQUIRES_OTP) |
|---|---|
CARD | REQUIRES_REDIRECT (3D Secure) |
HOSTED_PAGE | REQUIRES_REDIRECT |
MOBILE_WALLET | REQUIRES_REDIRECT (provider app) |
UPI | REQUIRES_QR_SCAN |
QR_CODE | REQUIRES_QR_SCAN |
CRYPTO | PENDING_VERIFICATION (manual address flow) |
BANK_TRANSFER | PENDING_VERIFICATION |
OTP verification, wrong codes, and lockout
text
GIVEN an intent in REQUIRES_OTP
WHEN verifyDeposit(transactionId, code) is called with a WRONG code
THEN the call rejects with WALLET_INVALID_OTP, the intent REMAINS in REQUIRES_OTP,
the attempt counter increments, and a payment event does NOT fire.
GIVEN an intent in REQUIRES_OTP that has reached the OTP attempt limit (default 5 / 15 min)
WHEN verifyDeposit is called with any code
THEN the call rejects with the common TOO_MANY_ATTEMPTS error (TooManyAttemptsError,
carrying retryAfterMs), and further attempts are refused until the window resets.Each wrong code surfaces WALLET_INVALID_OTP; the attempt counter follows the platform-wide lockout policy; the TOO_MANY_ATTEMPTS error is shared with auth (see Rate Limiting & Lockout and the Error Reference).
text
GIVEN an intent in REQUIRES_REDIRECT / REQUIRES_QR_SCAN / REQUIRES_OTP / PENDING_VERIFICATION
WHEN the method's confirmation window (expiresAt) passes without success
THEN the intent moves to FAILED, the user's balance is unchanged,
and a payment event with status FAILED fires.javascript
switch (intent.status) {
case 'REQUIRES_REDIRECT':
window.location.href = intent.redirectUrl;
break;
case 'REQUIRES_QR_SCAN':
showQRCodeOnScreen(intent.qrCode); // then watch for events; see Payment Events
break;
case 'REQUIRES_OTP':
const code = await askUserForOtp(intent.otpHint);
const result = await sdk.wallet.verifyDeposit(intent.transactionId, code);
break;
case 'PENDING_VERIFICATION':
showWalletAddress(intent.walletAddress); // user sends funds; watch for events
break;
case 'COMPLETED':
break; // instant method; already credited
}5. Verifying redirect & OTP deposits
For REQUIRES_REDIRECT and REQUIRES_OTP flows, finalize explicitly:
javascript
// On your return-URL page (redirect flows)
const pendingTxId = localStorage.getItem('pending_deposit_tx');
if (pendingTxId) {
const finalIntent = await sdk.wallet.verifyDeposit(pendingTxId);
// COMPLETED = credited; FAILED = declined/expired;
// PENDING_VERIFICATION = async method still processing; keep waiting for the event
}verifyDeposit is idempotent: calling it again for an already-COMPLETED intent returns the same result without double-crediting. Unknown IDs reject with WALLET_PAYMENT_NOT_FOUND.