Skip to Content
Get startedQuickstart

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.

You need a test API key pair (pk_test_… + sk_test_…) and a webhook signing secret (whsec_…). Generate them in the merchant dashboard  under Developers → API keys and Developers → Webhooks.

1. Install the browser SDK

npm install @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

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: "[email protected]", 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.

Clock skew matters. The gateway 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)

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

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.

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

What’s next