Seção 08

Webhooks

O Gaud Pay envia um POST ao seu endereço sempre que algo relevante acontece. Assim você não precisa consultar a API.

Cadastrar o endereço

PUT /webhook-endpoint

{ "url": "https://seusistema.com.br/webhooks/gaudpay", "active": true }

Regras do endereço:

  • precisa ser https;
  • não pode conter usuário e senha na URL;
  • precisa apontar para um endereço público (endereços internos, locais ou privados são recusados).

A resposta de criação traz o segredo de assinatura (whsec_…) em texto puro, uma única vez. O Portal também permite cadastrar o endereço em Configurações.

Para trocar o segredo (por exemplo, se vazou), use POST /webhook-endpoint/secret/rotate. O segredo anterior deixa de valer imediatamente: atualize o seu sistema logo em seguida.

Para validar a sua ponta antes de ir ao ar, use POST /webhook-endpoint/test: ele dispara um aviso webhook.test.

Eventos

Evento (type)Quando acontece
transaction.status_changedUma transação mudou de status (inclui o pagamento)
payment_link.paidUm link de pagamento foi pago
payment_link.canceledUm link de pagamento foi cancelado
chargeback.registeredUma contestação (chargeback) foi registrada
subscription.charge_createdUma cobrança de assinatura foi gerada
webhook.testAviso de teste, disparado por você

Formato do aviso

Todo aviso tem o mesmo envelope:

{
  "id": "evt_9c1f0a6b2d3e4f5a8b7c6d5e4f3a2b1c",
  "type": "transaction.status_changed",
  "created_at": "2026-10-12T17:02:11Z",
  "data": { }
}

transaction.status_changed — data:

{
  "payment_id": 30511,
  "transaction_id": 88213,
  "payment_status": "PAID",
  "from_status": "PROCESSING",
  "to_status": "PAID",
  "amount_cents": 15990,
  "payment_method": "PIX",
  "paid_at": "2026-10-12T17:02:11Z",
  "due_date": null
}

paid_at só vem preenchido quando to_status é PAID.

payment_link.paid e payment_link.canceled — data:

{
  "payment_link_id": 912,
  "status": "PAID",
  "amount_cents": 15990,
  "description": "Pedido 1001"
}

chargeback.registered — data:

{
  "chargeback_id": 55,
  "payment_id": 30511,
  "transaction_id": 88213,
  "amount_cents": 15990,
  "status": "OPEN",
  "opened_at": "2026-10-20T09:00:00Z"
}

subscription.charge_created — data:

{
  "subscription_id": 12,
  "payment_id": 30700,
  "amount_cents": 4990,
  "due_date": "2026-11-05T00:00:00Z"
}

Os avisos nunca trazem dados de cartão.

Cabeçalhos de cada envio

CabeçalhoConteúdo
X-Gaud-Event-IdIdentificador único do aviso (use para evitar processar duas vezes)
X-Gaud-Event-TypeTipo do evento (igual ao campo type)
X-Gaud-TimestampInstante do envio, em segundos Unix
X-Gaud-SignatureAssinatura: sha256= seguido do HMAC em hexadecimal

Validar a assinatura

A assinatura é o HMAC-SHA256, com o seu segredo whsec_…, da mensagem:

{X-Gaud-Timestamp}.{corpo bruto da requisição}

Importante: use o corpo bruto (os bytes exatamente como chegaram). Se você ler o JSON e reserializar, a assinatura não confere.

Node.js (Express)

import crypto from "node:crypto";
import express from "express";

const SECRET = process.env.GAUDPAY_WEBHOOK_SECRET; // whsec_...
const TOLERANCE_SECONDS = 300;

const app = express();

// raw: precisamos do corpo exatamente como chegou
app.post("/webhooks/gaudpay", express.raw({ type: "application/json" }), (req, res) => {
  const timestamp = req.get("X-Gaud-Timestamp");
  const received = req.get("X-Gaud-Signature") ?? "";
  const body = req.body.toString("utf8");

  const expected =
    "sha256=" + crypto.createHmac("sha256", SECRET).update(`${timestamp}.${body}`).digest("hex");

  const a = Buffer.from(received);
  const b = Buffer.from(expected);
  const valid = a.length === b.length && crypto.timingSafeEqual(a, b);
  const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) <= TOLERANCE_SECONDS;

  if (!valid || !fresh) return res.sendStatus(401);

  const event = JSON.parse(body);
  // 1) grave event.id e ignore se já processou
  // 2) enfileire o processamento e responda rápido
  res.sendStatus(200);
});

Python (FastAPI)

import hashlib
import hmac
import os
import time

from fastapi import FastAPI, HTTPException, Request

SECRET = os.environ["GAUDPAY_WEBHOOK_SECRET"]  # whsec_...
TOLERANCE_SECONDS = 300

app = FastAPI()


@app.post("/webhooks/gaudpay")
async def gaudpay_webhook(request: Request):
    body = await request.body()  # bytes brutos
    timestamp = request.headers.get("X-Gaud-Timestamp", "")
    received = request.headers.get("X-Gaud-Signature", "")

    message = f"{timestamp}.".encode() + body
    expected = "sha256=" + hmac.new(SECRET.encode(), message, hashlib.sha256).hexdigest()

    if not hmac.compare_digest(received, expected):
        raise HTTPException(status_code=401)
    if abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS:
        raise HTTPException(status_code=401)

    # 1) grave o id do evento e ignore se já processou
    # 2) enfileire o processamento e responda rápido
    return {"ok": True}

Dica de implementação: rejeite avisos com X-Gaud-Timestamp muito antigo (nos exemplos, mais de 5 minutos). Isso impede que alguém reapresente um aviso capturado.

Política de entrega e tentativas

  • O aviso é considerado entregue quando o seu endereço responde qualquer 2xx.
  • O tempo limite de resposta é de 10 segundos. Responda rápido e processe depois.
  • Redirecionamentos não são seguidos.
  • Se falhar, o Gaud Pay tenta de novo, até 7 tentativas no total:
Depois da tentativaEspera até a próxima
1ª1 minuto
2ª5 minutos
3ª30 minutos
4ª2 horas
5ª6 horas
6ª12 horas
7ª— (o aviso fica como FAILED)
  • Um aviso FAILED pode ser reenviado manualmente com POST /webhook-deliveries/{id}/retry.

Acompanhar os envios

GET /webhook-deliveries (filtros status e event_type; paginação com limit e offset):

{
  "items": [
    {
      "id": 301,
      "event_id": "evt_9c1f0a6b2d3e4f5a8b7c6d5e4f3a2b1c",
      "event_type": "transaction.status_changed",
      "status": "DELIVERED",
      "attempts": 1,
      "last_http_status": 200,
      "last_error": null,
      "next_attempt_at": null,
      "delivered_at": "2026-10-12T17:02:12Z",
      "created_at": "2026-10-12T17:02:11Z"
    }
  ]
}
Status do envioSignificado
PENDINGAguardando entrega (ou nova tentativa)
DELIVEREDEntregue
FAILEDEsgotou as tentativas

Boas práticas

  • Responda 2xx primeiro, processe depois. Grave o evento numa fila e devolva a resposta.
  • Seja idempotente. O mesmo aviso pode chegar mais de uma vez (reenvio, falha de rede). Use X-Gaud-Event-Id para descartar repetidos.
  • Não dependa da ordem. Os avisos podem chegar fora de ordem; confira o to_status e o created_at.
  • Tenha um plano B. Se o seu sistema ficou fora do ar, os avisos pendentes continuam sendo tentados pelo prazo acima, e você pode reenviar os falhos.