Event webhooks
Receive tenant lifecycle events on your HTTPS endpoints, verify the HMAC-SHA256 signature, and build idempotent consumers.
Webhooks push tenant events to your own systems as they happen: Obexal sends a signed JSON POST to every enabled endpoint subscribed to the event. Endpoints are managed per tenant with the tenant:manage permission, from the console or the admin API. Examples use https://accounts.obexal.com; replace it with your custom domain if you use one.
Available events
| Event | Emitted when |
|---|---|
user.created | A user account is created (signup, invitation activation, provisioning) |
user.login | A user signs in successfully |
user.deleted | A user account is deleted |
Subscribing to any other event name is refused at creation time. The catalog is also returned by GET /v1/admin/webhooks in the knownEvents field.
Create an endpoint
curl -X POST https://accounts.obexal.com/v1/admin/webhooks \
-H "Authorization: Bearer $OBX_TOKEN" \
-H "Content-Type: application/json" \
-d '{"url":"https://hooks.example.eu/obexal","events":["user.created","user.deleted"]}'{
"id": "wh_01hzx...",
"url": "https://hooks.example.eu/obexal",
"events": ["user.created", "user.deleted"],
"enabled": true,
"createdAt": "2026-07-02T09:15:00Z",
"secret": "f3a1c9...64 hex characters...b7d2"
}The secret (a 256-bit signing key) is returned only once. Obexal stores it encrypted and never exposes it again; if you lose it, delete the endpoint and create a new one.
GET /v1/admin/webhooks lists your endpoints (without secrets); DELETE /v1/admin/webhooks/{id} removes one.
The delivery request
Each delivery is an HTTP POST with these headers:
| Header | Content |
|---|---|
Content-Type | application/json |
User-Agent | Obexal-Webhooks/1 |
X-Obexal-Event | The event name, for example user.created |
X-Obexal-Delivery | A unique delivery identifier (hex) |
X-Obexal-Signature | sha256=<hex HMAC-SHA256 of the raw body> |
The body is a signed envelope:
{
"id": "9f2c4e8a1b7d3f6c9e0a5b2d4f8c1e7a",
"event": "user.created",
"tenantId": "ten_01hzx...",
"createdAt": "2026-07-02T09:15:00Z",
"data": {"userId": "usr_01hzx...", "email": "alice@example.eu"}
}The data payload depends on the event: user.created carries userId and email, user.login carries userId and tenantId, user.deleted carries userId.
Verify the signature
Compute an HMAC-SHA256 of the raw request body (byte for byte, before any parsing) with your endpoint secret, hex-encode it, and compare it to the header value after the sha256= prefix. Reject the request if they differ.
# body.json = the raw request body, exactly as received
expected="sha256=$(openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" < body.json | awk '{print $NF}')"
received="$HTTP_X_OBEXAL_SIGNATURE"
[ "$expected" = "$received" ] && echo "signature OK" || echo "REJECT"In application code, use a constant-time comparison function to compare the two signatures, and verify before parsing the JSON.
Target URL restrictions (anti-SSRF)
Webhook targets are chosen by tenant admins, so Obexal refuses to be turned into a proxy against internal infrastructure:
- The URL must be
https://. Plainhttp://,localhostand private IP literals are refused at creation. - At delivery time, the connection is checked again after DNS resolution, just before connect: loopback, private, link-local, ULA, unspecified and multicast addresses are refused. This closes the DNS rebinding window.
Delivery guarantees, honestly
Delivery is best-effort, at-most-once per endpoint:
- Any
2xxresponse acknowledges the delivery. Anything else (or a timeout) triggers a retry: 3 attempts in total, with a short backoff, then the delivery is abandoned and logged server-side. - Deliveries are queued in memory, outside the request path. If the queue saturates, the event is dropped (and logged).
- The queue is not durable: deliveries pending during a service restart are lost.
Treat webhooks as signals, not as a system of record. For guarantees, reconcile periodically against the API (for example GET /v1/admin/users) and use the audit log export.
Consumer best practices
- Be idempotent: deduplicate on
X-Obexal-Delivery(or the envelopeid). A retry after a lost2xxwould deliver the same event twice. - Acknowledge fast: respond
2xxwithin a few seconds and process asynchronously; slow handlers burn the retry budget. - Tolerate the unknown: ignore event types you do not handle, so future events do not break your consumer.
- Rotate by replacement: to change a secret, create a new endpoint, switch your verification, then delete the old one.