This guide walks through a complete Banxa Hosted Checkout integration on the web using the JS Native Payments SDK. By the end, you'll have a working buy flow: create an order on your backend, embed the checkout in an iframe on your page, and track the final order status.
Web works differently from React Native. There is no in-app WebView and no single client that runs everywhere. Instead the SDK ships two entry points, and you use both:
- Backend (
/api): theBanxaApiClientcreates orders and fetches quotes. It usesx-api-keyauthentication, so it must run server-side. Never ship your API key to the browser. - Frontend (
/web): the<banxa-hosted-checkout>web component embeds the returnedcheckoutUrlin an iframe and reports success, failure, and cancel events.
For the full API surface, see the JS Native Payments SDK Reference.
The package name refers to its Banxa Native payment sheet capability, a separate feature for partners who run their own KYC. Here, you're using it to create orders and embed the hosted checkout iframe, not the native payment sheet. Ignore the "Native Payments" framing unless you later move to Banxa Native.
- A web app with a backend you control (Node, or any runtime that can run the API client).
- Your Banxa partner reference and API key (sandbox for development, production after approval).
- A configured webhook endpoint (optional but recommended for order status tracking).
npm install @banxa-official/javascript-native-payments-sdkYou do not need the @primer-io/primer-js peer dependency for a Hosted Checkout iframe integration. It is only required for the native payment sheet, which is a Banxa Native feature.
Create a BanxaApiClient on your server. Import from the root (or /api) entry point so no browser or web component code is pulled in.
import { BanxaApiClient } from '@banxa-official/javascript-native-payments-sdk';
const banxa = new BanxaApiClient({
apiKey: process.env.BANXA_API_KEY,
partner: process.env.BANXA_PARTNER,
environment: 'sandbox', // or 'production'
});| Field | Description |
|---|---|
apiKey | Your v2 API key. Keep it server-side only. |
partner | Your partner identifier (for example binance, metamask). |
environment | 'sandbox' or 'production'. |
Fetch live pricing before showing an amount to the customer. Call this close to when you show the price, because crypto rates move quickly and quotes are indicative.
const quote = await banxa.getQuote({
fiat: 'AUD',
crypto: 'USDT',
blockchain: 'TRON',
paymentMethodId: 'debit-credit-card',
fiatAmount: '100',
}, 'buy');Either fiatAmount or cryptoAmount is required, along with fiat, crypto, blockchain, and paymentMethodId. Quotes carry no quote ID; there is nothing to pass into order creation.
Expose an endpoint that your frontend calls when the customer confirms. Create the order there and return the checkoutUrl to the browser.
// server.ts (Express example)
import express from 'express';
import { BanxaApiClient } from '@banxa-official/javascript-native-payments-sdk';
const app = express();
app.use(express.json());
const banxa = new BanxaApiClient({
apiKey: process.env.BANXA_API_KEY,
partner: process.env.BANXA_PARTNER,
environment: 'production',
});
app.post('/api/create-order', async (req, res) => {
const { userId, amount, fiat, crypto, walletAddress } = req.body;
try {
const order = await banxa.createOrder({
externalCustomerId: userId,
fiat,
crypto,
fiatAmount: amount,
walletAddress,
redirectUrl: `${process.env.FRONTEND_URL}/checkout/return`,
});
res.json({ orderId: order.id, checkoutUrl: order.checkoutUrl });
} catch (error) {
const message = error instanceof Error ? error.message : 'Order creation failed';
res.status(500).json({ error: message });
}
});externalCustomerId: your stable customer identifier. Use one per customer, not per order, so Banxa recognises returning customers.fiat,crypto, and eitherfiatAmountorcryptoAmount.walletAddress: the customer's receiving wallet address.redirectUrl: where the customer is returned after checkout. Use a URL on your checkout page origin so the iframe can detect the return.
{
"checkoutUrl": "https://partner.banxa.com/portal?expires=xxx&oid=xxx&signature=xxx",
"id": "191fa5b4b1f45e1cf784422e09317d56",
"externalOrderId": "a4b427ccb872a1744b317456bd0d165f",
"externalCustomerId": "user-123",
"fiat": "CAD",
"fiatAmount": "1000.00",
"crypto": "USDC",
"cryptoAmount": null,
"blockchain": "TRON"
}The checkoutUrl is valid for 1 minute from order creation and must be loaded within that window. Once loaded it does not expire. Create the order at the moment the customer confirms, then mount the component immediately. Do not create orders in advance. See FAQ.
Register the component once, then set the checkout-url and return-url and listen for the outcome events.
<script type="module">
import { registerBanxaHostedCheckout } from '@banxa-official/javascript-native-payments-sdk/web';
registerBanxaHostedCheckout();
</script>
<banxa-hosted-checkout id="checkout"></banxa-hosted-checkout>
<script type="module">
async function startCheckout() {
const res = await fetch('/api/create-order', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userId: 'user-123',
amount: '100',
fiat: 'AUD',
crypto: 'USDT',
walletAddress: '0x...',
}),
});
if (!res.ok) throw new Error('Failed to create order');
const { checkoutUrl } = await res.json();
const checkout = document.getElementById('checkout');
checkout.setAttribute('checkout-url', checkoutUrl);
checkout.setAttribute('return-url', `${window.location.origin}/checkout/return`);
checkout.addEventListener('banxa:checkout-success', () => { window.location.href = '/success'; });
checkout.addEventListener('banxa:checkout-failure', () => { /* show retry */ });
checkout.addEventListener('banxa:checkout-cancelled', () => { /* customer cancelled */ });
}
startCheckout().catch(console.error);
</script>Register the component once (for example in an entry module), then bind through a ref. Custom-element events are not React synthetic events, so attach them with addEventListener.
import { useEffect, useRef } from 'react';
import { registerBanxaHostedCheckout } from '@banxa-official/javascript-native-payments-sdk/web';
registerBanxaHostedCheckout();
function Checkout({ checkoutUrl }: { checkoutUrl: string }) {
const ref = useRef<HTMLElement>(null);
useEffect(() => {
const el = ref.current;
if (!el) return;
const onSuccess = () => { /* ... */ };
el.addEventListener('banxa:checkout-success', onSuccess);
return () => el.removeEventListener('banxa:checkout-success', onSuccess);
}, []);
return (
<banxa-hosted-checkout
ref={ref}
checkout-url={checkoutUrl}
return-url={`${window.location.origin}/checkout/return`}
/>
);
}Register the component once, bind attributes, and listen for events with @banxa:checkout-success. Tell your bundler to treat banxa-hosted-checkout as a custom element so Vue does not try to resolve it as a component.
If you would rather not wire the component by hand, runBuyCheckoutFlow creates the order and mounts the iframe for you:
import { runBuyCheckoutFlow } from '@banxa-official/javascript-native-payments-sdk/web';
const { element } = await runBuyCheckoutFlow({
client, // a BanxaApiClient; safe here only if this code runs server-adjacent, otherwise create the order via your backend
request: { externalCustomerId: 'user-123', fiat: 'AUD', crypto: 'USDT', fiatAmount: '100', walletAddress: '0x...', redirectUrl: `${window.location.origin}/checkout/return` },
container: document.getElementById('checkout'),
});
element.addEventListener('banxa:checkout-success', () => { /* completed */ });Because the BanxaApiClient uses your API key, prefer creating the order through your backend (Step 3) and mounting <banxa-hosted-checkout> with the returned checkoutUrl. Use runBuyCheckoutFlow only where the client can run without exposing the key.
For KYC camera capture, the embedding page must grant camera and microphone permissions. See Embedded Checkout, Mobile.
The success event is a UI signal, not the authoritative order state. Confirm the final status from your backend using the order ID you stored in Step 3.
const orderDetails = await banxa.getOrder(orderId);
if (orderDetails.status === 'complete') {
// Credit the customer, update your database, and so on.
}Only the terminal statuses are final. Do not credit the customer until the order reaches complete. For the full list, see Order Statuses.
For a full list of all orders across all customers (reconciliation, reporting), use the Banxa API directly. The SDK is not the reconciliation surface.
The banxa:checkout-success event fires in the browser when the customer finishes, but for reliable order tracking configure webhooks on your backend.
Webhooks fire for every order status change. Configure your webhook URL in the merchant dashboard.
The typical pattern:
- Browser success event: optimistic UI update ("your order is processing").
- Webhook to your backend: authoritative order state.
- Backend updates your records and notifies the client.
See Webhooks for payload structure and signature verification. Webhook signatures are verified with your HMAC secret, not the v2 x-api-key.
Wrap backend calls in try/catch. The client throws BanxaApiError for API and network failures.
import { BanxaApiClient, BanxaApiError } from '@banxa-official/javascript-native-payments-sdk';
try {
const order = await banxa.createOrder({ /* ... */ });
} catch (error) {
if (error instanceof BanxaApiError) {
console.error('Status code:', error.statusCode);
console.error('Response body:', error.responseBody);
console.error('Errors:', error.errors);
} else {
console.error('Unknown error:', error);
}
}Show user-facing messages only from validated error fields. Do not expose raw error strings that may include internal detail. For error codes and their meanings, see Error Codes.
Use the sandbox environment for all development and testing:
const banxa = new BanxaApiClient({
apiKey: process.env.BANXA_SANDBOX_API_KEY,
partner: process.env.BANXA_PARTNER,
environment: 'sandbox',
});For test credentials (mobile numbers, card numbers, PIN codes, KYC data), see Sandbox Test Data.
- JS Native Payments SDK Reference: full method and component reference.
- Webhooks: configure webhook notifications.
- Order Statuses: full status reference.
- Sandbox Test Data: credentials and test values.