Início rápido
Objetivo: receber um pagamento real em modo de teste de ponta a ponta. No fim você vai ter uma sessão criada a partir do seu servidor, um checkout aberto no navegador do comprador e um webhook assinado entregue no seu localhost.
Você precisa de um par de chaves de teste (pk_test_… + sk_test_…) e
de um segredo de assinatura de webhook (whsec_…). Gere as duas coisas
no dashboard do lojista em Developers →
API keys e Developers → Webhooks.
1. Instale o SDK do navegador
npm
npm install @lartech/infraio-checkout-js2. Crie uma sessão de checkout (servidor)
As sessões são criadas a partir do seu servidor, assinadas com HMAC-SHA256
usando a sua chave secreta. Nunca coloque sk_ no navegador.
Monte a string canônica de assinatura
METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + BODYPara uma requisição POST /b2b/v1/checkout-sessions/quick no
unix-timestamp 1715990400 com body {...}, a entrada de assinatura é:
POST
/b2b/v1/checkout-sessions/quick
1715990400
{"items":[...],"currency":"USD","success_url":"..."}Assine com HMAC-SHA256(secret_key, signingString), com saída em hex.
Envie a requisição
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", // o ID do seu pedido; armazenado no order, não ecoado nos webhooks — busque via order_id
expires_in: 1800, // segundos; padrão de 30 min
idempotency_key: crypto.randomUUID(), // seguro para 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,
});
// Toda resposta da API vem envelopada — o payload real fica em `data`.
// Trate `code`/`message` no nível de cima como o marcador de resultado
// (`201` / `"created"` para sucesso).
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;
// Envie session_key + checkout_url para o navegador. order_id e expires_at
// são para o seu controle interno.Defasagem de relógio importa. O gateway rejeita requisições com um
X-Timestamp mais de 5 minutos distante do horário do servidor.
Sincronize seus servidores via NTP; não confie no relógio de parede
de um cron rodando há tempos.
3. Abra o 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", // padrão
onReady: () => console.log("checkout interativo"),
onSuccess: ({ sessionId }) => {
// Só para UX — veja o aviso abaixo. O webhook é a fonte da verdade.
location.assign("/thanks");
},
onCancel: () => console.log("comprador fechou o popup"),
onError: (err) => console.error(err.code, err.message),
});4. Trate o webhook (servidor)
O webhook é o único sinal oficial de que o dinheiro se moveu.
Callbacks no navegador (onSuccess) podem disparar sem um pagamento
real sob certas condições de timing em testnet — nunca cumpra a venda
a partir de um evento do 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 BRUTOS, não parseados
const signature = req.headers.get("x-signature") ?? "";
const timestamp = req.headers.get("x-timestamp") ?? "";
// A assinatura é sobre `${timestamp}.${raw}` — NÃO só sobre o body.
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 });
}
// O corpo HTTP É o objeto de dados do evento (sem envelope externo
// estilo Stripe). O tipo do evento + ID da entrega + timestamp ficam
// nos headers.
const eventType = req.headers.get("x-event") ?? ""; // ex.: "payment.settled"
const deliveryId = req.headers.get("idempotency-key") // espelha X-Delivery
?? req.headers.get("x-delivery") ?? "";
const payload = JSON.parse(raw);
switch (eventType) {
case "payment.settled": {
// O payload traz order_id, payment_intent_id, tx_hash,
// amount_received, confirmations, token de liquidação + rede, etc.
// NÃO inclui o seu `external_ref` do order — busque do lado do
// servidor via order_id se precisar fazer o mapeamento de volta.
const { order_id, tx_hash, amount_received, confirmations } = payload;
// Marque o pedido como pago de forma idempotente. Use deliveryId
// (X-Delivery) como chave de dedup — é estável em todas as
// retentativas da mesma entrega. A gente faz até 6 retentativas
// (1m / 5m / 15m / 1h / 6h) em respostas que não forem 2xx.
break;
}
case "checkout.expired": { /* TTL da sessão venceu — libere estoque */ break; }
case "payment.refund.requested": { /* surface na fila de admin */ break; }
default:
// Compatibilidade futura: podemos adicionar eventos. Aceite e no-op.
}
return new Response("ok", { status: 200 });
}Veja Webhooks → Verificação de assinatura para casos extremos (normalização do body, rotação de segredo, proteção contra replay).
5. Acione um pagamento de teste
Sessões em modo de teste liquidam contra testnets reais (Sepolia, Base Sepolia, BSC Testnet, etc.) — não existe rede fictícia. Pegue fundos de teste na torneira (faucet) que corresponde e envie para o endereço de depósito que a página de checkout mostra. O webhook dispara quando as confirmações forem atingidas (1 conf na maioria das testnets).
Próximos passos
- Conceitos → Sessões — o modelo de dados em três níveis (CheckoutSession + Order + PaymentIntent).
- Conceitos → Redes e ativos — quais redes e tokens são suportados em live vs. teste.
- Webhooks → Visão geral — cada tipo de evento e o que dispara.
- Segurança → Chaves de API — escopos, rotação, rate limits.