Skip to content

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

FieldTypeRequiredNotes
walletIdstringyesTarget wallet; must be ACTIVE
methodIdstringyesFrom getPaymentMethods()
amountnumberyes> 0, finite; within the method's minAmountmaxAmount
currencystringyesMust equal the wallet's currency (WALLET_CURRENCY_MISMATCH otherwise)
idempotencyKeystringrecommendedClient-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.

StatusEntry triggerAllowed transitionsNotes
REQUIRES_REDIRECTinitiateDeposit 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_SCANinitiateDeposit on a UPI / QR_CODE method (not OTP-gated)PENDING_VERIFICATION (some crypto flows) · → COMPLETED (confirmed) · → FAILED (expired)Display qrCode; watch for events
REQUIRES_OTPinitiateDeposit on a method with requiresOtp: true (any category)COMPLETED (correct code) · → FAILED (expired)Collect code → verifyDeposit(txId, code)
PENDING_VERIFICATIONinitiateDeposit on a CRYPTO / BANK_TRANSFER method (not OTP-gated)COMPLETED (funds confirmed) · → FAILED (not received before expiresAt)Display walletAddress
COMPLETEDFunds creditedterminalnone
FAILEDDeclined, cancelled, or expiresAt passedterminalBalance 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:

CategoryInitial status (unless requiresOtp: true, which yields REQUIRES_OTP)
CARDREQUIRES_REDIRECT (3D Secure)
HOSTED_PAGEREQUIRES_REDIRECT
MOBILE_WALLETREQUIRES_REDIRECT (provider app)
UPIREQUIRES_QR_SCAN
QR_CODEREQUIRES_QR_SCAN
CRYPTOPENDING_VERIFICATION (manual address flow)
BANK_TRANSFERPENDING_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.