Webhooks
Receba cada mudança de status de um pagamento no seu servidor.
Quando um pagamento muda de status, a XPayZ faz um POST JSON no endpoint que você cadastrou.
É o jeito confiável de saber que um pedido foi pago: não dependa do retorno do pagador para a
back_url, ele pode fechar a aba antes.
Cadastre o endpoint
curl -X POST "$XPAYZ_URL/webhook" \
-H "Content-Type: application/json" \
-H "X-Secret-ID: $XPAYZ_SECRET_ID" \
-H "X-Secret-Key: $XPAYZ_SECRET_KEY" \
-d '{
"endpoint": "https://sualoja.com/webhooks/xpayz",
"topics": ["event.completed", "event.rejected", "event.expired", "event.refunded"]
}'Você só recebe os tópicos assinados. Para mudar os tópicos, exclua o webhook e cadastre de novo.
Tópicos
| Tópico | Quando |
|---|---|
event.created | Um pagamento foi iniciado (ex. QR Code Pix gerado). |
event.completed | O pagamento foi concluído. Libere o pedido aqui. |
event.rejected | O pagamento foi recusado. |
event.cancelled | O pagamento foi cancelado. |
event.expired | O pagamento expirou sem ser concluído. |
event.refunded | O estorno foi concluído. |
event.refund_rejected | A solicitação de estorno foi recusada. |
Payload
Todos os tópicos têm o mesmo formato:
{
"topic": "event.completed",
"data": {
"external_id": "0d4ba8f5-bbc6-4cbb-ae65-13de2f903271",
"amount": "100",
"currency": "BRL",
"status": "done",
"method": "credit_card",
"payer": {
"name": "John Doe",
"email": "john@example.com",
"document": "***.123.456-**"
},
"link": {
"external_id": "f9ce38d3-d6e2-4d3e-b9f3-0ebb396ef780",
"external_ref": "order-123"
}
}
}Prop
Tipo
Valide a assinatura
Cada entrega tem o header X-Signature: o HMAC-SHA256 do corpo bruto, em hexadecimal, com o
secret do webhook como chave. Pegue o secret em GET /webhook.
Validar garante que o evento veio da XPayZ e não foi alterado no caminho. Sem isso, qualquer pessoa
que descubra sua URL pode forjar um event.completed.
- Leia o corpo exatamente como chegou, antes de fazer parse do JSON.
- Calcule o HMAC-SHA256 dele com o
secret. - Compare com
X-Signatureusando comparação de tempo constante. - Se não bater, responda
401e descarte.
Não faça JSON.parse e JSON.stringify antes de validar: qualquer diferença de espaço ou ordem
de campos muda o hash.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const secret = process.env.XPAYZ_WEBHOOK_SECRET!;
app.post('/webhooks/xpayz', express.raw({ type: 'application/json' }), (req, res) => {
const expected = crypto.createHmac('sha256', secret).update(req.body).digest('hex');
const received = req.header('X-Signature') ?? '';
const valid =
received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
if (!valid) return res.sendStatus(401);
const event = JSON.parse(req.body.toString('utf8'));
// enfileire o processamento e responda rápido
res.sendStatus(200);
});Responda rápido e com 2xx
Qualquer resposta 2xx confirma a entrega. Qualquer outra coisa (4xx, 5xx, erro de conexão ou
mais de 30 segundos sem resposta) conta como falha. Processe o evento em segundo plano e
responda logo.
Reenvios
Uma entrega que falha é tentada de novo até 5 vezes, com intervalo crescente:
| Tentativa | Quando |
|---|---|
| 1ª | Na hora do evento |
| 2ª | 1 min depois da falha |
| 3ª | 2 min depois |
| 4ª | 3 min depois |
| 5ª | 4 min depois |
| 6ª | 5 min depois |
Se a última falhar, o evento fica failed. Acompanhe as entregas em
GET /webhook/{id}/events e reenvie um evento com
POST .../retrigger depois de corrigir seu endpoint.
Trate duplicados
Por causa dos reenvios, o mesmo evento pode chegar mais de uma vez. Use data.external_id junto com
topic como chave e ignore o que você já processou.
Webhook por link
Por padrão, todos os webhooks da conta recebem os eventos de todos os links. Para mandar os eventos
de um link para um só deles, passe webhook_url na criação do link com exatamente o mesmo
endpoint de um webhook cadastrado.
webhook_url filtra, não cadastra. Se nenhum webhook da conta tiver esse endpoint, os eventos
do link não são entregues a ninguém.