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

# Inicio rápido

Objetivo: tomar un pago real de modo de prueba de principio a fin. Al
final tendrás una sesión creada desde tu servidor, un checkout
abierto en el navegador del comprador, y un webhook firmado
entregado a tu localhost.

> **Note:**
>
> Necesitas un par de claves de API **de prueba** (`pk_test_…` +
> `sk_test_…`) y un **secreto de firma de webhook** (`whsec_…`).
> Genéralos en el [dashboard del comerciante](https://app.infraio.xyz)
> bajo **Developers → API keys** y **Developers → Webhooks**.

## 1. Instala el SDK de navegador

**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. Crea una sesión de checkout (servidor)

Las sesiones se crean desde tu servidor, firmadas con HMAC-SHA256
usando tu clave **secreta**. Nunca pongas `sk_` en el navegador.

### Construye la cadena canónica de firma

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

Para una petición `POST /b2b/v1/checkout-sessions/quick` en el
unix-timestamp `1715990400` con body `{...}`, el input de firma es:

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

Fírmalo con `HMAC-SHA256(secret_key, signingString)`, salida hex.

### Envía la petición

```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",        // tu order id; almacenado en la order, no replicado en webhooks — búscalo vía order_id
expires_in: 1800,                // segundos; por defecto 30 min
idempotency_key: crypto.randomUUID(), // seguro reintentar
});

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

// Cada respuesta de la API se envuelve en un envelope: el payload
// real está bajo `data`. Trata `code`/`message` en el nivel superior
// como el marcador de resultado (`201` / `"created"` para éxito).
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;
// Envía session_key + checkout_url al navegador. order_id y expires_at
// son para tu propia contabilidad.
```

> **Warning:**
>
> La desviación de reloj importa. La API rechaza peticiones con
> un `X-Timestamp` que se desvíe más de **5 minutos** del tiempo del
> servidor. Sincroniza tus servidores vía NTP; no confíes en el
> reloj de pared de un cron de larga duración.

## 3. Abre el checkout (navegador)

**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",            // por defecto
  onReady:   () => console.log("checkout interactive"),
  onSuccess: ({ sessionId }) => {
    // Solo UX — ver la advertencia abajo. El webhook es autoritativo.
    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",
});
// El navegador navega a checkout.infraio.xyz; el comprador regresa a
// tu `success_url` / `cancel_url` del payload de creación de sesión.
```

**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",   // tu objetivo DOM
  onReady: () => /* iframe cargado */ {},
});
```

## 4. Maneja el webhook (servidor)

El webhook es la **única** señal autoritativa de que el dinero se
movió. Los callbacks del navegador (`onSuccess`) pueden dispararse
sin un pago real bajo algunos timings de testnet: nunca cumplas
desde un evento del lado del navegador.

```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();                    // bytes CRUDOS, no parseados
  const signature = req.headers.get("x-signature") ?? "";
  const timestamp = req.headers.get("x-timestamp") ?? "";

  // La firma es sobre `${timestamp}.${raw}` — NO el body solo.
  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 });
  }

  // El body HTTP ES el objeto data por evento (sin envelope externo
  // estilo Stripe). El tipo de evento + delivery ID + timestamp viven en cabeceras.
  const eventType = req.headers.get("x-event") ?? "";        // p. ej. "payment.settled"
  const deliveryId = req.headers.get("idempotency-key")      // refleja X-Delivery
                   ?? req.headers.get("x-delivery") ?? "";

  const payload = JSON.parse(raw);
  switch (eventType) {
    case "payment.settled": {
      // El payload lleva order_id, payment_intent_id, tx_hash,
      // amount_received, confirmations, settlement token + network, etc.
      // NO incluye tu `external_ref` de la order — búscalo en el lado
      // del servidor vía order_id si necesitas mapear de vuelta.
      const { order_id, tx_hash, amount_received, confirmations } = payload;

      // Marca idempotentemente la order como pagada. Usa deliveryId
      // (X-Delivery) como clave de dedup: es estable a través de todos
      // los reintentos de la misma entrega. Reintentamos hasta 6 veces
      // (1m / 5m / 15m / 1h / 6h) en no-2xx.
      break;
    }
    case "checkout.expired": { /* TTL de sesión transcurrido — libera inventario */ break; }
    case "payment.refund.requested": { /* muestra en cola de admin */ break; }
    default:
      // Compatibilidad hacia adelante: podemos añadir eventos. Acepta y no-op.
  }
  return new Response("ok", { status: 200 });
}
```

Consulta [Webhooks → Verificación de firma](https://docs.infraio.xyz/es/webhooks/signature-verification)
para casos límite (normalización del body, rotación de secretos,
protección contra replay).

## 5. Dispara un pago de prueba

Las sesiones de modo de prueba liquidan contra **testnets reales**
(Sepolia, Base Sepolia, BSC Testnet, etc.): no hay cadena simulada.
Obtén fondos de prueba del faucet relevante, luego envía a la
dirección de depósito por pedido (CREATE2) que muestra la página de checkout. El webhook
se dispara una vez que las confirmaciones se completan (1 conf en
la mayoría de testnets).

En TRON Nile, Solana Devnet y TON Testnet (próximamente) no hay dirección de depósito: el comprador paga desde una wallet a través de la página de checkout. Ver [Redes de pago directo a la wallet](https://docs.infraio.xyz/es/concepts/chains#redes-de-pago-directo-a-la-wallet).

## Qué sigue

- [Conceptos → Sesiones](https://docs.infraio.xyz/es/concepts/sessions): el modelo de datos
  de tres niveles (CheckoutSession + Order + PaymentIntent).
- [Conceptos → Cadenas y activos](https://docs.infraio.xyz/es/concepts/chains): qué redes y
  tokens están soportados live vs test.
- [Webhooks → Resumen](https://docs.infraio.xyz/es/webhooks/overview): cada tipo de evento y
  qué lo dispara.
- [Seguridad → Claves de API](https://docs.infraio.xyz/es/security/api-keys): scopes,
  rotación, límites de tasa.
