KuvarPay Web SDK Integration Guide#
This guide shows how to embed KuvarPay's checkout modal on your website using the Web SDK, and how to drive collections, payouts, subscriptions and split payments from your backend with the Server SDK. Customers can pay in crypto or their local currency (bank transfer / mobile money), and the Server SDK additionally supports fiat payouts (sending to a recipient's bank / mobile-money account).Overview#
One line to include the SDK script
Initialize once with your publishable API key and business ID
Call KuvarPay.openPayment(...) for one-time payments
Call KuvarPay.openSubscription(...) for subscription approvals (supports FIXED and METERED billing)
Handle onSuccess, onCancel, and onError callbacks
Built-in idempotency for safe retries
Prerequisites#
Before integrating the SDK, you need:2.
API Keys — Go to your Merchant Dashboard → Settings → API Keys to generate:A Publishable (Client) Key — used in the browser SDK (starts with rsp_test_ or rsp_live_).
A Secret Key — used only on your server (starts with rsp_secret_ or rsp_secret_). Never expose this in client-side code.
3.
Business ID — Found in your Merchant Dashboard → Settings → Business Info.
Supported Currencies#
KuvarPay supports the following settlement (fiat) currencies:Customers can pay using popular cryptocurrencies including USDT, USDC, BNB, ETH, AVAX, and more across multiple networks (BSC, Ethereum, Avalanche, Polygon, etc.). Use the Currencies API to fetch the full list of supported crypto assets and networks dynamically.1) Include the SDK Script#
Or include with auto-initialization using data attributes (recommended when possible):Note: data-base-url is now optional. If omitted, the SDK will auto-detect the base URL from the script source (e.g., https://pay.kuvarpay.com).2) Initialize the SDK#
Call KuvarPay.init as early as possible (e.g., on page load):Use a publishable API key intended for client-side usage.
baseUrl is OPTIONAL. If not provided, it will be auto-detected from the script source. Set it explicitly only if you need to override the default (e.g., for local development or staging).
In redirect mode (inlineCheckout: false), the SDK navigates away from your page; therefore onSuccess/onCancel/onError callbacks will not fire on the originating page.
Use your redirectUrl (provided during session creation) to bring the user back to a success page on your site once payment completes.
3) Open the Payment Modal#
Trigger the modal when the customer clicks a button or when your flow requires payment.Option A: Create New Session (Default Behavior)#
Option B: Use Existing Session (Recommended for Server-Side Integration)#
If you've already created a checkout session on your server, you can use it directly:Attach to your checkout button:4) How It Works#
New Session Creation (Option A)#
The SDK creates a Checkout Session on your behalf.
Built-in idempotency ensures repeated clicks or retries won't create duplicate sessions.
The secure checkout is displayed in a modal overlay on your page.
Existing Session Usage (Option B)#
The SDK skips session creation and directly loads the existing session in the modal.
This prevents duplicate session creation when you've already created a session on your server.
Recommended for server-side integrations where you control session creation timing and parameters.
5) Payment Data Parameters#
| Parameter | Type | Required | Description |
|---|
amount | number | Yes (new sessions) | Payment amount in the target fiat currency |
currency | string | Yes (new sessions) | Settlement currency code (e.g., 'NGN', 'RWF', 'KES') |
description | string | No | Payment description shown to the customer |
customerEmail | string | No | Customer's email address |
customerName | string | No | Customer's full name |
redirectUrl | string | No | URL to redirect after successful payment |
callbackUrl | string | No | Server-side webhook URL — KuvarPay will POST payment status updates to this URL |
expiresIn | number | No | Session time-to-live in seconds. Default: 1800 (30 minutes) |
metadata | object | No | Custom key-value data for your internal tracking. Stored with the session and transaction, accessible in dashboards and webhook payloads, but not displayed to the customer |
sessionId | string | No | Existing session ID — skips session creation and opens the modal directly |
defaultPaymentMethod | string | No | 'fiat' opens the checkout directly on the Local Currency rail (bank transfer / mobile money). Omit to let the customer choose Crypto vs Local Currency |
inlineCheckout (boolean): When false, the SDK redirects to the standalone checkout page after creating/using a session. Default true (embedded modal).
Accepting local-currency (fiat) payments#
By default every checkout offers Pay with Crypto and Pay with Local
Currency side by side. Customers paying with local currency pick their country
(NGN, GHS, KES, ZAR and more), pay by bank transfer or mobile money (e.g. M-Pesa
STK push), and your integration receives the SAME onSuccess callback /
payment.confirmed webhook as a crypto payment. The customer pays the
local-currency equivalent of your checkout amount plus the gate fee — your
settlement amount is unchanged.Merchant checkout controls. You can restrict what the checkout offers —
show only crypto, only local currency, limit the crypto tokens (stablecoins /
popular / all, or an explicit allow-list), and limit which fiat countries appear.
Set business-wide defaults in Dashboard → Settings → Checkout, or override
per session with checkoutConfig (see Hosted-checkout controls).
These are enforced server-side (not just hidden in the UI). When only one
rail is enabled the checkout skips the chooser and opens that rail directly;
when crypto is the only rail the customer still picks Smart Pay vs. a specific
token.To deep-link straight to the fiat rail (when both rails are enabled), pass
defaultPaymentMethod: 'fiat' in paymentData.6) Options and Callbacks#
options.theme: 'light' | 'dark' (default 'light').
callbacks.onSuccess(sessionId): invoked when the payment session completes successfully.
callbacks.onCancel(): invoked when the user closes/cancels the modal.
callbacks.onError(error): invoked on network/validation failures.
Redirect mode considerations:When inlineCheckout: false, callbacks will not run because the page navigates away to the standalone checkout.
Handle success/failure on your own route using the redirectUrl or server-side webhooks via callbackUrl.
7) Security Notes#
Use only your publishable (client) API key on the frontend.
Keep secret keys on your server. Never embed secret keys in client code.
8) Troubleshooting#
If the modal doesn't open, ensure KuvarPay.init ran without errors (check console if debug: true).
Verify your amount and currency are valid and within min/max limits configured on your KuvarPay account.
When using existing sessions, ensure the sessionId is valid and the session hasn't expired.
Server SDK (Backend Integration)#
For server-side operations (creating sessions, managing subaccounts, verifying transactions), use the official KuvarPay Server SDK for Node.js.Installation#
Constructor#
Core Payment Methods#
createCheckoutSession(data)#
Create a checkout session to lock in a fiat amount before payment.Returns: { sessionId, approvalUrl, raw }
Hosted-checkout controls (optional checkoutConfig)#
Merchants set defaults in the dashboard → Settings → Checkout (which rails to
accept, which crypto tokens, which fiat countries). You can override per
session by passing checkoutConfig — it is merged over the business defaults
(per field) and enforced server-side at transaction creation, so it can't be
bypassed by a headless caller. Omit it to inherit the business defaults; omit
everything and the checkout behaves exactly as before (both rails, all tokens).On the hosted checkout: passing only crypto hides the Local-Currency rail (the
customer still picks smart-pay vs. direct-token); only fiat hides Crypto and
drops the customer straight into currency/country selection; both (or omitted)
shows the chooser as today. Fetch a scoped token list directly with
getCurrencies({ scope: 'stablecoins' }).createTransaction(data)#
The "Headless" engine. Use this to generate a specific crypto deposit address for a session.Parameters: { checkoutSessionId, fromCurrency, fromNetwork, ... }
Returns: { transactionId, depositAddress, fromAmount, reference, raw }
calculatePayment(data)#
Predict exactly how much crypto a user needs to send for a target fiat amount. Use this for previewing rates in your custom UI.Parameters: { fromCurrency, fromNetwork, toCurrency, toAmount }
verifyPayment(sessionId)#
A convenience wrapper to check if a session has reached a final COMPLETED state.Returns: { verified: boolean, status, sessionId, transactionId, raw }
Direct Integration (Speed Mode)#
Use these methods to skip multiple round-trips if you already know the payment/subscription details.createDirectPayment(data)#
Creates a checkout session and immediately initiates a blockchain transaction.Payload: { amount, currency, fromCurrency, fromNetwork, description, ... }
Returns: The Transaction object (including depositAddress and fromAmount).
Best For: Headless checkouts where you want to show the address immediately.
createDirectSubscription(data)#
Creates a subscription checkout session and returns the approval URL.Payload: { amount, currency, customer: {email}, billingMode, ... }
Returns: { sessionId, approvalUrl, raw }
Best For: Reducing friction in subscription signups.
Subaccounts & Split Payments#
createSubaccount(data)#
Create a subaccount (SUB_xxx) to receive shares of split payments.Required: business_name, percentage_charge, settlement_bank, account_number, currency.
createSplitGroup(data)#
Create a group (SPL_xxx) to automatically distribute a percentage of a transaction across multiple subaccounts.getSubaccount(code) / getSplitGroup(code)#
Fetch details for a specific entity.
Banks & Account Verification#
getBanks(params)#
Fetch the supported payment methods (banks / mobile-money operators) for a corridor, from the fiat gateway. Returns only the corridor's primary-rail methods, each with an opaque methodId. Pass either country (ISO-3166 alpha-2, e.g. 'NG') or currency (ISO-4217, e.g. 'NGN') — a currency is resolved to its country server-side; when both are given, currency further filters. A bare string is treated as a currency.Examples: kv.getBanks({ currency: 'NGN', type: 'bank' }) · kv.getBanks({ country: 'KE', type: 'mobile_money' }) · kv.getBanks('NGN')
sandbox: set true (per call, or new KuvarPayServer({ sandbox: true })) when the key belongs to a sandbox-mode business. Live and sandbox are separate methodId spaces; a mismatch surfaces later as reason: 'METHOD_MODE_MISMATCH'.
A 3-letter country is treated as a currency, so getFiatCountries().code can be passed straight through.
resolveBankAccount(data) (deprecated)#
Deprecated alias for resolveRecipient. The legacy POST /api/v1/banks/resolve endpoint it used to call has been retired and now returns 404; this method routes to POST /api/v1/fiat/resolve-recipient instead.Required: methodId (from getBanks({ country, direction: 'outbound' })), plus accountNumber for a bank or phone for mobile money. Legacy method_id / account_number spellings are accepted.
No longer accepted: bank_code — passing it throws with a message pointing at getBanks.
Returns: the resolveRecipient shape (supported, resolved, accountName, bankName, reason) plus account_name / account_number aliases for older call sites.
Coverage: name enquiry resolves NGN bank accounts. Other corridors return supported: false — collect the name manually rather than blocking the flow.
Fiat Collection (Pay with Local Currency)#
Accept bank-transfer / mobile-money payments (NGN, GHS, KES, ZAR and more) on
any checkout session. Settlement, webhooks (payment.confirmed), and ledger
behave exactly like a crypto payment — the customer simply pays in their own
currency. The hosted checkout already offers this with zero code; the methods
below are for headless integrations that build their own payment UI.getFiatCountries()#
Supported collection corridors.Mind the field names — this is the most common integration mistake:| Field | What it actually is | Example |
|---|
code | ISO-4217 currency — not a country | KES |
country | Display name (may echo the currency on some rows) | Kenya |
countryCode | ISO-3166 alpha-2 — use this wherever a country is required | KE |
depositAvailable | A collection channel is live right now | true |
comingSoon | No provider at all — neither deposit nor payout | false |
Filter on depositAvailable before offering a corridor. The SDK guarantees
countryCode even against gateways that don't yet return it (it is recovered
from the flag emoji, which encodes ISO-2 exactly).getFiatMethods({ country | currency, type?, sandbox? }) (deprecated — alias for getBanks)#
Banks / mobile-money operators for a corridor. Each entry carries an opaque
methodId — pass that straight through at transaction creation.Takes an ISO-2 country (KE) or an ISO-4217 currency (KES). These
are different query parameters server-side; the SDK routes 2-letter codes to
country and 3-letter to currency, so either works.
Example: kv.getFiatMethods({ country: 'KE', type: 'mobile_money' }) — prefer kv.getBanks({ country: 'KE', type: 'mobile_money' })
Returns: { methods: [{ methodId, name, currency, type, ... }] }
An unknown corridor returns { methods: [] } — an empty list, not an
error. Check the length before indexing.
getFiatCheckoutQuote({ amount, baseCurrency, currency, country })#
Preview how much local currency the customer must pay to settle amount
baseCurrency to you (the customer bears the FX spread + gate fee).createFiatTransaction(data)#
Create the FIAT collection transaction on a checkout session.Required: checkoutSessionId, fromCurrency (customer's local currency), country (ISO-2).
Optional: methodId, phone (mobile money), customerEmail, customerName.
Async: poll getSessionStatus(sessionId) until raw.data.fiatDeposit.depositId
appears (~1–2 s), then drive the flow off fiatDeposit.nextAction.
getFiatDeposit(depositId)#
Poll the deposit: status (AWAITING_PAYMENT → RECEIVED → PROCESSING → CREDITED),
nextAction, paymentInstructions (bank account / STK state / redirect URL),
and the locked quote.submitFiatDepositAction(depositId, action, otp?)#
Drive the next step the deposit asks for:nextAction === 'CONFIRM_QUOTE' → submitFiatDepositAction(id, 'CONFIRM_QUOTE') — locks the rate and (for mobile money) fires the payment prompt.
nextAction === 'ENTER_OTP' → submitFiatDepositAction(id, 'SUBMIT_OTP', '123456').
nextAction === 'ENTER_PIN' → no call needed; the customer approves on their phone — keep polling.
simulateFiatDepositComplete(depositId)#
Sandbox only. Force-completes a deposit through the full pipeline
(transaction COMPLETED → session COMPLETED → your webhook fires).waitForFiatDeposit(sessionId, options?)#
Polls the session until the deposit projection attaches, with a bounded
deadline and backoff. Prefer this over hand-rolling a loop.Options: timeoutMs (default 30000), pollIntervalMs (default 1000),
maxIntervalMs (default 5000), until (custom stop condition).
Rejects with err.code === 'FIAT_DEPOSIT_TIMEOUT' on expiry, carrying
err.lastDeposit so you can see how far it got.
Wait for settlement instead with until: d => d.status === 'CREDITED'.
createFiatPayment(data) — one-shot#
Creates the checkout session, starts the fiat transaction, and waits for the
deposit in a single call. The fiat analogue of createDirectPayment.Required: amount, currency, fromCurrency, country.
Optional: methodId, phone, plus any createCheckoutSession field.
Control: waitForDeposit (default true), autoConfirmQuote
(default false), timeoutMs, pollIntervalMs.
Returns { sessionId, transactionId, depositId, nextAction, quote,
paymentInstructions, deposit, session, transaction }.
It deliberately stops at the quote. Confirming locks the FX rate and (for
mobile money) fires the prompt on the customer's phone, so show them
quote first and confirm explicitly. paymentInstructions stays null
until the quote is confirmed — that is the gateway's behaviour, not a bug.The equivalent long-hand, if you need the intermediate steps:
Payouts (Sends) — Secret key only#
Move money out of your USD balance to a recipient's bank or mobile-money
account. These endpoints require your secret key (client/publishable keys are
rejected) and must run server-side only. Payouts settle asynchronously — listen
for the completion webhook (verify with verifyWebhookSignature) or poll
getFiatSend.Merchant payouts are bank / mobile money only — crypto withdrawals are not
available to merchants.
getBanks({ country, type?, direction: 'outbound' })#
List payout (outbound) banks / mobile-money operators for a country. (direction
defaults to 'inbound', which returns collection methods.) Pass sandbox: true — or
construct the client with it — when your key is a sandbox-mode business.resolveRecipient({ methodId, accountNumber?, phone? })#
Name-enquiry on a recipient before sending. Returns { supported, resolved, accountName, bankName, reason }.Coverage: NGN bank accounts. Every other currency — and all mobile money — returns
supported: false. That is a normal response, not an error, and it is the signal to collect
the recipient's name from your user and carry on. Do not gate the payout on resolved:
outside NGN it is always false, so gating makes every other corridor un-payable. Methods
flagged manualInput in the getBanks response behave the same way.reason tells the failures apart:reason | meaning |
|---|
METHOD_MODE_MISMATCH | the methodId came from the other environment — see getBanks and sandbox |
null (with supported: false) | name enquiry is unavailable for this corridor |
MISSING_FIELDS | no accountNumber / phone for the method's rail |
NOT_FOUND | the lookup ran and the provider did not recognise the account |
In sandbox the enquiry is forwarded to the provider's own sandbox environment — it is not
stubbed, and there is no published test account number for it. Treat supported: false or
NOT_FOUND as an ordinary sandbox outcome and exercise your manual-name path instead of
asserting on a resolved name.createFiatSend(data)#
Initiate a bank / mobile-money payout (USD-debited). Provide EXACTLY ONE of
sourceAmountUsd (USD-input; the recipient's local amount floats) or
destinationAmount (recipient receives this exact local amount).Required: destinationCountry, destinationCurrency, recipient.methodId.
Returns: { sendId }. ownerType/ownerId default to your business.
getFiatSend(sendId)#
Poll a payout's status: PROCESSING → COMPLETED / FAILED / REFUNDED.
Webhook Security#
Protect your server from spoofed notifications by verifying the KuvarPay signature.The header arrives as sha256=<hex>. Pass it through unmodified —
verifyWebhookSignature strips the prefix and also accepts a bare hex
digest. It returns false for bad or malformed signatures rather than
throwing, so your handler never 5xxs on junk input (the gateway retries any
5xx as a failed delivery).
Other Utility Methods#
| Method | Description |
|---|
getSessionStatus(id) | Detailed state of a checkout session. |
getTransactionStatus(id) | Detailed state of a blockchain transaction. |
getTransactionStatusByReference(ref) | Fetch status using your merchant reference. |
listTransactions(params) | Paginated list of recent transactions. |
getCurrencies(params) | Fetch all supported crypto tokens and networks. |
getOptimalTransferFee(amount, currency) | Calculate estimated network/payout fees. |
Example: Headless Checkout Flow#
If you don't want to use the modal, follow this server-side flow:
End-to-End Integration Example#
This shows the recommended flow: create the session on your server, pass the sessionId to the frontend, open the modal, and verify on your server.Step 1: Server — Create the checkout session#
Step 2: Frontend — Open the modal with the sessionId#
Step 3: Server — Verify the payment#
Subscription Integration#
The SDK also supports embedded subscription approvals via KuvarPay.openSubscription(...). This loads the subscription approval UI and communicates status back to your page using callbacks, similar to one-time payments.Redirect (Non-Embedded) Subscription Approval#
When inlineCheckout: false is set at init, openSubscription(...) will redirect to the standalone approval page instead of opening a modal.As with payments, callbacks will not fire on the original page because the browser navigates away; use your return routes or server-side notifications to update user state.1) Open the Subscription Modal#
Option A: Use Existing Subscription Session#
If you have already created a subscription checkout session on your backend (recommended), you can pass either the session ID or the approval URL returned by your backend. When both are provided, the SDK will prefer the approval URL.Option B: Create New Session From the Browser (Metered-only)#
If you prefer to let the SDK create the session from the browser, pass the required metered parameters.2) Callback Events#
The SDK handles all communication between the subscription modal and your page automatically. Use the callbacks provided to openSubscription(...) to respond to events:Success — fired when the subscription is successfully approved/activated
Error — fired when approval fails (insufficient funds, network errors, authorization issues, etc.)
Cancel — fired when the user cancels/closes the approval flow
3) Parameters for openSubscription(...)#
Required for new session creation (metered-only):customer — { email: string; firstName: string; lastName: string }
expectedUsage — { amount: number; currency: string } (REQUIRED)
strategy — 'conservative' | 'moderate' | 'liberal' | 'custom' (REQUIRED)
customMultiplier — number (REQUIRED if strategy is 'custom', must be > 0)
metadata — object (e.g., { planName, planDescription })
sessionId — use when you already have a session created (skip creation)
approvalUrl — alternatively, pass the approval URL returned by your backend
options.theme — 'light' | 'dark'
businessId — optional per-call override; SDK will use the businessId provided at KuvarPay.init(...) if not specified
onSuccess(result) — subscription confirmed; result typically includes { sessionId, subscriptionId, status, txHash? }
onCancel() — closed/cancelled
onError(error) — any failure in session creation or approval flow
Verifying Session Status Programmatically#
If you need to confirm whether a checkout session completed successfully (or fetch its latest status/details), the SDK provides a helper:The response includes the current session status (PENDING, COMPLETED, etc.).
This is useful in redirect flows where you land on a confirmation page and want to double-check the final status.
Subscription Invoices (Client-Side Helper)#
For existing subscriptions, you may need to generate usage-based invoices programmatically. The SDK provides helpers:KuvarPay.createSubscriptionInvoice(subscriptionId, data) — create an invoice immediately or scheduled based on chargeSchedule
KuvarPay.scheduleSubscriptionInvoice(subscriptionId, data) — convenience wrapper that enforces a future dueDate and sets chargeSchedule: 'SCHEDULED'
For backward compatibility, KuvarPay.createMeteredInvoice(...) remains available as an alias.
The SDK does not automatically create invoices after subscription approval. Merchants decide when to invoice.
For trials, you can either:Schedule the invoice for after the trial ends using chargeSchedule: 'SCHEDULED' and a dueDate (Unix seconds);
Or create the invoice on the day the trial expires with chargeSchedule: 'IMMEDIATE'.
SDK Versioning and Notes#
Metadata support for payments: Pass a metadata object in openPayment(...) or createCheckoutSession(...) to attach custom key-value data to sessions and transactions. Metadata is stored server-side, included in webhooks and dashboards, but not shown to customers on the payment page.
Metered-only subscription creation parameters (expectedUsage, strategy, customMultiplier).
Client-side helper to create subscription invoices for existing subscriptions (alias: createMeteredInvoice).
New helper: KuvarPay.getSessionStatus(sessionId) to verify the latest status of a checkout session.
Backward-compatible: Passing sessionId to openSubscription continues to work as before.
Auto-initialization: If your script tag includes data-api-key, data-business-id, and data-base-url, the SDK will initialize itself automatically.
Modified at 2026-08-02 19:42:45