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
- Você cadastra uma URL do seu sistema e escolhe quais eventos quer receber.
- Quando o evento acontece, a SwitchPay faz um
POSTnessa URL, com os dados da transação em JSON. - O seu sistema confere a assinatura, responde
2xxe segue a vida (libera o pedido, avisa o cliente, etc.).
Eventos
| Evento | Quando chega |
|---|---|
transaction.created | A transação foi criada (você confirmou a intenção). |
transaction.paid | O pagamento foi confirmado. |
transaction.canceled | A 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.
localhoste 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çalho | O que traz |
|---|---|
X-Swp-Event-Id | Id único do evento. Use pra descartar aviso repetido. |
X-Swp-Event | Tipo do evento (ex.: transaction.paid). |
X-Swp-Timestamp | Momento do envio, em segundos (Unix time). |
X-Swp-Signature | A 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.
- Monte o texto
"{X-Swp-Timestamp}.{corpo}", usando o corpo exatamente como chegou (os bytes crus, antes de transformar em objeto). - Calcule o HMAC-SHA256 desse texto com o seu
secret. Resultado em hexadecimal minúsculo. - Compare com o que vem depois de
v1=no cabeçalho, usando comparação em tempo constante. - Recuse se o
X-Swp-Timestamptiver 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
2xxem 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-Iddos 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 | |
|<-------------------------| |- Ao criar a cobrança, guarde no seu pedido o
transactionIde a situação (pendente). - Quando o aviso chegar, confira a assinatura, ache o pedido pelo
data.transaction.id(ou peloreferenceId), guarde a nova situação e responda2xx. - 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.
- 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
confirmjá 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.