Webhooks
Assine eventos de booking, voucher e cupom. Cada delivery carrega assinatura HMAC-SHA256 e retenta com backoff exponencial.
CRUD de assinaturas
Crie endpoints por tenant. O secret é devolvido em texto puro uma única vez — na resposta do POST. Se perder, apague e recrie.
POST /webhooks
curl -X POST https://greenflow.live/api/v1/webhooks \
-H "Authorization: Bearer gfc_7s2wprmy_DWTZRBGBEV6La4dTOoFdMkEhWmWCsXdwc3zVMnqP" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/hooks/gfcars",
"events": ["booking.created", "voucher.emitted"]
}'
{
"data": {
"id": 7,
"url": "https://example.com/hooks/gfcars",
"events": ["booking.created", "voucher.emitted"],
"active": true,
"consecutive_failures": 0,
"last_success_at": null,
"last_failure_at": null,
"disabled_at": null,
"created_at": "2026-04-19T03:30:00+00:00",
"secret": "j8R3xT...48-chars...K2pNqL"
}
}
GET /webhooks
curl -s https://greenflow.live/api/v1/webhooks \
-H "Authorization: Bearer gfc_7s2wprmy_DWTZRBGBEV6La4dTOoFdMkEhWmWCsXdwc3zVMnqP"
PATCH /webhooks/{id}
curl -X PATCH https://greenflow.live/api/v1/webhooks/7 \
-H "Authorization: Bearer gfc_7s2wprmy_DWTZRBGBEV6La4dTOoFdMkEhWmWCsXdwc3zVMnqP" \
-H "Content-Type: application/json" \
-d '{ "active": true }'
DELETE /webhooks/{id}
curl -X DELETE https://greenflow.live/api/v1/webhooks/7 \
-H "Authorization: Bearer gfc_7s2wprmy_DWTZRBGBEV6La4dTOoFdMkEhWmWCsXdwc3zVMnqP"
POST /webhooks/{id}/test
curl -X POST https://greenflow.live/api/v1/webhooks/7/test \
-H "Authorization: Bearer gfc_7s2wprmy_DWTZRBGBEV6La4dTOoFdMkEhWmWCsXdwc3zVMnqP"
Verificação de assinatura
Cada delivery envia X-GFCars-Signature = sha256= + HMAC-SHA256(secret, raw_body). Verifique contra os bytes crus do body, não contra JSON re-serializado — a assinatura é calculada com JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE, e qualquer framework que re-parseia e re-stringifica quebra a comparação timing-safe.
| Header | Valor | Uso |
|---|---|---|
| X-GFCars-Event | booking.created | Event discriminator. |
| X-GFCars-Delivery | 123 | Idempotency key — same id ⇒ same attempt. |
| X-GFCars-Signature | sha256=<hex> | HMAC-SHA256 of raw body. |
| X-GFCars-Timestamp | 1745067023 | Unix epoch — enforce replay window. |
Verificador em Node.js
const crypto = require('crypto');
function verifyGfCarsSignature(rawBody, signatureHeader, secret) {
if (!signatureHeader || !signatureHeader.startsWith('sha256=')) return false;
const expected = signatureHeader.slice('sha256='.length);
const actual = crypto.createHmac('sha256', secret).update(rawBody, 'utf8').digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(actual, 'hex'));
}
// Express — note the express.raw() middleware to preserve raw bytes.
app.post('/hooks/gfcars', express.raw({ type: 'application/json' }), (req, res) => {
const ok = verifyGfCarsSignature(
req.body,
req.header('X-GFCars-Signature'),
process.env.GFCARS_WEBHOOK_SECRET
);
if (!ok) return res.status(401).end('bad signature');
const ts = parseInt(req.header('X-GFCars-Timestamp'), 10);
if (Math.abs(Date.now() / 1000 - ts) > 300) return res.status(401).end('stale');
const deliveryId = req.header('X-GFCars-Delivery');
const payload = JSON.parse(req.body.toString('utf8'));
// ... process idempotently by deliveryId
res.status(200).end();
});
Verificador em Python
import hmac, hashlib, time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = b"...your-secret..."
def verify(raw_body: bytes, sig_header: str) -> bool:
if not sig_header or not sig_header.startswith("sha256="):
return False
expected = sig_header[len("sha256="):]
actual = hmac.new(SECRET, raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, actual)
@app.post("/hooks/gfcars")
def hooks():
raw = request.get_data()
if not verify(raw, request.headers.get("X-GFCars-Signature", "")):
abort(401, "bad signature")
ts = int(request.headers.get("X-GFCars-Timestamp", "0"))
if abs(time.time() - ts) > 300:
abort(401, "stale")
delivery_id = request.headers["X-GFCars-Delivery"]
# ... process idempotently by delivery_id
return "", 200
Política de retentativas
Deliveries falhados retentam até 5 vezes com backoff exponencial capeado em 60 minutos. Após 10 falhas consecutivas entre deliveries, o endpoint é desabilitado automaticamente — PATCH com active = true para reativar.
| Attempt | Delay |
|---|---|
| 1 | 2 min |
| 2 | 4 min |
| 3 | 8 min |
| 4 | 16 min |
| 5 | 32 min |
| 6+ | 60 min (cap) |
Auto-disable threshold: after WebhookEndpoint::AUTO_DISABLE_THRESHOLD = 10 consecutive failures (across deliveries), the endpoint is paused. Re-enable via PATCH /webhooks/{id} with {"active": true}.
Catálogo de eventos
| Evento | Disparado quando | Payload |
|---|---|---|
| booking.created | After a 201 on /bookings. | {"booking_code":"...","local_number":"...","brand":"..."} |
| booking.canceled | After a successful DELETE /bookings/{code}. | {"booking_code":"...","canceled_at":"..."} |
| voucher.emitted | When the PDF is generated and emailed. | {"booking_code":"...","voucher_number":"..."} |
| coupon.applied | On each successful /coupons/validate. | {"code":"...","discount_type":"..."} |
| webhook.test | Manual dispatch via POST /webhooks/{id}/test. | {"message":"hello"} |
["*"] in events subscribes to all (including new events added later).
Idempotência
Deduplique por X-GFCars-Delivery. O mesmo delivery pode chegar várias vezes (retentativa, expiração de lock, re-dispatch manual).