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
When a webhook subscription has a secret, ObjectOS 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. Anything else is treated as a failure and retried. - Treat any non-2xx as "do not commit." The dispatcher does not consume the row until you ack.
- Be idempotent — deduplicate on the delivery id header or your own event id in the payload.
Failure handling
When something fails:
- Check
sys_webhook_deliveryfor the row —response_code,response_body, andattemptare 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.