Ir para o conteúdo
Docs

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.

EventoGrupoQuandodata
charge.paidEntradaO 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.paidSaídaO saque chegou na chave PIX de destino.Saque com status paid.
payout.canceledSaídaO saque foi recusado ou cancelado e o valor voltou ao saldo disponível.Saque com status canceled e failureReason.
payout.refundedSaídaO 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.

CampoDescrição
idIdentificador do evento, no formato tipo:id do objeto. É o mesmo em todas as tentativas e reenvios.
typeTipo do evento.
createdAtQuando o evento aconteceu, em ISO 8601 UTC.
dataCobrança ou saque completo.

data em payout.refunded

  • refundedAmountCentsinteger

    Valor devolvido ao seu saldo, em centavos.

CabeçalhosHTTP
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
CorpoJSON
{
  "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.

  1. Leia o corpo bruto, antes de qualquer parser de JSON. Reformatar o JSON muda a assinatura.
  2. Monte timestamp + "." + corpo, calcule o HMAC com o segredo e monte o valor esperado.
  3. Compare em tempo constante e rejeite com 401 se não bater.
  4. Rejeite timestamps com mais de 5 minutos de diferença do seu relógio. Isso impede que uma entrega capturada seja reenviada depois.
Verificação
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.

AjusteValoresPadrão
timeoutSeconds2 a 10 segundos10 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.

TentativaQuandoFase
1Na horaRápida
210 segundos depois da anteriorRápida
31 minuto depois da anteriorRápida
45 minutos depois da anteriorEspaçada
530 minutos depois da anteriorEspaçada
62 horas depois da anteriorEspaçada
76 horas depois da anteriorEspaçada
824 horas depois da anteriorEspaç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.

Corpo do testeJSON
{
  "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
}
Resultado no painelJSON
{
  "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 id dos 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.
Processar uma vez sóNode.js
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);
  }
}
esc