<!-- Source: https://docs.infraio.xyz/en/sdks/javascript -->
<!-- Last updated: 2026-10-04 -->

# JavaScript / Browser SDK

`@lartech/infraio-checkout-js` is the only SDK we publish today. It
runs in the browser and opens our hosted checkout. There is no
backend SDK yet. Call the API directly; the [Quickstart](https://docs.infraio.xyz/en/get-started/quickstart)
includes an HMAC signing helper.

- Current version: `0.1.1-beta.17` (pre-1.0; expect minor breaks)
- Formats: **ESM** (`index.js`), **CJS** (`index.cjs`), **IIFE** (`index.global.js`)
- Types bundled (`index.d.ts`)
- Zero runtime peers — no React, jQuery, or other deps

> **Note:**
>
> There is **no server-side entry**. Signature verification helpers
> for webhooks are not bundled — implement them yourself with `crypto`
> (the [signature verification page](https://docs.infraio.xyz/en/webhooks/signature-verification)
> has copy-paste code in 4 languages).

## Install

**npm**

```bash
npm install @lartech/infraio-checkout-js
```

**pnpm**

```bash
pnpm add @lartech/infraio-checkout-js
```

**yarn**

```bash
yarn add @lartech/infraio-checkout-js
```

**bun**

```bash
bun add @lartech/infraio-checkout-js
```

**CDN**

```html
<script src="https://unpkg.com/@lartech/infraio-checkout-js/dist/index.global.js"></script>
<script>
  const sdk = await InfraIo.loadInfraIo("pk_live_yourkeyhere");
  sdk.checkout({ sessionId, checkoutUrl });
</script>
```

## `loadInfraIo(publicKey, options?)`

Returns a `Promise<InfraIoInstance>`.

```ts
import { loadInfraIo } from "@lartech/infraio-checkout-js";

const sdk = await loadInfraIo("pk_live_yourkeyhere", {
  // Optional. Override only when pointing at a non-prod environment.
  checkoutUrl: "https://checkout-dev.infraio.xyz",
});
```

| Param | Type | Required | Notes |
| --- | --- | --- | --- |
| `publicKey` | `string` | ✓ | Must match `pk_(live\|test)_…` |
| `options.checkoutUrl` | `string` | — | Override the base checkout URL. Default: `https://checkout.infraio.xyz`. The checkout page connects to the matching API automatically. |

## `sdk.checkout({ … })`

Opens the hosted checkout. Returns `void` (use callbacks for state).

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `sessionId` | `string` | ✓ | `session_key` returned by `POST /b2b/v1/checkout-sessions/quick` |
| `checkoutUrl` | `string` | — | Full URL returned by the same endpoint. If omitted, SDK constructs it from `loadInfraIo()`'s `checkoutUrl` (or default) + `sessionId` |
| `mode` | `"popup" \| "redirect" \| "embed"` | — | Default `"popup"` |
| `container` | `string \| HTMLElement` | embed only | CSS selector or DOM element where the iframe mounts |
| `width` | `number` | — | Popup only. Default `560`. Clamped `[320, 1280]` |
| `height` | `number` | — | Popup only. Default `780`. Clamped `[400, 1000]` |
| `timeoutMs` | `number` | — | Popup only. Iframe load timeout. Default `30000`. Pass `0` to disable |
| `locale` | `string` | — | BCP-47 tag forwarded as `?locale=` to the checkout page (`en`, `vi`, `ja`, `ko`, `es`, `pt-BR`, `ru`, `tr`, `zh-CN`, `zh-TW`) |
| `hideSummary` | `boolean` | — | Hide the order-summary column. Default `false` |
| `hideHeader` | `boolean` | — | Hide the InfraIO Pay header and built-in wallet-connect button. Default `false`. Pair with `walletAddress` for full white-label |
| `walletAddress` | `string` | — | Pre-connect a buyer wallet. Requires `onSignRequest` |
| `walletChainId` | `number` | — | EVM chain ID for the pre-connected wallet |
| `onReady` | `() => void` | — | Fires once the iframe is interactive. Popup/embed only |
| `onSignRequest` | `(req: { method: string; params: unknown[] }) => Promise<string>` | when `walletAddress` set | The SDK proxies wallet RPCs to your handler; return the signed hex |
| `onSuccess` | `({ sessionId }) => void` | — | Fires on successful payment. **Not authoritative — webhook is** |
| `onCancel` | `() => void` | — | Fires when the buyer closes the popup/embed without paying |
| `onError` | `(err: InfraIoError) => void` | — | Fires on iframe load failure (popup + embed modes). Invalid arguments are thrown synchronously, **not** delivered here. Redirect mode has no runtime error surface — failure is observed in the redirected page. |

> **Warning:**
>
> `onSuccess` is **not** authoritative. It can fire even when the
> webhook later determines the payment failed (testnet reorgs,
> buyer-side timing). Use it only for UX (show "Thanks!", redirect).
> Always confirm via webhook before fulfilling.

## `sdk.close()`

Programmatically dismiss an open popup or embed. No-op for redirect
mode.

```ts
sdk.checkout({ sessionId, mode: "popup", /* … */ });
// later, e.g. when user navigates away
sdk.close();
```

## `sdk.openRefundRequest({ … })`

Opens the hosted **refund-request form** for a one-time token your
backend minted via `POST /b2b/v1/merchants/{merchant_id}/refund-requests`. The buyer fills in
their refund destination address + reason + (optional) metadata on
our page; your page only handles the open/close lifecycle. Returns a
**`close()` function** — call it to programmatically dismiss the
popup or detach the embed iframe. In redirect mode the returned
function is a no-op.

> **Note:**
>
> The form lives at `https://checkout.infraio.xyz/refund-request/:token`.
> This method just wraps that URL in a popup / redirect / embed so the
> buyer never leaves your domain (in popup / embed) or returns to it
> automatically (in redirect). The endpoint your backend hits to mint
> the token is `POST /b2b/v1/merchants/{merchant_id}/refund-requests` — HMAC-signed with your
> secret key, same auth as the rest of the B2B surface. See
> [Concepts → Refunds](https://docs.infraio.xyz/en/concepts/refunds#mint-via-b2b-api).

```ts
import { loadInfraIo } from "@lartech/infraio-checkout-js";

const sdk = await loadInfraIo("pk_live_yourkeyhere");

// Mint the token server-side, then hand it to the SDK in the browser.
const { token } = await fetch("/api/mint-refund-token", { method: "POST" }).then(r => r.json());

const close = sdk.openRefundRequest({
  token,
  mode: "popup",
  onSuccess: ({ linkToken, refundId }) => {
    // Buyer submitted the form.
    // linkToken → /r/:linkToken status page (share with the buyer).
    // refundId  → use with B2B API to approve / reject.
    window.location.href = `/r/${linkToken}`;
  },
  onCancel: () => { /* buyer closed without submitting */ },
});

// Programmatically dismiss the popup later if needed:
// close();
```

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `token` | `string` | ✓ | The `rfqt_…` token returned by `POST /b2b/v1/merchants/{merchant_id}/refund-requests` |
| `mode` | `"popup" \| "redirect" \| "embed"` | — | Default `"popup"`. Same surface semantics as `sdk.checkout()` — see [Mode notes](#mode-notes) |
| `container` | `string \| HTMLElement` | embed only | CSS selector or DOM element where the iframe mounts |
| `locale` | `string` | — | BCP-47 tag forwarded as `?locale=` (`en`, `vi`, `ja`, `ko`, `es`, `pt-BR`, `ru`, `tr`, `zh-CN`, `zh-TW`) |
| `hideHeader` | `boolean` | — | Hide the InfraIO Pay header inside the iframe. In popup/embed the SDK draws its own modal chrome, so the page header is usually unnecessary. Default `false` |
| `hideSummary` | `boolean` | — | Hide the Order Summary column, showing only the refund form. Default `false` |
| `walletAddress` | `string` | — | Pre-fill the destination wallet field (`?wallet_address=`). Lets a merchant that already knows the buyer's wallet skip the manual re-type |
| `onSuccess` | `(data: { linkToken: string; refundId: string }) => void` | — | Fires after the buyer submits the form. `linkToken` → `/r/:linkToken` status page to share with the buyer. `refundId` → use with the B2B API to approve / reject |
| `onCancel` | `() => void` | — | Fires when the buyer closes the popup/embed without submitting |
| `onError` | `(err: InfraIoError) => void` | — | Fires on iframe load failure or invalid args. Token-expiry / cancellation is handled by the hosted page, not via `onError` |

**Token states surfaced via `onError`**

If the buyer opens a stale token, the page itself handles the
display (renders an "Expired — request new link" prompt, etc.), and
the SDK does **not** fire `onError` for those cases — the buyer is
inside the form flow and your code doesn't need to react. `onError`
only fires for things your code can act on (bad arguments, network
failure loading the iframe).

> **Warning:**
>
> `openRefundRequest` records the refund intent — it does **not** move
> funds. After `onSuccess`, the refund row is `PENDING` (or `APPROVED`
> if your merchant config auto-approves customer refunds). You still
> need to sign and broadcast the on-chain transfer from your merchant
> wallet, then post the tx hash to `POST /b2b/v1/refunds/:id/submit-tx`.
> See [Concepts → Refunds](https://docs.infraio.xyz/en/concepts/refunds) for the full lifecycle.

## Error class

```ts
import { InfraIoError } from "@lartech/infraio-checkout-js";

sdk.checkout({
  sessionId,
  onError: (err: InfraIoError) => {
    switch (err.code) {
      case "invalid_request_error":   /* bad sessionId / args */ break;
      case "iframe_load_error":       /* iframe failed to load */ break;
      case "iframe_timeout_error":    /* exceeded timeoutMs */ break;
      case "already_open_error":      /* another checkout is already open */ break;
      case "network_error":           /* transient network problem talking to the checkout origin */ break;
      case "api_error":               /* backend returned a non-2xx for an SDK-issued call */ break;
    }
  },
});
```

`sdk.openRefundRequest()` throws only `invalid_request_error` (missing
or invalid token / args), synchronously. `iframe_load_error` (the
refund-request iframe failed to load) is delivered **asynchronously via
`onError`**, not thrown. It does **not** emit `iframe_timeout_error` or
`already_open_error` — the refund-request popup has no load-timeout
and allows multiple concurrent popups.

## Mode notes

### Popup
- Centered overlay with a dark semi-transparent backdrop
- `z-index: 2147483647` (max int32) — sits above everything else
- Body scroll is locked while open; restored on close
- Close button gets initial focus; Tab is trapped in the popup
- Closed by: close button, Escape, click outside, `sdk.close()`. All
  of them call `onCancel`
- The checkout page can request resize via `postMessage` — the SDK
  clamps within `width`/`height` limits

### Redirect
- Hard navigation via `window.location.href`
- Automatically appends `?return_url=<current-page>` so the buyer
  returns where they came from. If your `success_url` / `cancel_url`
  on the session already cover this, the round-trip ignores
  `return_url`

### Embed
- iframe with `allow="payment; clipboard-write"` (HTML5 Feature
  Policy directives — not the `sandbox` attribute). The iframe is
  served from the checkout origin, so wallet popups and clipboard
  writes from the buyer side work without further opt-in.
- Container width is 100%; height auto-sizes via `INFRAIO_RESIZE`
  postMessage, clamped `[200, 2000]px`
- No CSS isolation beyond the iframe boundary — your parent page
  styles do not bleed in
- Always wire `onReady` so you can hide your own loading state when
  the checkout becomes interactive

## TypeScript

All types are bundled. Most useful exports:

```ts
import type {
  CheckoutOptions,
  RefundRequestOptions,
  LoadOptions,
  InfraIoInstance,
  InfraIoErrorCode,
} from "@lartech/infraio-checkout-js";
import { loadInfraIo, InfraIoError, VERSION } from "@lartech/infraio-checkout-js";
```

`VERSION` is the SDK's own version string — useful in bug reports.
