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.
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
bajo Developers → API keys y Developers → Webhooks.
1. Instala el SDK de navegador
npm
npm install @lartech/infraio-checkout-js2. 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" + BODYPara 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
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", // 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.La desviación de reloj importa. El gateway 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
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),
});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.
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 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 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).
Qué sigue
- Conceptos → Sesiones: el modelo de datos de tres niveles (CheckoutSession + Order + PaymentIntent).
- Conceptos → Cadenas y activos: qué redes y tokens están soportados live vs test.
- Webhooks → Resumen: cada tipo de evento y qué lo dispara.
- Seguridad → Claves de API: scopes, rotación, límites de tasa.