Receive Webhooks
Webhooks are the recommended way to find out when a payment changes state — register an endpoint once and Gladys pushes events to it, instead of you polling GET /payments/:reference in a loop.
1. Register an endpoint
curl -X POST https://dev.gladys.the-all.io/api/v1/webhook-endpoints \
-H "Authorization: Bearer <your access token>" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/gladys/webhooks",
"description": "Order fulfillment"
}'
Leave events out (or empty) to subscribe to everything; pass an array to subscribe to specific event types. The response includes a secret (whsec_...) — shown exactly once, on creation (and again if you rotate it with POST /webhook-endpoints/:id/rotate-secret). Store it; there's no way to read it back later.
The endpoint's environment is inherited from the OAuth client that registered it — a TEST client's endpoint only ever receives TEST events, and a LIVE endpoint must be https (a TEST endpoint may use plain http, useful for pointing at a local tunnel during development).
2. Handle the delivery
Every event is a POST with this body:
{
"event_id": "evt_...",
"event_type": "payment.captured",
"created_at": "2026-08-31T12:00:00Z",
"payment": { "reference": "pay_...", "status": "CAPTURED", "...": "..." },
"refund": null
}
refund is only present on payment.refunded — it names which refund this is, since one payment can be partially refunded more than once. The event types are: payment.authorized, payment.captured, payment.cancelled, payment.expired, payment.refunded.
Respond with any 2xx status to acknowledge receipt. Anything else — including a timeout — is treated as a failed attempt and retried.
3. Verify the signature
Every delivery carries these headers:
| Header | Contents |
|---|---|
Gladys-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256> |
Gladys-Event-Id | Stable across retries — use it to deduplicate |
Gladys-Event-Type | e.g. payment.captured |
Gladys-Delivery-Attempt | 1-indexed attempt number |
The signature is an HMAC-SHA256 over "<unix timestamp>.<raw request body>", keyed with your endpoint's secret:
import hashlib
import hmac
import time
def verify(secret: str, raw_body: bytes, signature_header: str, tolerance_seconds: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in signature_header.split(","))
timestamp, signature = int(parts["t"]), parts["v1"]
if abs(time.time() - timestamp) > tolerance_seconds:
return False # too old — possible replay
signed_payload = f"{timestamp}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed_payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)
Verify against the raw request body bytes, before any JSON parsing — re-serializing and re-signing your parsed object will not match. Reject anything more than 5 minutes old.
4. Retries
A delivery that doesn't get a 2xx is retried on this schedule: 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours, 12 hours — 8 attempts in total, spread over roughly a day. After the last attempt, the delivery is marked FAILED but never deleted.
You can inspect delivery history and manually retry a specific one:
curl https://dev.gladys.the-all.io/api/v1/webhook-deliveries \
-H "Authorization: Bearer <your access token>"
curl -X POST https://dev.gladys.the-all.io/api/v1/webhook-deliveries/<delivery id>/replay \
-H "Authorization: Bearer <your access token>"
A replay resends the exact original payload and signature — it doesn't regenerate the event, so the signature verification code above still works unmodified.
Falling back to polling
Webhooks are best-effort delivery, not a guarantee — your endpoint could be down for the entire retry window. GET /payments/:reference (see Accept a Payment) always reflects the current state and is a reasonable reconciliation check even if you rely on webhooks day to day.