onhr
Browse guides

Outbound webhooks

Send signed workspace event metadata to an HTTPS receiver.

Workspace owners and admins manage subscriptions under Account → Integrations → Webhooks. A verified MFA session is required. Staff, Viewer and demo sessions cannot manage subscriptions. Existing API keys and connected AI apps are separate integrations.

Set up a receiver

  1. Prepare a public HTTPS endpoint on port 443 with a valid TLS certificate and IPv4 DNS records. Private/reserved addresses, IPv6-only destinations, credentials in the URL, query strings, fragments and redirects are unsupported. DNS is validated again and the public address pinned for each request.
  2. Create a subscription, choose its event topics and save the displayed signing secret in your receiver's secret configuration. It is shown only after creation or rotation; the server stores an encrypted copy.
  3. Configure signature verification and event-ID deduplication below. Queue a test. This sends webhook.test metadata even while the subscription is paused; it requires the delivery worker to be enabled and scheduled.
  4. Refresh status after the worker runs. A 2xx test response permits Enable. This confirms delivery, not ownership of the endpoint. Enable explicitly to receive new matching events; no old business events are backfilled.

There are at most 10 unarchived subscriptions per workspace. URL and topics are immutable: create a replacement subscription to change them. Rotate secret pauses delivery, cancels queued work and requires another successful test. Pause and Archive also cancel queued deliveries. An in-flight request may still reach the receiver. Archived subscriptions cannot be restored.

Payload

The first supported topics are:

TopicTrigger
hire.application.createdAn application is inserted, from any supported application source.
hire.offer_decision.recordedAn offer-decision snapshot is inserted or its decision changes. Status is unknown, pending, accepted or declined.
finance.invoice.status_changedAn invoice enters posted or void. This does not report invoice payment or opening.
finance.payment.status_changedA Finance money movement (incoming/outgoing, including transfer movements) enters posted or void. This is not a CHIP gateway payment notification.
{
  "version": 1,
  "id": "event-uuid",
  "type": "finance.invoice.status_changed",
  "created_at": "2026-11-01T02:00:00+00:00",
  "workspace_id": "workspace-uuid",
  "data": { "id": "record-uuid", "status": "posted" }
}

A test has the same envelope with type: "webhook.test" and data: { "test": true }. Normal events include only IDs, status and timestamps. No names, emails, amounts, CVs, notes or invoice contents are included. Fetching additional data requires a separately authorized integration. Retries keep the original payload and event ID; multiple subscriptions for the same mutation share its event ID.

Verify the signature

Requests are JSON POSTs with these headers:

  • X-ONHR-Event-Id: the payload's event ID.
  • X-ONHR-Timestamp: Unix time in seconds, refreshed for each delivery attempt.
  • X-ONHR-Signature: v1= followed by lowercase hex HMAC-SHA256 of timestamp + "." + rawBody, using the displayed secret as a UTF-8 string, not hex-decoded bytes.

Verify against the exact request body bytes before parsing JSON. Check the timestamp is within five minutes of your server clock and compare signatures using a constant-time comparison. Check header ID matches payload ID. Do not log the secret or unverified request body.

Use a durable unique key of subscription/receiver identity plus event ID. Record that key atomically with your business effect, then return 2xx. Return 2xx for an already processed event. If several subscriptions intentionally feed one shared processor, deduplicate by event ID across those subscriptions. A valid signature proves knowledge of the configured secret; it does not grant permission to act outside that workspace.

ONHR uses Node's HTTPS request API and DNS lookup API for validated, pinned connections. TLS certificate verification remains enabled and redirects are treated as failed deliveries.

Delivery and failure history

Delivery is asynchronous, at least once and not ordered. A timeout may occur after the receiver has already processed an event. A response in the 2xx range completes delivery; any other status, network failure or invalid endpoint is retried. Attempts have a five-second DNS limit and ten-second HTTP limit. Response bodies are discarded; history contains only status codes and fixed failure codes.

There are five attempts per delivery, with backoffs of 1 minute, 5 minutes, 30 minutes and 6 hours. A claimed delivery has a five-minute lease; a crashed worker can be reclaimed without resetting its attempt count. Each scheduled call claims at most five deliveries. Overlapping calls use separate claims; late/stale completions cannot overwrite cancelled or reclaimed work. Allowing overlapping requests can reorder deliveries, so use event timestamps and fetch current record state when needed.

Retry failed events queues up to 20 latest exhausted business events. Their original event IDs remain unchanged and their old failure records remain in history. Events with a newer delivery already present are skipped. Tests are retried through Queue test, limited to one new test per minute and one outstanding test per subscription.

Each subscription has a 1,000-delivery pending/processing cap. Reaching it pauses new event collection and records queue_limit; that triggering event and subsequent events while paused are not backfilled. Drain or cancel the backlog and explicitly re-enable. Enqueue failures never block saving the source business record. Webhooks are a best-effort integration, not an accounting or compliance ledger. The screen shows the latest 100 delivery records, including archived subscriptions.

Operator configuration

This feature is disabled by default. Apply 20261128123000_outbound_webhooks.sql after the Phase 6B offer-decision migration. Configure AI_KEYS_ENCRYPTION_SECRET (existing server encryption secret, at least 32 characters), SUPABASE_SERVICE_ROLE_KEY and CRON_SECRET. Keep the encryption secret stable; replacing it prevents existing signing secrets from opening until subscriptions are rotated under the new key.

Schedule authenticated POST /api/webhooks/send every minute with Authorization: Bearer <CRON_SECRET>. Set OUTBOUND_WEBHOOKS_ENABLED=true only when ready to send to user-configured endpoints. The route has a 120-second duration budget; configure the reverse proxy/runtime accordingly. Disabled workers leave queued work pending. Do not place cron or signing secrets in URLs or source control.

Terminal delivery records are retained for 30 days; pending/processing records are retained until completed or cancelled. The migration schedules a daily pruning job if pg_cron is installed. Otherwise, the database operator must schedule select private.prune_outbound_webhooks() daily with a privileged database connection. It is intentionally unavailable to application roles. Subscription audit entries contain IDs/actions, not secrets or response contents. This development step does not deploy the migration, configure production scheduling or enable live deliveries.