GreenFlow docs

Webhooks

Suscribite a eventos de booking, voucher y cupón. Cada delivery lleva firma HMAC-SHA256 y reintenta con backoff exponencial.

CRUD de suscripciones

Creá endpoints por tenant. El secret se devuelve en texto plano una única vez — en la respuesta al POST. Si lo perdés, borrá y recreá.

POST /webhooks

bash
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"]
  }'
json
{
  "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

bash
curl -s https://greenflow.live/api/v1/webhooks \
  -H "Authorization: Bearer gfc_7s2wprmy_DWTZRBGBEV6La4dTOoFdMkEhWmWCsXdwc3zVMnqP"

PATCH /webhooks/{id}

bash
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}

bash
curl -X DELETE https://greenflow.live/api/v1/webhooks/7 \
  -H "Authorization: Bearer gfc_7s2wprmy_DWTZRBGBEV6La4dTOoFdMkEhWmWCsXdwc3zVMnqP"

POST /webhooks/{id}/test

bash
curl -X POST https://greenflow.live/api/v1/webhooks/7/test \
  -H "Authorization: Bearer gfc_7s2wprmy_DWTZRBGBEV6La4dTOoFdMkEhWmWCsXdwc3zVMnqP"

Verificación de firma

Cada delivery manda X-GFCars-Signature = sha256= + HMAC-SHA256(secret, raw_body). Verificá contra los bytes crudos del body, no contra un JSON re-serializado — la firma se computa con JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE, y cualquier framework que re-parsea y re-stringifica rompe la comparación timing-safe.

Header Valor Uso
X-GFCars-Eventbooking.createdEvent discriminator.
X-GFCars-Delivery123Idempotency key — same id ⇒ same attempt.
X-GFCars-Signaturesha256=<hex>HMAC-SHA256 of raw body.
X-GFCars-Timestamp1745067023Unix epoch — enforce replay window.

Verificador en Node.js

javascript
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 en Python

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 reintentos

Los deliveries fallidos reintentan hasta 5 veces con backoff exponencial capeado en 60 minutos. Tras 10 fallos consecutivos entre deliveries, el endpoint se deshabilita automáticamente — PATCH con active = true para re-activarlo.

AttemptDelay
12 min
24 min
38 min
416 min
532 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 Se dispara cuando Payload
booking.createdAfter a 201 on /bookings.{"booking_code":"...","local_number":"...","brand":"..."}
booking.canceledAfter a successful DELETE /bookings/{code}.{"booking_code":"...","canceled_at":"..."}
voucher.emittedWhen the PDF is generated and emailed.{"booking_code":"...","voucher_number":"..."}
coupon.appliedOn each successful /coupons/validate.{"code":"...","discount_type":"..."}
webhook.testManual dispatch via POST /webhooks/{id}/test.{"message":"hello"}

["*"] in events subscribes to all (including new events added later).

Idempotencia

Deduplicá por X-GFCars-Delivery. El mismo delivery puede llegar varias veces (reintento, expiración del lock, re-dispatch manual).