Tamino Docs

Webhooks

Notificações assíncronas de eventos — a única forma confiável de saber que um PIX/boleto foi pago (não faça polling em produção).

Configurar

Backoffice → Webhooks → Novo endpoint (informe URL + um secret, usado pra assinar cada entrega) → inscreva-se nos tipos de evento que te interessam. Não há um endpoint público em /v1 pra isso nesta entrega — é uma operação de configuração, feita uma vez, via Backoffice/API admin.

Tipos de evento

Só os eventos abaixo são realmente disparados hoje — cada um foi conferido direto no código (não é uma lista de aspiração). customer.created, merchant.created, payment.authorized, payment.processing e payment.cancelled existem como tipo reservado no sistema, mas nenhum lugar do domínio ainda os dispara — não assine, você nunca vai receber nada.

EventoQuando dispara
checkout.startedCliente abriu o checkout.
checkout.completedCheckout terminou (pago ou falhou).
payment.createdPagamento submetido (cliente escolheu método e confirmou).
payment.paidAprovado — qualquer método. Dispara junto com pix.paid quando o método é PIX.
payment.failedRecusado ou erro.
payment.refundedEstornado — pela API (POST /v1/pix/charges/:id/refund, POST /v1/payments/:id/refund) ou pelo Backoffice.
pix.createdCobrança PIX emitida.
pix.paidPIX pago.
pix.expiredPIX expirou sem pagamento.
boleto.createdBoleto emitido.
boleto.paidBoleto compensado.
settlement.createdRepasse agendado.
settlement.paidRepasse processado.
pix.dispute_openedDisputa MED aberta contra um pagamento seu — ver PIX disputes (MED).
pix.dispute_resolvedDisputa MED resolvida (aceita ou recusada) por um admin ou pelo provider.
payout.createdSaque PIX solicitado (inclusive quando fica pendente de aprovação).
payout.paidSaque confirmado — na hora (provider síncrono) ou depois, via webhook do provider (provider assíncrono).
payout.failedSaque falhou — ou foi devolvido pelo banco do recebedor depois de pago (ver exemplos abaixo).

Um saque que fica processing (provider assíncrono que ainda não confirmou) não dispara payout.paid nem payout.failed — só quando o desfecho real chega. Um saque da própria plataforma (sem tenant dono) nunca dispara nenhum evento — não há pra quem entregar.

Payload

O corpo da entrega é o evento — sem wrapper. Todo evento carrega esse envelope comum:

CampoSempre presente?Conteúdo
idsimId único deste evento — use pra dedupe (ver "Duplicidade e ordem").
typesimEx.: pix.paid.
tenantIdsimTenant dono do evento.
merchantIdquando aplicávelAusente em eventos de nível tenant (ex. settlement.*).
occurredAtsimISO 8601, quando o evento aconteceu no Gateway.
correlationIdquando disponívelPra correlacionar com o request original que originou o evento.

O resto do payload (o que aparece além do envelope) varia por evento — abaixo, o formato real de cada família, direto do código que publica (nunca um formato "ideal" nunca implementado).

Em todos os eventos de cobrança — payment.*, pix.* (created/paid/expired), boleto.*, payment.refunded e os de MED (pix.dispute_opened/pix.dispute_resolved) — chega também o externalId: o id do pedido que você mandou ao criar a cobrança (POST /v1/pix/charges, POST /v1/payments ou POST /v1/boletos; null se não mandou). Use-o pra casar o evento com o pedido do seu lado sem precisar guardar o nosso id.

payment.* / pix.* / boleto.* confirmados via webhook do provider

Este é o caminho de PIX/boleto: o provider confirma o pagamento via webhook próprio, o Gateway processa e publica. Payload mais completo do sistema:

