ObjectOS
Configure

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:

ObjectPurpose
sys_webhookWebhook subscription (target URL, event filter, secret, status)
sys_webhook_deliveryDelivery 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_delivery for 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 2xx within 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:

  1. Check sys_webhook_delivery for the row — response_code, response_body, and attempt are recorded.
  2. Confirm outbound network access from ObjectOS to the receiver.
  3. If the receiver was permanently changed, update the subscription URL and re-deliver the row from the Console.
  4. 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.

On this page