Webhooks
Outbound webhook delivery, signing, and retries.
Webhooks
ObjectOS uses a persistent outbox model for outbound webhooks. When the webhook plugin is enabled, business changes enqueue a delivery row and a background dispatcher delivers it with retries — so a slow or unavailable receiver never blocks the originating transaction.
Enabling webhooks
Webhooks are an optional capability. The deployed ObjectOS image must
include @objectstack/plugin-webhooks, and the application artifact
must register webhook subscriptions (typically as records of a
sys_webhook object).
When enabled, two objects show up in the Console:
| Object | Purpose |
|---|---|
sys_webhook | Webhook subscription (target URL, event filter, secret, status) |
sys_webhook_delivery | Delivery log (URL, response code, attempt count, retry timestamp) |
Delivery semantics
- At-least-once. A delivery may be retried after a transient failure; receivers must be idempotent.
- Persistent. Deliveries survive ObjectOS restarts because they are stored in the business database.
- Partitioned. Each dispatcher worker claims a partition of the outbox so deployments can horizontally scale dispatch without double delivery.
- Bounded retries. Failed deliveries are retried with backoff until
a configurable cap; exhausted rows stay in
sys_webhook_deliveryfor inspection.
Signing
Every delivery carries identifying headers so receivers can route, deduplicate, and verify it:
X-Objectstack-Event: <event type, e.g. data.record.created>
X-Objectstack-Delivery: <delivery id — use as your idempotency key>
X-Objectstack-Attempt: <attempt number, starting at 1>When a webhook subscription has a secret, ObjectOS also signs every
request:
X-Objectstack-Signature: sha256=<hex hmac>The signature is HMAC-SHA256(secret, body) computed over the raw
request body. Verify it on the receiver before trusting the payload:
import { createHmac, timingSafeEqual } from 'node:crypto';
function verify(body, signatureHeader, secret) {
const expected = 'sha256=' + createHmac('sha256', secret).update(body).digest('hex');
return timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected));
}Rotate secrets by issuing a new subscription with the new secret, running both for a transition window, then disabling the old one.
Receiver expectations
- Respond with
2xxwithin a few seconds. A408,429, or5xxresponse (or a timeout/transport error) is retriable and gets retried with backoff. Any other4xxis treated as a permanent failure and moved todeadwithout further retries. - The dispatcher does not mark a delivery successful until it sees a
2xx, so a failed receiver keeps the row insys_webhook_deliveryfor inspection or redelivery. - Be idempotent — deduplicate on the
X-Objectstack-Deliveryheader (the delivery id) or your own event id in the payload.
Failure handling
When something fails:
- Check
sys_webhook_deliveryfor the row —status,response_code,response_body, andattemptsare recorded. - Confirm outbound network access from ObjectOS to the receiver.
- If the receiver was permanently changed, update the subscription URL and re-deliver the row from the Console.
- For incident review, audit logs (
sys_audit_log) capture subscription edits but not payloads — payloads stay in the outbox.
Operational tips
- Do not put secrets in webhook URLs (query strings get logged).
- Use a dedicated receiver hostname so you can shed load by blocking it at the edge without affecting the main app.
- Watch the dispatcher lag — a growing outbox usually means the receiver is degraded.