CampoConteúdo
checkoutSessionIdId da sessão de checkout.
externalIdId do pedido no seu sistema, como enviado na criação da cobrança (null se não enviado).
statusStatus do pagamento no momento do evento (paid, expired, ...).
paymentMethodpix | boleto.
amountInCentsValor bruto da cobrança.
currencySempre BRL nesta entrega.
customerIdId do cliente, quando o checkout capturou um.
payerDocumentCPF/CNPJ que o checkout informou na criação da cobrança (pode ser diferente de quem pagou de fato — para isso, use payer).
payerSó em pix.paid/payment.paid de PIX: quem pagou de fato, como o banco/PSP informou — nome, CPF/CNPJ, ISPB, banco, agência, conta. Ver tabela abaixo.
pix{ txid, endToEndId, paidAt } — só em pix.paid/pix.expired. endToEndId/paidAt só existem depois do pagamento confirmado.
boleto{ ourNumber } — só em boleto.paid.
{
  "id": "8f0e1c2a-...",
  "type": "pix.paid",
  "tenantId": "961c52b2-...",
  "merchantId": "0d303c3b-...",
  "occurredAt": "2026-09-04T22:01:06.707Z",
  "checkoutSessionId": "cf810b10-...",
  "externalId": "pedido-12345",
  "status": "paid",
  "paymentMethod": "pix",
  "amountInCents": 5000,
  "currency": "BRL",
  "customerId": "cust_abc123",
  "payerDocument": "12345678909",
  "payer": {
    "name": "Maria da Silva",
    "document": "12345678909",
    "documentType": "cpf",
    "ispb": "00360305",
    "bankName": "Caixa Econômica Federal",
    "branch": "0001",
    "account": "12345-6",
    "accountType": "CACC"
  },
  "pix": {
    "txid": "1500700188",
    "endToEndId": "E58160789202609042129cgTvo93yroq",
    "paidAt": "2026-09-04T22:01:05.000Z"
  }
}

Objeto payer (quem pagou o PIX)

Os dados vêm do banco/PSP de quem pagou, repassados como o provider informou. Cada campo só aparece quando o provider o envia — nunca é inventado nem completado com o que o checkout pediu. Nome, CPF/CNPJ e ISPB vêm em praticamente todo pagamento; agência e conta dependem do PSP.

CampoConteúdo
nameNome do titular da conta pagadora.
documentCPF ou CNPJ do pagador, só dígitos.
documentTypecpf | cnpj.
ispbISPB (8 dígitos) da instituição do pagador.
bankNameNome da instituição do pagador.
bankCodeCódigo COMPE (3 dígitos), quando informado.
branchAgência do pagador.
accountConta do pagador (com dígito, como o banco informou).
accountTypeTipo da conta como o banco informou (ex.: CACC corrente, SVGS poupança, TRAN conta de pagamento).

Os mesmos dados ficam gravados na cobrança: GET /v1/pix/charges/:id (e GET /v1/payments/:id) devolve metadata.pixPayer e metadata.pixPaidAt depois do pagamento.

Por privacidade, nenhum webhook informa qual provider processou a transação.

payment.paid chega junto com pix.paid/boleto.paid — mesmo envelope, dois eventos pra cada pagamento aprovado por PIX/boleto: assine só payment.paid se você não se importa com o método, ou o específico se precisa (ex. endToEndId só existe em pix.paid). pix.expired pode chegar com um payload mais enxuto (só checkoutSessionId/externalId/status/paymentMethod/ amountInCents/currency, sem customerId/payerDocument/payer/pix) quando a expiração é detectada por varredura de tempo em vez de um webhook do provider — nunca assuma que esses campos extras vêm preenchidos num pix.expired.

payment.paid / pix.paid confirmados de forma síncrona (cartão)

Cartão de crédito/débito confirma na hora, sem esperar um webhook do provider — o payload é diferente do formato acima, montado a partir do carrinho, não da sessão:

CampoConteúdo
checkoutSessionIdId da sessão de checkout.
transactionIdId da transação no provider de cartão.
valueInCentsValor pago (mesmo valor de amountInCents no outro formato, nome diferente por vir de outro código).
currencySempre BRL nesta entrega.
items[{ id, name, quantity, unitAmountInCents }] — os itens do carrinho, quando informados na criação do checkout.
paymentMethodcredit_card | debit_card | pix (ver nota abaixo).

