Webhooks

Webhook é um aviso automático. Em vez de o seu sistema ficar perguntando "já pagou?", a SwitchPay chama um endereço seu (uma URL) sempre que algo acontece com uma transação: foi criada, foi paga, foi cancelada.

É o jeito certo de saber que um Pix ou um boleto foi pago: o comprador paga quando quiser, e o aviso chega na hora.

Como funciona

  1. Você cadastra uma URL do seu sistema e escolhe quais eventos quer receber.
  2. Quando o evento acontece, a SwitchPay faz um POST nessa URL, com os dados da transação em JSON.
  3. O seu sistema confere a assinatura, responde 2xx e segue a vida (libera o pedido, avisa o cliente, etc.).

Eventos

EventoQuando chega
transaction.createdA transação foi criada (você confirmou a intenção).
transaction.paidO pagamento foi confirmado.
transaction.canceledA transação foi cancelada, recusada, expirou ou teve chargeback.

Cadastrando a URL

POST /v2/webhooks

{
  "url": "https://integracao.parceiro.com.br/switchpay/webhook",
  "events": ["transaction.created", "transaction.paid", "transaction.canceled"]
}

A resposta traz o id do cadastro (whk_…) e o secret (whsec_…), que você usa pra conferir a assinatura dos avisos.

O secret aparece só nessa resposta. Guarde na hora, em lugar seguro. Se perder, apague o cadastro e faça outro.

Regras da URL:

  • Tem que ser https://.
  • Tem que ser um endereço público. localhost e endereços de rede interna são recusados (422 WEBHOOK_URL_INVALID).
  • Até 10 URLs ativas por empresa.

Pra ver as URLs cadastradas: GET /v2/webhooks. Pra apagar uma: DELETE /v2/webhooks/{id}.

O que chega no seu endereço

O corpo do aviso:

{
  "id": "evt_5b6c7d8e-9f0a-4b1c-8d2e-3f4a5b6c7d8e",
  "type": "transaction.paid",
  "createdAt": "2026-10-06T14:05:00.000Z",
  "data": {
    "transaction": { "id": "trx_6f1a2b3c-4d5e-4f60-8a7b-9c0d1e2f3a4b", "status": "PAID", "amountCents": 15000 }
  }
}

data.transaction vem completa, no mesmo formato de GET /v2/transactions/{id} (o exemplo acima está resumido). Você não precisa consultar a API de novo pra saber o que mudou.

Junto vêm estes cabeçalhos:

CabeçalhoO que traz
X-Swp-Event-IdId único do evento. Use pra descartar aviso repetido.
X-Swp-EventTipo do evento (ex.: transaction.paid).
X-Swp-TimestampMomento do envio, em segundos (Unix time).
X-Swp-SignatureA assinatura: v1= seguido de um texto em hexadecimal.

Conferindo a assinatura

Qualquer um que descubra a sua URL pode mandar um POST pra ela. A assinatura é o que prova que o aviso veio da SwitchPay e não foi alterado no caminho. Não processe aviso sem conferir.

  1. Monte o texto "{X-Swp-Timestamp}.{corpo}", usando o corpo exatamente como chegou (os bytes crus, antes de transformar em objeto).
  2. Calcule o HMAC-SHA256 desse texto com o seu secret. Resultado em hexadecimal minúsculo.
  3. Compare com o que vem depois de v1= no cabeçalho, usando comparação em tempo constante.
  4. Recuse se o X-Swp-Timestamp tiver mais de 5 minutos.

Exemplo em Node.js:

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

function isFromSwitchPay(rawBody, headers, secret) {
  const timestamp = headers['x-swp-timestamp'];
  const received = String(headers['x-swp-signature'] ?? '').replace(/^v1=/, '');
  const expected = createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');

  const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) <= 300;
  const same =
    received.length === expected.length && timingSafeEqual(Buffer.from(received), Buffer.from(expected));

  return fresh && same;
}

O erro mais comum: calcular a assinatura sobre o JSON já convertido e escrito de novo. Qualquer espaço ou ordem de campo diferente muda o resultado. Use sempre o corpo cru.

