Webhooks
Outbound HTTP webhook endpoints for entity lifecycle events, with HMAC-SHA256 signature verification, Inngest-driven delivery, and automatic failure tracking.
Webhooks
Webhooks deliver HTTP POST notifications to external systems when events occur in the platform. Administrators configure endpoint URLs, select which events to subscribe to, and provide a secret for signature verification. Delivery is handled asynchronously via Inngest with retry on failure.
Overview
The module lives in features/webhooks/ and provides the data layer (types, server actions, signature utilities) and admin UI. Webhook delivery is handled by the features/inngest/functions/webhook-delivery.ts Inngest function, which sends the HTTP request, verifies success, and increments the failure counter on error.
Webhooks are also used as action side effects: field-population lifecycle events (on_populate, on_approve, on_reject) can trigger webhook deliveries when configured for an action.
Key Concepts
WebhookEndpoint -- The database record for a configured endpoint:
url-- the destination URL that receives POST requestssecret-- shared secret for HMAC-SHA256 signature generationevents-- array of event types this endpoint subscribes toenabled-- toggle to pause delivery without deleting the endpointfailure_count-- incremented on delivery failure, visible in the admin UIdescription-- optional human-readable labellast_triggered_at-- timestamp of last successful delivery
WebhookEndpointView -- The WebhookEndpoint type with the secret field omitted. Used in API responses and admin UI to avoid exposing secrets.
Available Events -- The platform defines five webhook event types:
entity/created-- a new entity record was createdentity/updated-- an entity was modifiedentity/deleted-- an entity was removeddocument/uploaded-- a document was uploadedagent/heartbeat-- an agent heartbeat run completed
Signature Verification -- Every webhook payload is signed with HMAC-SHA256 using the endpoint's secret. The signature is included as a header so receivers can verify authenticity.
How It Works
Configuration
Administrators manage webhooks via the Admin panel. The WebhookAdmin component provides:
- A list of configured endpoints showing URL, subscribed events, enabled status, and failure count
- A dialog for creating new endpoints (URL, secret, description, event selection)
- Toggle switches to enable/disable endpoints
- Delete buttons for removal
Delivery Pipeline
- An event occurs in the platform (entity created, document uploaded, etc.)
- The Inngest function
webhook-deliveryreceives the event - It queries
webhook_endpointsfor enabled endpoints subscribed to this event type - For each matching endpoint:
- Serialize the event payload as JSON
- Generate HMAC-SHA256 signature using
signPayload(payload, secret) - Send HTTP POST to the endpoint URL with the payload and signature header
- On success: update
last_triggered_at - On failure: increment
failure_count
Signature Verification
The features/webhooks/lib/signature.ts module provides:
signPayload(payload, secret)-- generates HMAC-SHA256 hex digestverifySignature(payload, signature, secret)-- constant-time comparison to prevent timing attacks
Receivers should verify the signature header against the payload using their copy of the shared secret.
API Reference
Server Actions
| Function | Location | Purpose |
|---|---|---|
listWebhooks() | features/webhooks/server/actions.ts | List all endpoints (secrets excluded) |
getWebhook(id) | Same | Fetch single endpoint |
createWebhook(input) | Same | Create endpoint with URL, secret, events |
updateWebhook(id, input) | Same | Update endpoint fields |
deleteWebhook(id) | Same | Delete endpoint |
Signature Functions
| Function | Location | Purpose |
|---|---|---|
signPayload(payload, secret) | features/webhooks/lib/signature.ts | Generate HMAC-SHA256 signature |
verifySignature(payload, signature, secret) | Same | Constant-time signature verification |
Types
| Type | Location | Purpose |
|---|---|---|
WebhookEndpoint | features/webhooks/types.ts | Full endpoint record |
WebhookEndpointView | Same | Endpoint without secret |
CreateWebhookInput | Same | Create input shape |
UpdateWebhookInput | Same | Partial update input |
Components
| Component | Location | Purpose |
|---|---|---|
WebhookAdmin | features/webhooks/components/webhook-admin.tsx | Full admin CRUD UI |
For Agents
Agents do not directly manage webhooks. Webhook configuration is an admin-only operation. However, agent actions (creating entities, updating records, running heartbeats) generate the events that trigger webhook deliveries.
Agents can also indirectly trigger webhooks through extraction field actions: when a field has an on_populate or on_approve action of type webhook, the webhook delivery pipeline fires after extraction completes.
Inbound Webhooks
The platform also accepts inbound webhooks from external systems and converts them into entity records. This is the push side of external integrations: a third-party service (Acuity Scheduling, Stripe, any SaaS with a webhook feature) posts to a per-tenant endpoint URL; the platform verifies the signature, maps the payload to entity fields, and creates or upserts the entity.
Configuration
Each inbound endpoint is a row in inbound_webhook_endpoints and carries:
token— URL-safe random token that uniquely identifies the endpoint (/api/webhooks/inbound/{token})entity_type_slug— which entity type to create/upsertfield_mapping— JSON mapping from source payload keys to entitycontentfield namessignature_secret— shared secret used to verify the HMAC signature from the sendersignature_header— the header name the sender puts the signature in (e.g.x-acuity-signature,stripe-signature)signature_encoding— encoding of the digest in the signature header.hex(default, 64 chars — Stripe, Slack, GitHub, most SaaS) orbase64(44 chars — Acuity Scheduling, Squarespace Scheduling). Mismatch causes verification to short-circuit on length, so set this correctly per provider.
Inbound endpoint rows are created via seed scripts or the Admin > Webhooks UI (inbound tab).
Delivery Route
POST /api/webhooks/inbound/{token}Accepted content types:
application/json— default; body parsed directly as JSONapplication/x-www-form-urlencoded— form-encoded bodies (Acuity Scheduling, SendGrid, Mailgun, and other legacy SaaS providers); decoded to a flatRecord<string, string>before field mapping
Multipart/form-data is intentionally not supported. Route through a JSON-converting intermediary if needed.
HMAC verification runs over the raw wire bytes before content-type decoding, so signature integrity is preserved for all content types.
Acuity Scheduling Example
Acuity sends a skinny webhook on appointment events ({action, id, calendarID, appointmentTypeID}) as application/x-www-form-urlencoded. The DOC'S integration uses this flow:
- Acuity posts to
/api/webhooks/inbound/{token}withx-acuity-signatureheader (base64-encoded HMAC-SHA256) - Platform verifies HMAC using the Acuity API key as the shared secret, with
signature_encoding: 'base64'set on the endpoint row field_mappingcreates a stub visit withacuity_idandsync_status: 'webhook'entity_createdevent fires- Linker action (
docs-acuity-link-visit-to-patient) runs: fetches the full appointment from the Acuity REST API via the tenant'sagent_connectionsrow, expands the stub, and resolves the visit to a patient record
This skinny-webhook + REST-expansion pattern keeps processInboundWebhook as a lean router and puts the round-trip fetch inside a retryable action with session-event logging.
Design Decisions
- Raw-bytes HMAC — signature verification runs before content-type parsing so the HMAC covers the unmodified wire representation. This matches what Acuity and most SaaS providers expect.
- Stub + linker pattern — inbound webhook creates the minimum entity; a triggered action does the heavy enrichment. Separation of concerns; enrichment is retryable and observable.
- No multipart support — file uploads belong in a dedicated pipeline. If a provider sends files via webhook, pipe through an intermediary.
Related Modules
- Inngest (
features/inngest/) --webhook-deliveryfunction handles async delivery - Extraction (
features/entities/extraction/) -- field actions can trigger webhooks - API Keys (
features/api-keys/) -- external systems receiving webhooks may use API keys to call back into the platform - Admin -- webhook management lives in the Admin panel
- Connections (
features/agents/connection-presets.ts) -- REST connection presets used by actions that expand inbound webhook stubs