Sim — os dois formatos de payment.paid/pix.paid existem de verdade (dois pontos diferentes do código publicam o mesmo tipo de evento, um pra confirmação síncrona, outro pra confirmação via webhook do provider). Se seu handler espera amountInCents, trate valueInCents como alternativa (ou vice-versa) em vez de assumir que só um formato chega.

payment.created / pix.created / boleto.created

Payload mínimo — só o suficiente pra saber que algo nasceu, sem repetir dados que GET /v1/payments/:id já devolve:

{ "checkoutSessionId": "cf810b10-...", "externalId": "pedido-12345" }

payment.created pode vir com metadata adicional (ex. method) quando disponível; pix.created/ boleto.created não trazem mais nada além do checkoutSessionId e do externalId.

payment.failed

Mesmo payload mínimo de payment.created ({ checkoutSessionId, externalId }, mais metadata quando disponível) — para o motivo específico da recusa, consulte GET /v1/payments/:id.

payment.refunded

{
  "id": "b7d2e4f1-9a3c-4e5b-8d6f-1a2b3c4d5e6f",
  "type": "payment.refunded",
  "tenantId": "961c52b2-...",
  "merchantId": "0d303c3b-...",
  "occurredAt": "2026-10-27T14:10:05.120Z",
  "paymentId": "cf810b10-...",
  "externalId": "pedido-12345"
}

Só o id do pagamento estornado (e o externalId da cobrança). O estorno é sempre total; quando e quanto ficam em metadata.refundedAt / metadata.refundedAmountInCents de GET /v1/pix/charges/:id (ou GET /v1/payments/:id). O status do pagamento continua paid.

checkout.started / checkout.completed

{ "checkoutSessionId": "cf810b10-..." }

checkout.completed dispara tanto pra um checkout que terminou pago quanto pra um que falhou — não use como sinônimo de "pago" (para isso, assine payment.paid).

settlement.created / settlement.paid

CampoConteúdo
settlementIdId do repasse.
totalAmountInCentsValor total do repasse.
{ "settlementId": "8b2f...", "totalAmountInCents": 150000 }

pix.dispute_opened

{
  "pixInfractionId": "d4a1...",
  "providerInfractionId": "med-9f21...",
  "endToEndId": "E58160789202609042129cgTvo93yroq",
  "infractionType": "REFUND_REQUEST",
  "infractionSituationType": null,
  "amountInCents": 5000,
  "checkoutSessionId": "cf810b10-...",
  "externalId": "pedido-12345"
}

O valor de amountInCents já foi bloqueado preventivamente na sua conta (ver PIX disputes (MED)) — este evento é seu aviso pra investigar antes de um admin responder a disputa.

pix.dispute_resolved

{
  "pixInfractionId": "d4a1...",
  "providerInfractionId": "med-9f21...",
  "endToEndId": "E58160789202609042129cgTvo93yroq",
  "outcome": "accepted",
  "amountInCents": 5000,
  "checkoutSessionId": "cf810b10-...",
  "externalId": "pedido-12345"
}

Como em pix.dispute_opened, vêm também o checkoutSessionId e o externalId da cobrança contestada.

outcome: "accepted" — a disputa foi aceita, o valor bloqueado saiu de vez pra conta do provider (nunca volta). "rejected" — a disputa foi recusada, o valor foi liberado de volta pro seu saldo.

payout.created / payout.paid / payout.failed

O payload é o mesmo formato de GET /v1/payouts/:id (ver Payouts — todos os campos explicados lá), mais payoutId e o envelope do evento. Atenção: no webhook, id é o id do evento (o envelope); o id do saque vem em payoutId.

Em payout.paid chegam também o endToEndId (comprovante do Pix no Bacen) e o objeto recipient — o titular da conta que recebeu, como o banco informou: nome, CPF/CNPJ (document, só dígitos, e documentType), ISPB, nome do banco e, quando o banco informa, agência, conta e tipo de conta. Cada campo de recipient só aparece quando o banco do recebedor o envia. failureReason vem em payout.failed.

