Skip to main content

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:

HeaderContents
Gladys-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256>
Gladys-Event-IdStable across retries — use it to deduplicate
Gladys-Event-Typee.g. payment.captured
Gladys-Delivery-Attempt1-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.