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_changed | Uma transação mudou de status (inclui o pagamento) |
payment_link.paid | Um link de pagamento foi pago |
payment_link.canceled | Um link de pagamento foi cancelado |
chargeback.registered | Uma contestação (chargeback) foi registrada |
subscription.charge_created | Uma cobrança de assinatura foi gerada |
webhook.test | Aviso 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çalho | Conteúdo |
|---|---|
X-Gaud-Event-Id | Identificador único do aviso (use para evitar processar duas vezes) |
X-Gaud-Event-Type | Tipo do evento (igual ao campo type) |
X-Gaud-Timestamp | Instante do envio, em segundos Unix |
X-Gaud-Signature | Assinatura: 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-Timestampmuito 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 tentativa | Espera 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
FAILEDpode ser reenviado manualmente comPOST /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 envio | Significado |
|---|---|
PENDING | Aguardando entrega (ou nova tentativa) |
DELIVERED | Entregue |
FAILED | Esgotou as tentativas |
Boas práticas
- Responda
2xxprimeiro, 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-Idpara descartar repetidos. - Não dependa da ordem. Os avisos podem chegar fora de ordem; confira o
to_statuse ocreated_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.