Em todos os eventos de saque chega também o externalId — o id do pedido que você mandou no POST /v1/payouts (null se não mandou). Use-o pra casar o evento com o pedido do seu lado sem precisar guardar o nosso id.

{
  "id": "3f7a2c91-5b0e-4d8a-a1f3-6c2e9b7d4a10",
  "type": "payout.paid",
  "tenantId": "961c52b2-8fb7-48f4-9717-4d842412ab19",
  "merchantId": "0d303c3b-cb6d-417e-8aa7-2a5cc8fde653",
  "occurredAt": "2026-10-26T21:02:48.700Z",
  "payoutId": "8f1c2a4e-3b7d-4c1a-9e2f-5d6a7b8c9d0e",
  "object": "payout",
  "externalId": "saque-2026-000123",
  "status": "paid",
  "amountInCents": 51675,
  "feeAmountInCents": 517,
  "netAmountInCents": 51158,
  "feeMode": "deducted",
  "currency": "BRL",
  "pixKey": "11222333000181",
  "pixKeyType": "cnpj",
  "recipientDocument": "11222333000181",
  "endToEndId": "E60701190202610262102abcdef123456",
  "recipient": {
    "name": "Fornecedor Exemplo LTDA",
    "document": "11222333000181",
    "documentType": "cnpj",
    "ispb": "60701190",
    "bankName": "Itaú Unibanco"
  },
  "failureReason": null,
  "approvedAt": null,
  "createdAt": "2026-10-26T21:02:42.926Z",
  "updatedAt": "2026-10-26T21:02:48.610Z",
  "processedAt": "2026-10-26T21:02:48.610Z"
}

payout.failed — saque que falhou

O saque não foi efetivado: status: "failed", motivo em failureReason, e o valor já voltou pro seu saldo. endToEndId e recipient vêm null — o dinheiro não chegou a sair.

{
  "id": "5c9e1a7b-2d4f-4b8e-9a1c-3e5f7a9b1d2c",
  "type": "payout.failed",
  "tenantId": "961c52b2-8fb7-48f4-9717-4d842412ab19",
  "merchantId": "0d303c3b-cb6d-417e-8aa7-2a5cc8fde653",
  "occurredAt": "2026-10-27T16:05:21.340Z",
  "payoutId": "8f1c2a4e-3b7d-4c1a-9e2f-5d6a7b8c9d0e",
  "object": "payout",
  "externalId": "saque-2026-000123",
  "status": "failed",
  "amountInCents": 51675,
  "feeAmountInCents": 517,
  "netAmountInCents": 51158,
  "feeMode": "deducted",
  "currency": "BRL",
  "pixKey": "11222333000181",
  "pixKeyType": "cnpj",
  "recipientDocument": "11222333000181",
  "endToEndId": null,
  "recipient": null,
  "failureReason": "Pagamento rejeitado pelo PSP do recebedor",
  "approvedAt": null,
  "createdAt": "2026-10-27T16:05:18.002Z",
  "updatedAt": "2026-10-27T16:05:21.310Z",
  "processedAt": null
}

payout.failed — saque devolvido depois de pago

Quando o banco do recebedor devolve um saque que já tinha sido pago (ex.: Pix duplicado, conta encerrada), você recebe primeiro o payout.paid e, depois, um payout.failed com status: "cancelled". O valor devolvido volta pro seu saldo e failureReason explica o motivo; endToEndId e recipient continuam os do envio original. Não existe um evento separado de devolução — é sempre payout.failed, e o status diz qual dos dois casos é.

