Webhooks
Até 10 endpoints por empresa, eventos de entrada e saída, assinatura HMAC SHA-256 com timestamp, reenvio automático e envio de teste.
Configurar endpoints
Em Desenvolvedor, Webhooks, cadastre até 10 endpoints por empresa. Cada endpoint tem a própria URL, os eventos que recebe e o próprio tempo limite, então você pode separar, por exemplo, o sistema que libera pedidos do que concilia saques.
Ao criar um endpoint, o painel mostra o segredo whsec_ dele uma única vez. Cada endpoint tem um segredo diferente. Para trocar, use Rotacionar segredo: o novo segredo aparece uma vez e o anterior deixa de valer, então publique o novo no seu servidor logo em seguida.
Em produção, a URL precisa usar https e não pode apontar para IP privado, loopback ou endereço de metadata de nuvem.
Eventos
Cada endpoint escolhe os eventos de Entrada, o dinheiro que chega, e de Saída, o dinheiro que sai. Pelo menos um evento por endpoint.
| Evento | Grupo | Quando | data |
|---|---|---|---|
charge.paid | Entrada | O pagador pagou a cobrança. O valor líquido já está no saldo disponível. | Cobrança com status paid, feeCents, netCents e endToEndId. |
payout.paid | Saída | O saque chegou na chave PIX de destino. | Saque com status paid. |
payout.canceled | Saída | O saque foi recusado ou cancelado e o valor voltou ao saldo disponível. | Saque com status canceled e failureReason. |
payout.refunded | Saída | O banco de destino devolveu um saque já pago. O valor voltou ao saldo disponível. | Saque com status paid e refundedAmountCents. |
Novos tipos podem surgir dentro de /v1. Responda 2xx e ignore os tipos que você não trata.
Formato da entrega
Cada evento chega num POST com JSON. data traz o objeto completo, no mesmo formato da referência, no momento do evento. O cabeçalho X-MaxxiPay-Event-Id repete o id do corpo e é o mesmo em todas as tentativas; X-MaxxiPay-Attempt diz qual tentativa é esta.
| Campo | Descrição |
|---|---|
id | Identificador do evento, no formato tipo:id do objeto. É o mesmo em todas as tentativas e reenvios. |
type | Tipo do evento. |
createdAt | Quando o evento aconteceu, em ISO 8601 UTC. |
data | Cobrança ou saque completo. |
data em payout.refunded
refundedAmountCentsintegerValor devolvido ao seu saldo, em centavos.
POST /webhooks/maxxipay HTTP/1.1
Content-Type: application/json
X-MaxxiPay-Event-Id: charge.paid:6f0c1a52-1d7e-4b8a-9f43-2c5e8d9b7a10
X-MaxxiPay-Event-Type: charge.paid
X-MaxxiPay-Attempt: 1
X-MaxxiPay-Timestamp: 1793643221
X-MaxxiPay-Signature: sha256=5d2c9f0e8a41b7c3d6e2f1a09b8c7d6e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c{
"id": "charge.paid:6f0c1a52-1d7e-4b8a-9f43-2c5e8d9b7a10",
"type": "charge.paid",
"createdAt": "2026-11-02T18:13:41.000Z",
"data": {
"id": "6f0c1a52-1d7e-4b8a-9f43-2c5e8d9b7a10",
"object": "charge",
"status": "paid",
"amountCents": 4990,
"feeCents": 99,
"netCents": 4891,
"currency": "BRL",
"description": "Pedido 1042",
"payerName": "Cliente Exemplo",
"payerDocument": "52998224725",
"pixCopyPaste": "00020101021226870014br.gov.bcb.pix2565pix.exemplo.com.br/qr/v2/6f0c1a521d7e4b8a9f432c5e8d9b7a1052040000530398654044.905802BR6304B7C1",
"pixQrCode": "iVBORw0KGgoAAAANSUhEUgAA…",
"endToEndId": "E18236120202611021813s0a1b2c3d4e",
"externalId": "pedido-1042",
"metadata": {
"canal": "checkout"
},
"failureReason": null,
"expiresAt": "2026-11-02T19:12:00.000Z",
"paidAt": "2026-11-02T18:13:41.000Z",
"createdAt": "2026-11-02T18:12:00.000Z",
"updatedAt": "2026-11-02T18:13:41.000Z"
}
}Verificar a assinatura
O cabeçalho X-MaxxiPay-Timestamp traz o momento do envio em segundos Unix. O cabeçalho X-MaxxiPay-Signature traz sha256= seguido do HMAC SHA-256, em hexadecimal, de ${timestamp}.${corpo}: o timestamp, um ponto e o corpo bruto da requisição, calculado com o segredo whsec_. Cada tentativa tem timestamp e assinatura novos.
- Leia o corpo bruto, antes de qualquer parser de JSON. Reformatar o JSON muda a assinatura.
- Monte
timestamp + "." + corpo, calcule o HMAC com o segredo e monte o valor esperado. - Compare em tempo constante e rejeite com 401 se não bater.
- Rejeite timestamps com mais de 5 minutos de diferença do seu relógio. Isso impede que uma entrega capturada seja reenviada depois.
import crypto from "node:crypto";
import express from "express";
const app = express();
const secret = process.env.MAXXIPAY_WEBHOOK_SECRET;
function isValid(rawBody, timestamp, header) {
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!Number.isFinite(age) || age > 300) return false;
const signed = Buffer.concat([Buffer.from(timestamp + "."), rawBody]);
const expected = "sha256=" + crypto.createHmac("sha256", secret).update(signed).digest("hex");
const received = Buffer.from(header ?? "");
const wanted = Buffer.from(expected);
return received.length === wanted.length && crypto.timingSafeEqual(received, wanted);
}
app.post("/webhooks/maxxipay", express.raw({ type: "application/json" }), async (req, res) => {
if (!isValid(req.body, req.get("x-maxxipay-timestamp") ?? "", req.get("x-maxxipay-signature"))) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString("utf8"));
res.sendStatus(200);
await processEvent(event);
});Política de reenvio
Uma entrega conta como recebida quando o endpoint responde qualquer status 2xx dentro do tempo limite. Outros status, redirecionamentos, timeout e erro de rede geram nova tentativa. Webhooks não seguem redirecionamentos.
| Ajuste | Valores | Padrão |
|---|---|---|
timeoutSeconds | 2 a 10 segundos | 10 segundos |
Todo endpoint segue a mesma agenda: 3 tentativas rápidas e, se todas falharem, tentativas espaçadas, num total de 8. Cada intervalo conta a partir da tentativa anterior e varia até 10% para mais ou para menos.
| Tentativa | Quando | Fase |
|---|---|---|
| 1 | Na hora | Rápida |
| 2 | 10 segundos depois da anterior | Rápida |
| 3 | 1 minuto depois da anterior | Rápida |
| 4 | 5 minutos depois da anterior | Espaçada |
| 5 | 30 minutos depois da anterior | Espaçada |
| 6 | 2 horas depois da anterior | Espaçada |
| 7 | 6 horas depois da anterior | Espaçada |
| 8 | 24 horas depois da anterior | Espaçada |
Depois da última tentativa a entrega fica failed e o responsável pela conta recebe um aviso por e-mail. Corrija o endpoint e reenvie pelo painel ou pela API de eventos.
Enviar teste
Cada endpoint tem o botão Enviar teste. Ele dispara na hora um evento simulado do tipo escolhido, assinado com o segredo do endpoint e com os mesmos cabeçalhos X-MaxxiPay-Timestamp e X-MaxxiPay-Signature das entregas reais.
O corpo do teste traz "livemode": false. Use esse campo para não liberar pedidos nem mexer em estoque a partir de um teste.
O painel mostra o status HTTP que o seu servidor respondeu, o tempo da resposta e o corpo devolvido.
{
"id": "charge.paid:6f0c1a52-1d7e-4b8a-9f43-2c5e8d9b7a10",
"type": "charge.paid",
"createdAt": "2026-11-02T18:13:41.000Z",
"data": {
"id": "6f0c1a52-1d7e-4b8a-9f43-2c5e8d9b7a10",
"object": "charge",
"status": "paid",
"amountCents": 4990,
"feeCents": 99,
"netCents": 4891,
"currency": "BRL",
"description": "Pedido 1042",
"payerName": "Cliente Exemplo",
"payerDocument": "52998224725",
"pixCopyPaste": "00020101021226870014br.gov.bcb.pix2565pix.exemplo.com.br/qr/v2/6f0c1a521d7e4b8a9f432c5e8d9b7a1052040000530398654044.905802BR6304B7C1",
"pixQrCode": "iVBORw0KGgoAAAANSUhEUgAA…",
"endToEndId": "E18236120202611021813s0a1b2c3d4e",
"externalId": "pedido-1042",
"metadata": {
"canal": "checkout"
},
"failureReason": null,
"expiresAt": "2026-11-02T19:12:00.000Z",
"paidAt": "2026-11-02T18:13:41.000Z",
"createdAt": "2026-11-02T18:12:00.000Z",
"updatedAt": "2026-11-02T18:13:41.000Z"
},
"livemode": false
}{
"ok": true,
"eventId": "charge.paid:6f0c1a52-1d7e-4b8a-9f43-2c5e8d9b7a10",
"type": "charge.paid",
"statusCode": 200,
"durationMs": 184,
"responseBody": "ok",
"responseTruncated": false,
"error": null
}Boas práticas
- Responda 2xx assim que validar a assinatura e processe numa fila. Trabalho pesado antes da resposta causa timeout e reenvio.
- O mesmo evento pode chegar mais de uma vez. Guarde o
iddos eventos processados e ignore repetições. - A ordem de chegada não é garantida. Compare o status recebido com o que você já tem antes de regredir um pedido.
- Antes de liberar algo de valor alto, confirme o status com
GET /v1/charges/:id.
async function processEvent(event) {
const firstTime = await db.webhookEvents.insertIfAbsent({ id: event.id });
if (!firstTime) return;
if (event.type === "charge.paid") {
await orders.markPaid(event.data.externalId, event.data.id);
}
}