Headlessly
REST API

Webhooks

Receive real-time event notifications via HTTP webhooks.

Webhooks deliver real-time event notifications to your server whenever entities are created, updated, deleted, or transition through custom verbs. Every mutation in the immutable event log can trigger a webhook.

Register a Webhook

curl -X POST "https://headless.ly/~acme/webhooks" \
  -H "Authorization: Bearer hly_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/webhooks/headlessly",
    "events": ["Contact.created", "Contact.qualified", "Deal.closed"],
    "secret": "whsec_your_signing_secret"
  }'

Parameters

FieldTypeRequiredDescription
urlstringYesHTTPS endpoint that receives webhook payloads
eventsstring[]YesEvent types to subscribe to (see Event Types below)
secretstringNoSigning secret for payload verification. Auto-generated if omitted

Response

{
  "$id": "webhook_rT4nXwBp",
  "url": "https://your-app.com/webhooks/headlessly",
  "events": ["Contact.created", "Contact.qualified", "Deal.closed"],
  "status": "active",
  "createdAt": "2026-01-15T09:30:00Z"
}

Event Types

Events follow the pattern {Entity}.{verb_past_tense}:

EventTrigger
Contact.createdA Contact entity was created
Contact.updatedA Contact entity was updated
Contact.deletedA Contact entity was deleted
Contact.qualifiedThe qualify verb was executed on a Contact
Deal.closedThe close verb was executed on a Deal
Subscription.upgradedThe upgrade verb was executed on a Subscription
*.createdAny entity was created (wildcard)
*.updatedAny entity was updated (wildcard)

Use * as a wildcard for the entity type to receive all events of that verb.

Webhook Payload

Each delivery sends a POST request to your URL with a JSON body:

{
  "id": "evt_nB2wRtLp",
  "type": "Contact.qualified",
  "timestamp": "2026-01-15T12:30:00Z",
  "tenant": "acme",
  "data": {
    "$id": "contact_fX9bL5nRd",
    "$type": "Contact",
    "name": "Alice Chen",
    "email": "alice@startup.io",
    "stage": "Qualified"
  },
  "previous": {
    "stage": "Lead"
  }
}
FieldTypeDescription
idstringUnique event ID for idempotency
typestringEvent type in Entity.verb format
timestampstringISO 8601 timestamp of when the event occurred
tenantstringTenant slug
dataobjectCurrent state of the entity after the event
previousobjectChanged fields with their previous values (only for updates and verbs)

Signature Verification

Every webhook delivery includes a signature header for payload verification:

X-Headlessly-Signature: sha256=a1b2c3d4e5f6...

Verify the signature by computing HMAC-SHA256 of the raw request body using your webhook secret:

import { createHmac } from 'crypto'

function verifyWebhook(body: string, signature: string, secret: string): boolean {
  const expected = 'sha256=' + createHmac('sha256', secret).update(body).digest('hex')
  return expected === signature
}

Reject any request where the signature does not match. This prevents forged payloads.

Retry Policy

Failed deliveries (non-2xx response or timeout) are retried with exponential backoff:

AttemptDelay
1st retry30 seconds
2nd retry2 minutes
3rd retry15 minutes
4th retry1 hour
5th retry6 hours

After 5 failed retries, the webhook is marked as failing. It remains active and continues to receive new events, but a warning appears in your dashboard. Fix the endpoint and the backlog will drain automatically.

Deliveries time out after 10 seconds. Return a 2xx status code quickly and process the payload asynchronously if needed.

Manage Webhooks

# List all webhooks
GET /~acme/webhooks

# Get a specific webhook
GET /~acme/webhooks/webhook_rT4nXwBp

# Update events or URL
PUT /~acme/webhooks/webhook_rT4nXwBp
{ "events": ["Contact.created", "Deal.*"] }

# Delete a webhook
DELETE /~acme/webhooks/webhook_rT4nXwBp

# View delivery history
GET /~acme/webhooks/webhook_rT4nXwBp/deliveries

Idempotency

Each webhook payload includes a unique id field. Use this to deduplicate deliveries on your end, since retries can result in the same event being delivered more than once.

On this page