{
  "id": "9a3b5c7d-1e2f-4a6b-8c0d-2e4f6a8b0c1d",
  "type": "payout.failed",
  "tenantId": "961c52b2-8fb7-48f4-9717-4d842412ab19",
  "merchantId": "0d303c3b-cb6d-417e-8aa7-2a5cc8fde653",
  "occurredAt": "2026-10-27T18:40:02.915Z",
  "payoutId": "8f1c2a4e-3b7d-4c1a-9e2f-5d6a7b8c9d0e",
  "object": "payout",
  "externalId": "saque-2026-000123",
  "status": "cancelled",
  "amountInCents": 51675,
  "feeAmountInCents": 517,
  "netAmountInCents": 51158,
  "feeMode": "deducted",
  "currency": "BRL",
  "pixKey": "11222333000181",
  "pixKeyType": "cnpj",
  "recipientDocument": "11222333000181",
  "endToEndId": "E60701190202610271605abcdef123456",
  "recipient": {
    "name": "Fornecedor Exemplo LTDA",
    "document": "11222333000181",
    "documentType": "cnpj",
    "ispb": "60701190",
    "bankName": "Itaú Unibanco"
  },
  "failureReason": "Devolvido pelo banco do recebedor — Devolucao pix duplicado (51158 centavos creditados de volta).",
  "approvedAt": null,
  "createdAt": "2026-10-27T16:05:18.002Z",
  "updatedAt": "2026-10-27T18:40:02.880Z",
  "processedAt": "2026-10-27T16:05:21.310Z"
}

Headers de assinatura

Toda entrega chega com 4 headers:

HeaderConteúdo
X-Gateway-SignatureHMAC-SHA256 hex de ${timestamp}.${rawBody}
X-Gateway-TimestampUnix timestamp da assinatura
X-Gateway-Event-IdId do evento (mesmo de payload.id)
X-Gateway-Delivery-IdId desta tentativa específica de entrega

Verificar a assinatura

Sempre use o corpo cru da request (nunca req.body já parseado, que pode re-serializar o JSON e invalidar a assinatura):

TypeScript
import { verifySignature } from '@gateway/sdk';

app.post('/webhooks/gateway', (req, res) => {
  const ok = verifySignature({
    payload: req.rawBody,
    timestamp: req.headers['x-gateway-timestamp'],
    signature: req.headers['x-gateway-signature'],
    secret: process.env.GATEWAY_WEBHOOK_SECRET!,
  });
  if (!ok) return res.status(401).end();

  const event = JSON.parse(req.rawBody);
  // ...
  res.status(200).end();
});
PHP
$ok = $webhooks->verifySignature(
    file_get_contents('php://input'),
    $_SERVER['HTTP_X_GATEWAY_TIMESTAMP'],
    $_SERVER['HTTP_X_GATEWAY_SIGNATURE'],
    getenv('GATEWAY_WEBHOOK_SECRET'),
);

Sem SDK

O texto assinado é o timestamp, um ponto e o corpo cru: ${timestamp}.${rawBody}. Exemplo: com X-Gateway-Timestamp: 1730000000 e o corpo {"id":"evt_1"}, o texto é exatamente 1730000000.{"id":"evt_1"}. Calcule o HMAC-SHA256 dele com o secret do endpoint, em hexadecimal, e compare com X-Gateway-Signature (em tempo constante):

TypeScript
import { createHmac, timingSafeEqual } from 'node:crypto';

const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
const ok = expected.length === signature.length && timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
PHP
$expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
$ok = hash_equals($expected, $signature);

Duplicidade e ordem

O mesmo evento pode chegar mais de uma vez (retry de rede do nosso lado, ou reenvio) — sempre trate pelo id do evento (idempotente do seu lado: já processou esse id? ignore). Eventos também não chegam necessariamente na ordem em que aconteceram — nunca assuma que payment.created chega antes de payment.paid; trate cada evento como um snapshot de status, não como um passo sequencial.

Retry e dead letter

Uma entrega que falhar (timeout, 4xx/5xx da sua URL) é tentada de novo com backoff crescente: 1min, 5min, 15min, 1h, 6h, 24h. Depois da última tentativa sem sucesso, a entrega vai pra dead letter — visível no Backoffice, com replay manual disponível (reprocessa sob demanda, sem esperar o schedule).

Responda rápido

Devolva 2xx assim que tiver validado a assinatura e enfileirado o processamento internamente — não faça o processamento pesado (chamadas a outros serviços, etc.) antes de responder; isso conta como timeout do lado do Gateway e aciona um retry desnecessário.