Webhooks

Em vez de consultar a API em loop, receba uma requisição HTTP quando algo acontece.

Configurar

Cadastre a URL de destino no painel, em Webhooks. Ela precisa usar HTTPS e responder 2xx em até 15 segundos.

Ao cadastrar, você recebe um segredo de assinatura. Guarde-o como variável de ambiente: é com ele que você comprova que a requisição veio de nós.

Formato do evento

POST para a sua URL
{
  "id": "evt_01j9x5t2q4n7s9v6x8z1b3d5",
  "type": "charge.paid",
  "created_at": "2026-07-25T14:32:11Z",
  "data": {
    "id": "chg_01j9x2m4k8p3q7r5t1v6w8y0",
    "status": "paid",
    "amount": 14990,
    "fee_amount": 373,
    "net_amount": 14617,
    "reference_id": "1042",
    "paid_at": "2026-07-25T14:32:09Z"
  }
}

O campo data contém o recurso no estado em que ele estava quando o evento foi gerado.

Cabeçalhos
SlimPay-Signature: t=1784995931,v1=5f2b8c1e9a...
SlimPay-Event-Id: evt_01j9x5t2q4n7s9v6x8z1b3d5
SlimPay-Event-Type: charge.paid

Eventos disponíveis

EventoQuando é disparado
charge.paidO PIX foi confirmado e o saldo foi creditado.
charge.expiredA cobrança venceu sem pagamento.
charge.canceledVocê cancelou a cobrança.
payout.completedO saque caiu na conta de destino.
payout.failedO saque falhou e o valor voltou ao saldo.

Assine apenas os eventos que você usa. Novos tipos podem ser adicionados no futuro, então trate tipos desconhecidos com um 200 silencioso em vez de erro.

Verificar a assinatura

Este é o passo que não pode ser pulado. Sua URL é pública: sem verificação, qualquer pessoa pode enviar um charge.paid falso e liberar um pedido que ninguém pagou.

O cabeçalho SlimPay-Signature traz o timestamp t e o HMAC-SHA256 v1, calculado sobre a string {t}.{corpo bruto} usando o seu segredo.

TypeScript
import crypto from "node:crypto";

const TOLERANCE_SECONDS = 300;

export function verifySignature(
  rawBody: string,
  header: string,
  secret: string,
): boolean {
  const parts = new Map(
    header.split(",").map((part) => part.split("=") as [string, string]),
  );

  const timestamp = parts.get("t");
  const received = parts.get("v1");
  if (!timestamp || !received) return false;

  // Reject old signatures so a captured request cannot be replayed later.
  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!Number.isFinite(age) || age > TOLERANCE_SECONDS) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(received, "hex");
  if (a.length !== b.length) return false;

  return crypto.timingSafeEqual(a, b);
}

Três detalhes importam: use o corpo bruto, compare em tempo constante e rejeite timestamps antigos. Cada um cobre um ataque diferente.

Um handler completo

app/api/webhooks/slimpay/route.ts
export async function POST(request: Request) {
  // The signature covers the exact bytes we sent. Read the raw body first:
  // parsing and re-serializing the JSON changes it and breaks verification.
  const rawBody = await request.text();
  const signature = request.headers.get("slimpay-signature");

  if (!signature || !verifySignature(rawBody, signature, process.env.SLIMPAY_WEBHOOK_SECRET!)) {
    return new Response("invalid signature", { status: 401 });
  }

  const event = JSON.parse(rawBody);

  // Acknowledge before doing the slow work, so a timeout on our side does not
  // turn into a duplicate delivery on yours.
  await enqueue(event);

  return new Response(null, { status: 200 });
}

Trate eventos duplicados

O mesmo evento pode chegar mais de uma vez — é assim que garantimos entrega. Guarde os id já processados e ignore repetições, idealmente com uma constraint de unicidade no banco em vez de uma checagem em memória.

A ordem também não é garantida. Um charge.expired pode chegar depois de um charge.paid por atraso de rede: use o created_at do evento e o status atual do recurso para decidir, nunca a ordem de chegada.

Retentativas

Se a sua URL não responder 2xx, repetimos por até 24 horas com espera exponencial: 30s, 1min, 5min, 30min, 2h e 6h. Depois disso o evento é marcado como não entregue.

O painel lista todas as tentativas, com corpo enviado e resposta recebida, e permite reenviar qualquer evento manualmente. Falhas persistentes suspendem o endpoint e notificam você por e-mail.

Boas práticas

  • Responda rápido e processe em fila. Trabalho pesado dentro do handler gera timeout e entrega duplicada.
  • Trate o webhook como gatilho, não como fonte da verdade. Em operações críticas, confirme o valor consultando a cobrança.
  • Não confie no IP de origem como autenticação. A assinatura é a única garantia.
  • Registre os eventos recebidos. Quando algo divergir, esse log é o que vai explicar o porquê.