Skip to content
Last updated

JS Native Payments SDK Integration Guide (Web)

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): the BanxaApiClient creates orders and fetches quotes. It uses x-api-key authentication, so it must run server-side. Never ship your API key to the browser.
  • Frontend (/web): the <banxa-hosted-checkout> web component embeds the returned checkoutUrl in an iframe and reports success, failure, and cancel events.

For the full API surface, see the JS Native Payments SDK Reference.

Why 'Native Payments' in the name?

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.


Before you start

Prerequisites

  • 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).

Install the SDK

npm install @banxa-official/javascript-native-payments-sdk

You 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.


Step 1: Initialise the client on your backend

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'
});
FieldDescription
apiKeyYour v2 API key. Keep it server-side only.
partnerYour partner identifier (for example binance, metamask).
environment'sandbox' or 'production'.

Step 2: Get a quote

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.


Step 3: Create an order from your backend

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 });
  }
});

Required fields

  • externalCustomerId: your stable customer identifier. Use one per customer, not per order, so Banxa recognises returning customers.
  • fiat, crypto, and either fiatAmount or cryptoAmount.
  • 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.

Response

{
  "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 checkout URL expires in 1 minute

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.


Step 4: Embed the checkout on your page

Register the component once, then set the checkout-url and return-url and listen for the outcome events.

Vanilla JS

<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>

React

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`}
    />
  );
}

Vue

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.

Let the SDK pick the flow

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.

Permissions

For KYC camera capture, the embedding page must grant camera and microphone permissions. See Embedded Checkout, Mobile.


Step 5: Look up order status

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.


Error handling

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.


Testing

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.


Next steps