Obexal Docs

Docs/Administration/Webhooks

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

EventEmitted when
user.createdA user account is created (signup, invitation activation, provisioning)
user.loginA user signs in successfully
user.deletedA 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"
}
Warning

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:

HeaderContent
Content-Typeapplication/json
User-AgentObexal-Webhooks/1
X-Obexal-EventThe event name, for example user.created
X-Obexal-DeliveryA unique delivery identifier (hex)
X-Obexal-Signaturesha256=<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"
Note

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://. Plain http://, localhost and 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 2xx response 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 envelope id). A retry after a lost 2xx would deliver the same event twice.
  • Acknowledge fast: respond 2xx within 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.