<!-- Source: https://docs.infraio.xyz/en/get-started/quickstart -->
<!-- Last updated: 2026-10-04 -->

# Quickstart

Goal: take a real test-mode payment end-to-end. By the end you'll have
a session created from your server, a checkout opened in the buyer's
browser, and a signed webhook delivered to your localhost.

> **Note:**
>
> You need a **test** API key pair (`pk_test_…` + `sk_test_…`) and a
> **webhook signing secret** (`whsec_…`). Generate them in the [merchant
> dashboard](https://app.infraio.xyz) under **Developers →
> API keys** and **Developers → Webhooks**.

## 1. Install the browser SDK

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

## 2. Create a checkout session (server)

Sessions are created from your server, signed with HMAC-SHA256 using
your **secret** key. Never put `sk_` in the browser.

### Build the canonical signing string

```
METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + BODY
```

For a `POST /b2b/v1/checkout-sessions/quick` request at
unix-timestamp `1715990400` with body `{...}`, the signing input is:

```
POST
/b2b/v1/checkout-sessions/quick
1715990400
{"items":[...],"currency":"USD","success_url":"..."}
```

Sign it with `HMAC-SHA256(secret_key, signingString)`, output hex.

### Send the request

```ts filename="server/create-session.ts"
import { fetch } from "undici";
import { createHmac } from "node:crypto";

const PUBLIC_KEY = process.env.INFRAIO_PUBLIC_KEY!;   // pk_test_…
const SECRET_KEY = process.env.INFRAIO_SECRET_KEY!;   // sk_test_…
const BASE_URL   = "https://api-dev.infraio.xyz";

const path = "/b2b/v1/checkout-sessions/quick";
const body = JSON.stringify({
items: [
  {
    sku: "tee-large-blue",
    label: "Indigo Tee — L",
    unit_price: "49.00",
    quantity: 1,
    currency: "USD",
    image_url: "https://your-shop.test/img/tee.png",
  },
],
currency: "USD",
customer_email: "buyer@example.com",
success_url: "https://your-shop.test/return?status=success",
cancel_url:  "https://your-shop.test/return?status=cancel",
external_ref: "ord_1234",        // your order id; stored on the order, not echoed in webhooks — look it up via order_id
expires_in: 1800,                // seconds; default 30 min
idempotency_key: crypto.randomUUID(), // safe to retry
});

const timestamp = Math.floor(Date.now() / 1000).toString();
const signingInput = ["POST", path, timestamp, body].join("\n");
const signature = createHmac("sha256", SECRET_KEY)
.update(signingInput)
.digest("hex");

const res = await fetch(`${BASE_URL}${path}`, {
method: "POST",
headers: {
  "Content-Type": "application/json",
  "X-Client-ID":  PUBLIC_KEY,
  "X-Timestamp":  timestamp,
  "X-Signature":  signature,
},
body,
});

// Every API response is wrapped in an envelope — the actual payload
// is under `data`. Treat `code`/`message` at the top level as the
// outcome marker (`201` / `"created"` for success).
const { code, message, data } = await res.json() as {
code: number;
message: string;
data: { session_key: string; checkout_url: string; order_id: string; expires_at: string };
};
const { session_key, checkout_url, order_id, expires_at } = data;
// Send session_key + checkout_url to the browser. order_id and expires_at
// are for your own bookkeeping.
```

> **Warning:**
>
> Clock skew matters. The API rejects requests with a `X-Timestamp`
> more than **5 minutes** off from server time. Sync your servers via
> NTP; don't trust the wall clock of a long-running cron.

## 3. Open the checkout (browser)

**Popup**

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

const sdk = await loadInfraIo(process.env.NEXT_PUBLIC_INFRAIO_PK!);

sdk.checkout({
  sessionId: session_key,
  checkoutUrl: checkout_url,
  mode: "popup",            // default
  onReady:   () => console.log("checkout interactive"),
  onSuccess: ({ sessionId }) => {
    // UX only — see the warning below. Webhook is authoritative.
    location.assign("/thanks");
  },
  onCancel: () => console.log("buyer closed the popup"),
  onError:  (err) => console.error(err.code, err.message),
});
```

**Redirect**

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

const sdk = await loadInfraIo(process.env.NEXT_PUBLIC_INFRAIO_PK!);

sdk.checkout({
  sessionId: session_key,
  checkoutUrl: checkout_url,
  mode: "redirect",
});
// Browser navigates to checkout.infraio.xyz; the buyer returns to
// your `success_url` / `cancel_url` from the create-session payload.
```

**Embed**

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

const sdk = await loadInfraIo(process.env.NEXT_PUBLIC_INFRAIO_PK!);

sdk.checkout({
  sessionId: session_key,
  checkoutUrl: checkout_url,
  mode: "embed",
  container: "#checkout-container",   // your DOM target
  onReady: () => /* iframe loaded */ {},
});
```

## 4. Handle the webhook (server)

The webhook is the **only** authoritative signal that money moved.
Browser callbacks (`onSuccess`) can fire without a real payment under
some testnet timings — never fulfil from a browser-side event.

```ts filename="server/webhook.ts (Next.js App Router)"
import { createHmac, timingSafeEqual } from "node:crypto";

const WEBHOOK_SECRET = process.env.INFRAIO_WEBHOOK_SECRET!; // whsec_…

export async function POST(req: Request) {
  const raw = await req.text();                    // RAW bytes, not parsed
  const signature = req.headers.get("x-signature") ?? "";
  const timestamp = req.headers.get("x-timestamp") ?? "";

  // The signature is over `${timestamp}.${raw}` — NOT the body alone.
  const expected = "sha256=" + createHmac("sha256", WEBHOOK_SECRET)
    .update(`${timestamp}.${raw}`)
    .digest("hex");

  const a = Buffer.from(signature);
  const b = Buffer.from(expected);
  if (a.length !== b.length || !timingSafeEqual(a, b)) {
    return new Response("bad signature", { status: 400 });
  }

  // The HTTP body IS the per-event data object (no Stripe-style outer
  // envelope). The event type + delivery ID + timestamp live in headers.
  const eventType = req.headers.get("x-event") ?? "";        // e.g. "payment.settled"
  const deliveryId = req.headers.get("idempotency-key")      // mirrors X-Delivery
                   ?? req.headers.get("x-delivery") ?? "";

  const payload = JSON.parse(raw);
  switch (eventType) {
    case "payment.settled": {
      // The payload carries order_id, payment_intent_id, tx_hash,
      // amount_received, confirmations, settlement token + network, etc.
      // It does NOT include your `external_ref` from the order — look it
      // up server-side via order_id if you need to map back.
      const { order_id, tx_hash, amount_received, confirmations } = payload;

      // Idempotently mark order paid. Use deliveryId (X-Delivery) as the
      // dedup key — it's stable across all retries of the same delivery.
      // We retry up to 6 times (1m / 5m / 15m / 1h / 6h) on non-2xx.
      break;
    }
    case "checkout.expired": { /* session TTL elapsed — free inventory */ break; }
    case "payment.refund.requested": { /* surface in admin queue */ break; }
    default:
      // Forward-compat: we may add events. Accept and no-op.
  }
  return new Response("ok", { status: 200 });
}
```

See [Webhooks → Signature verification](https://docs.infraio.xyz/en/webhooks/signature-verification)
for edge cases (body normalisation, secret rotation, replay protection).

## 5. Trigger a test payment

Test-mode sessions settle against **real testnets** (Sepolia, Base
Sepolia, BSC Testnet, etc.) — there's no mock chain. Get test funds
from the relevant faucet, then send to the deposit address the checkout
page shows. The webhook fires once confirmations clear (1 conf on most
testnets).

On TRON Nile, Solana Devnet and TON Testnet (coming soon) there is no deposit address: the buyer pays from a wallet through the checkout page. See [Direct-to-wallet networks](https://docs.infraio.xyz/en/concepts/chains#direct-to-wallet-networks).

## What's next

- [Concepts → Sessions](https://docs.infraio.xyz/en/concepts/sessions) — the three-level data
  model (CheckoutSession + Order + PaymentIntent).
- [Concepts → Chains & assets](https://docs.infraio.xyz/en/concepts/chains) — which networks
  and tokens are supported live vs test.
- [Webhooks → Overview](https://docs.infraio.xyz/en/webhooks/overview) — every event type and
  what triggers it.
- [Security → API keys](https://docs.infraio.xyz/en/security/api-keys) — scopes, rotation,
  rate limits.