Respondendo e repetições

  • Responda com qualquer código 2xx em até 10 segundos. Se o seu processamento demora, guarde o aviso, responda logo e processe depois.
  • Sem 2xx, a SwitchPay tenta de novo com espera crescente: 1 min, 5 min, 30 min, 2 h e 12 h (5 tentativas em cerca de 24 h).
  • O mesmo evento pode chegar mais de uma vez. Guarde o X-Swp-Event-Id dos avisos já tratados e ignore o que repetir.
  • Não conte com a ordem de chegada. Se precisar ter certeza do estado atual, consulte GET /v2/transactions/{id}.

Do aviso até a tela do cliente

O aviso chega no seu servidor, não na tela de quem está pagando. Pra a tela mostrar "pagamento aprovado" num Pix ou boleto, falta um caminho entre os dois. O desenho mais simples:

Tela do cliente             Seu servidor                    SwitchPay
      |                          |                              |
      | 1. pagar com Pix         |                              |
      |------------------------->| 2. cria a cobrança           |
      |                          |----------------------------->|
      | 3. QR Code               |    guarda: pedido PENDENTE   |
      |<-------------------------|                              |
      |                          |                              |
      |        (o cliente paga no aplicativo do banco)          |
      |                          |                              |
      |                          | 4. aviso transaction.paid    |
      |                          |<-----------------------------|
      |                          |    confere, guarda: PAGO     |
      | 5. "já pagou?"           |    responde 2xx              |
      |------------------------->|                              |
      | 6. PAGO                  |                              |
      |<-------------------------|                              |
  1. Ao criar a cobrança, guarde no seu pedido o transactionId e a situação (pendente).
  2. Quando o aviso chegar, confira a assinatura, ache o pedido pelo data.transaction.id (ou pelo referenceId), guarde a nova situação e responda 2xx.
  3. A tela pergunta ao seu servidor como está o pedido, de tempos em tempos, enquanto o QR Code está aberto. Se você já usa um canal aberto com a tela (SSE ou WebSocket), o servidor pode avisar por ele em vez de esperar a pergunta.
  4. Pare de perguntar quando a situação for final (pago ou cancelado) ou quando o QR Code vencer (paymentDetails.pix.expiresAt).

No servidor:

// Recebe o aviso da SwitchPay. req.rawBody é o corpo cru, guardado antes de virar objeto.
app.post('/switchpay/webhook', async (req, res) => {
  if (!isFromSwitchPay(req.rawBody, req.headers, process.env.SWP_WEBHOOK_SECRET)) {
    return res.sendStatus(401);
  }
  if (await alreadyHandled(req.headers['x-swp-event-id'])) {
    return res.sendStatus(200); // aviso repetido: já tratado
  }

  const { transaction } = req.body.data;
  await saveOrderStatus(transaction.id, transaction.status); // PAID, CANCELED…

  res.sendStatus(200);
});

// A tela pergunta aqui. Responde o que está guardado; não chama a SwitchPay.
app.get('/orders/:id/status', async (req, res) => {
  res.json({ status: await getOrderStatus(req.params.id) });
});

Na tela:

const timer = setInterval(async () => {
  const response = await fetch(`/orders/${orderId}/status`);
  const { status } = await response.json();

  if (status === 'PENDING') return; // ainda esperando

  clearInterval(timer);
  if (status === 'PAID') showApproved();
  else showCanceled();
}, 3000);

O que saber:

  • A tela pergunta ao seu servidor, nunca à SwitchPay. A chave de API não pode ir pra tela, e essas perguntas não gastam o seu limite de chamadas.
  • Só libere o pedido pelo que o seu servidor guardou, nunca pelo que a tela diz.
  • Rede de segurança: se o aviso demorar ou se perder, o seu servidor consulta GET /v2/transactions/{id} uma vez (por exemplo, quando o cliente volta à página ou perto do vencimento do QR Code) e acerta o pedido.
  • No cartão a tela não precisa esperar: o confirm já devolve o resultado (ver Cartão: cofre e cifra).

No sandbox

Os avisos são enviados normalmente no sandbox, inclusive quando você simula o pagamento com POST /v2/sandbox/transactions/{id}/pay. Por enquanto, lá é uma tentativa só, sem repetição.

Veja a tag Webhooks pra referência das rotas e o formato completo do aviso.