WarmblyDocs
Endpoint reference

Webhooks

Subscribe to Warmbly platform events and receive HMAC-signed delivery callbacks at your own HTTPS endpoints.

Webhooks let your systems react to Warmbly activity in real time. You register one or more HTTPS endpoints, optionally filter them to specific event types, and Warmbly POSTs a signed JSON payload to each matching endpoint whenever a subscribed event fires. Every delivery is signed with HMAC-SHA256 and carries a unique event id so you can verify authenticity and dedupe replays. Failed deliveries are retried with exponential backoff and their history is queryable for debugging.

All webhook management endpoints are org-scoped and share one auth gate: Scope WEBHOOKS · Org permission manage_settings. The signing secret is server-generated and only returned at create and rotate time, so capture it immediately.

List webhook endpoints

GET /webhooks

Returns every endpoint configured for the caller's organization. Secrets are never included in this response.

Auth: Scope WEBHOOKS · Org permission manage_settings.

Response

Returns an object with the configured endpoints array. This is not a data + pagination envelope.

{
  "endpoints": [
    {
      "id": "a3f1c2e4-5b6d-7e8f-9a0b-1c2d3e4f5a6b",
      "organization_id": "11111111-2222-3333-4444-555555555555",
      "url": "https://hooks.example.com/warmbly",
      "description": "Production event sink",
      "event_types": ["campaign.reply_received", "meeting.booked"],
      "enabled": true,
      "verified_at": "2026-06-11T18:02:14Z",
      "ownership_confirmed": true,
      "last_success_at": "2026-06-11T18:02:14Z",
      "last_failure_at": null,
      "last_failure_reason": null,
      "consecutive_failures": 0,
      "created_at": "2026-05-01T09:00:00Z",
      "updated_at": "2026-06-11T18:02:14Z"
    }
  ]
}

List available event types

GET /webhooks/event-types

Returns the canonical catalog of every event Warmbly can emit. Use this to build an event-type picker or validate event_types values before writing them.

Auth: Scope WEBHOOKS · Org permission manage_settings.

Response

{
  "event_types": [
    {
      "type": "campaign.reply_received",
      "category": "campaign",
      "description": "A human reply lands for a campaign thread.",
      "firehose": false
    },
    {
      "type": "campaign.email_sent",
      "category": "campaign",
      "description": "A campaign email is dispatched by a worker.",
      "firehose": true
    }
  ]
}

firehose: true means the event is high-volume. An endpoint with an empty event_types list does not receive firehose events automatically. To receive them you must list each one explicitly in event_types. See Firehose events for details.

Create a webhook endpoint

POST /webhooks

Creates a new subscription. The response is the only time the signing secret is returned, so store it immediately. The URL must be HTTPS and resolve to a publicly routable host. Loopback, private, and link-local targets are rejected unless the server runs with unsafe webhook URLs enabled for local or self-hosted development.

New endpoints start in an unverified state. They receive a single signed challenge request and no real events until verification passes. See Endpoint verification.

Auth: Scope WEBHOOKS · Org permission manage_settings.

Request body

FieldTypeRequiredDescription
urlstringYesHTTPS URL that will receive POST callbacks. Must be publicly routable.
descriptionstringNoFree-text label for your own reference.
event_typesstring[]NoEvent names to subscribe to. Each must be a known type. An empty or omitted array subscribes to all non-firehose events.
enabledbooleanNoWhether the endpoint is active. Defaults to true.
{
  "url": "https://hooks.example.com/warmbly",
  "description": "Production event sink",
  "event_types": ["campaign.reply_received", "meeting.booked"],
  "enabled": true
}

Response

201 Created with the full endpoint object, including the one-time secret. A new endpoint starts unverified (verified_at is null) and only receives the live event stream once it passes verification.

{
  "id": "a3f1c2e4-5b6d-7e8f-9a0b-1c2d3e4f5a6b",
  "organization_id": "11111111-2222-3333-4444-555555555555",
  "url": "https://hooks.example.com/warmbly",
  "description": "Production event sink",
  "event_types": ["campaign.reply_received", "meeting.booked"],
  "enabled": true,
  "verified_at": null,
  "ownership_confirmed": false,
  "last_success_at": null,
  "last_failure_at": null,
  "last_failure_reason": null,
  "consecutive_failures": 0,
  "created_at": "2026-06-11T18:00:00Z",
  "updated_at": "2026-06-11T18:00:00Z",
  "secret": "whsec_9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c5b4a39281706f5e4d3c2b1a0"
}

Update a webhook endpoint

PATCH /webhooks/:id

Updates a subscription's URL, description, event filter, or enabled state. The signing secret is not changed here. Use the rotate endpoint for that. The same URL and event-type validation as create applies. Changing the URL host re-arms endpoint verification.

Auth: Scope WEBHOOKS · Org permission manage_settings.

ParameterInTypeDescription
idpathUUIDThe endpoint id to update.

Request body

Same shape as create. All fields are replaced with the values sent, so send the complete desired state (event_types is overwritten, not merged).

FieldTypeRequiredDescription
urlstringYesHTTPS, publicly routable URL.
descriptionstringNoFree-text label.
event_typesstring[]NoReplacement event filter. Empty means all non-firehose events.
enabledbooleanNoActive state. Defaults to true if omitted.

Response

200 OK with the updated endpoint (no secret).

Delete a webhook endpoint

DELETE /webhooks/:id

Deletes a subscription and cascades to its delivery history.

Auth: Scope WEBHOOKS · Org permission manage_settings.

ParameterInTypeDescription
idpathUUIDThe endpoint id to delete.

Response

204 No Content with an empty body.

Rotate the signing secret

POST /webhooks/:id/rotate-secret

Issues a new signing secret and returns it once. In-flight deliveries that were already signed continue to verify against the old secret until they settle, while new deliveries use the new secret. Update your verifier promptly after rotating.

Auth: Scope WEBHOOKS · Org permission manage_settings.

ParameterInTypeDescription
idpathUUIDThe endpoint id to rotate.

Response

200 OK with the new secret. This is the only time it is returned.

{
  "secret": "whsec_1a2b3c4d5e6f70819a2b3c4d5e6f70819a2b3c4d5e6f70819a2b3c4d5e6f7081"
}

Endpoint verification

POST /webhooks/:id/verify

New endpoints start unverified and do not receive real events until they pass a reachability check. Calling this endpoint sends a single signed challenge request to the target URL. It is also useful as a "send a test event" button after the endpoint is already verified.

Auth: Scope WEBHOOKS · Org permission manage_settings.

ParameterInTypeDescription
idpathUUIDThe endpoint id to verify.

Challenge request

Warmbly sends a POST to the endpoint URL with X-Warmbly-Event: webhook.test and a body of:

{
  "event_type": "webhook.test",
  "data": {
    "challenge": "whcg_...",
    "endpoint_id": "a3f1c2e4-5b6d-7e8f-9a0b-1c2d3e4f5a6b",
    "message": "This is a Warmbly webhook verification request."
  }
}

The challenge is signed with the endpoint's secret, so you can verify it exactly like any other delivery (see Verifying signatures).

Responding to the challenge

For simple reachability (dashboard-registered endpoints): respond with any 2xx. The endpoint is marked verified.

For URL ownership proof (required for OAuth-app-registered endpoints): additionally echo the challenge value back. You may return it in the JSON response body:

{ "challenge": "whcg_..." }

Or in a response header:

X-Warmbly-Webhook-Challenge: whcg_...

Changing an endpoint's URL host re-arms verification: the endpoint reverts to unverified and must pass the challenge again before it resumes receiving events.

List all deliveries (org-wide)

GET /webhooks/deliveries

Returns delivery attempts across all endpoints for the organization, newest first. Accepts query parameters to narrow the result.

Auth: Scope WEBHOOKS · Org permission manage_settings.

ParameterInTypeDescription
statusquerystringFilter by delivery status. One of pending, in_flight, delivered, failed, abandoned.
event_typequerystringFilter by event type, for example campaign.reply_received.
limitqueryintegerMax rows. Between 1 and 200, defaults to 50. Out-of-range values return 400.
cursorquerystringOpaque pagination cursor from the previous response's pagination.next_cursor.

Response

Uses the standard data + pagination envelope with an opaque cursor.

{
  "data": [
    {
      "id": "d1e2f3a4-b5c6-7d8e-9f0a-1b2c3d4e5f6a",
      "endpoint_id": "a3f1c2e4-5b6d-7e8f-9a0b-1c2d3e4f5a6b",
      "organization_id": "11111111-2222-3333-4444-555555555555",
      "event_type": "campaign.reply_received",
      "event_id": "e9f8a7b6-c5d4-3e2f-1a0b-9c8d7e6f5a4b",
      "status": "delivered",
      "attempt_count": 1,
      "max_attempts": 8,
      "next_attempt_at": null,
      "last_attempt_at": "2026-06-11T18:02:14Z",
      "response_status": 200,
      "response_body_excerpt": "ok",
      "error_reason": null,
      "created_at": "2026-06-11T18:02:13Z",
      "updated_at": "2026-06-11T18:02:14Z"
    }
  ],
  "pagination": {
    "next_cursor": "eyJpZCI6ImQx...",
    "has_more": true
  }
}

List deliveries for one endpoint

GET /webhooks/:id/deliveries

Returns recent delivery attempts for a specific endpoint, newest first. Each row reflects the latest state across all retry attempts for that event.

Auth: Scope WEBHOOKS · Org permission manage_settings.

ParameterInTypeDescription
idpathUUIDThe endpoint id whose deliveries to list.
statusquerystringFilter by delivery status.
event_typequerystringFilter by event type.
limitqueryintegerMax rows. Between 1 and 200, defaults to 50. Out-of-range values return 400.
cursorquerystringOpaque pagination cursor.

Response

Uses the same data + pagination envelope as the org-wide list.

Delivery status is one of pending, in_flight, delivered, failed, or abandoned (retries exhausted). response_body_excerpt is capped at the first 1024 bytes of the subscriber's response.

Redeliver a delivery

POST /webhooks/deliveries/:deliveryId/redeliver

Manually re-enqueues a delivery regardless of its current status. Useful for replaying a failed or abandoned delivery after you have fixed the receiver. The redelivery is a new attempt with its own retry lifecycle, but it carries the same event_id so your idempotency key logic will dedupe it correctly.

Auth: Scope WEBHOOKS · Org permission manage_settings.

ParameterInTypeDescription
deliveryIdpathUUIDThe delivery id to redeliver.

Response

200 OK with the updated delivery object.

List throttle drops

GET /webhooks/throttle-drops

Returns events that were suppressed by the per-endpoint delivery rate limit or the per-org per-event-type dispatch throttle. Throttled events are surfaced here rather than silently discarded, so you can monitor for integration pressure and redeliver if needed.

Auth: Scope WEBHOOKS · Org permission manage_settings.

Delivery payload

Every callback is an HTTP POST with a Content-Type: application/json body in this shape:

FieldTypeDescription
idUUIDUnique event id. Stable across retries of the same event. Matches X-Warmbly-Event-Id.
event_typestringThe event name, for example campaign.reply_received.
organization_idUUIDThe organization the event belongs to.
created_atstringRFC 3339 UTC timestamp of when the event was dispatched.
dataobjectEvent-specific payload. Shape depends on event_type.
{
  "id": "e9f8a7b6-c5d4-3e2f-1a0b-9c8d7e6f5a4b",
  "event_type": "campaign.reply_received",
  "organization_id": "11111111-2222-3333-4444-555555555555",
  "created_at": "2026-06-11T18:02:13Z",
  "data": { "...": "event-specific object" }
}

There is no version field in the payload. If an event's payload ever changes in a backward-incompatible way, it is published under a new event_type name. Existing event type names are never re-versioned in place.

Delivery headers

Each delivery carries these request headers:

HeaderDescription
X-Warmbly-SignatureHMAC-SHA256 signature in the form t=<unix>,v1=<hex>.
X-Warmbly-EventThe event type, so you can route without parsing the body.
X-Warmbly-Event-IdThe unique event id (same as the payload id). Stable across retries. Use it as your idempotency key.
User-AgentWarmbly-Webhooks/1.0.

Verifying signatures

Warmbly signs every delivery with HMAC-SHA256 using the endpoint's signing secret. The X-Warmbly-Signature header has the form t=<unix-timestamp>,v1=<hex-digest>. The signed string is the timestamp, a literal ., then the exact raw request body.

To verify:

  1. Parse t and v1 from the header.
  2. Reject the request if t is more than 5 minutes from the current wall clock. Never use a tolerance of 0 (that would break deliveries that arrive even one second late).
  3. Compute HMAC-SHA256(secret, t + "." + rawRequestBody) and hex-encode the result.
  4. Compare your digest against v1 using a constant-time comparison to avoid timing attacks.
  5. Ignore any scheme that is not v1 (downgrade defense: future schemes may exist).

Always compute the HMAC over the raw, unmodified request body before any JSON re-serialization. The secret is the whsec_-prefixed value returned at create or rotate time.

# Pseudocode
signed_payload = t + "." + raw_body
expected_v1    = hex(hmac_sha256(whsec_..., signed_payload))
is_valid       = constant_time_equal(expected_v1, v1) AND abs(now() - t) < 300

Example in Python:

import hashlib, hmac, time

def verify(secret: str, header: str, raw_body: bytes) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t, v1 = parts.get("t"), parts.get("v1")
    if not t or not v1:
        return False
    if abs(time.time() - int(t)) > 300:
        return False  # replay too old
    key = secret.encode()  # whsec_... as bytes
    signed = f"{t}.".encode() + raw_body
    expected = hmac.new(key, signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)

Use X-Warmbly-Event-Id (or the payload id field) as an idempotency key to dedupe replays. The same event id can arrive more than once if a retry fires before Warmbly receives your 2xx.

Retries and backoff

Deliveries are enqueued and drained asynchronously, so the dispatching API call returns immediately and is not blocked on your endpoint.

  • A 2xx response marks the delivery delivered and resets the endpoint's failure health.
  • Any non-2xx response (including 3xx: redirects are never followed), a connection error, or a timeout (15-second request timeout) marks the attempt failed and schedules a retry.
  • Backoff doubles each retry starting at 30 seconds: 30s, 1m, 2m, 4m, 8m, 16m, 32m. Subsequent waits are capped at 1 hour.
  • The default max_attempts is 8. Once attempts are exhausted the delivery is marked abandoned.
  • Both 4xx and 5xx responses are retried up to the attempt cap. A 4xx may be a transient validation flap.
  • Endpoints that fail continuously over a sustained window are auto-disabled to protect third parties. Re-enable them in Settings once your receiver is stable.

Make your handler idempotent. Dedupe on X-Warmbly-Event-Id since the same event id can arrive more than once across retries.

Firehose events

High-volume events (marked firehose: true in the event catalog) are opt-in only. An endpoint whose event_types list is empty or omitted subscribes to all standard events but will not receive firehose events. To receive a firehose event, list it explicitly:

{
  "event_types": [
    "campaign.reply_received",
    "campaign.email_sent",
    "campaign.email_opened"
  ]
}

This keeps default integrations from being overwhelmed by send-pipeline or inbox-sync volume. Subscribe selectively and ensure your receiver can handle the sustained throughput before opting in.

Security

  • HTTPS only: endpoint URLs must use https://. Plain HTTP is rejected at registration time.
  • SSRF protection: outbound delivery is hardened against SSRF. Private, loopback, link-local, and cloud-metadata IP ranges are blocked at dial time. Redirects are never followed. Only standard web ports are allowed.
  • Rate limits: a per-endpoint delivery rate limit and a per-org per-event-type dispatch throttle protect both your receivers and third parties. Throttled events appear in GET /webhooks/throttle-drops.
  • Signature verification: always verify the X-Warmbly-Signature header. Ignoring it means any party that knows your endpoint URL can replay or spoof events.

Event catalog

The full catalog is available at GET /webhooks/event-types. Below is the complete reference grouped by category. Events marked firehose must be subscribed to explicitly (see Firehose events).

Campaign

EventDescriptionFirehose
campaign.startedA campaign starts sending.
campaign.pausedA campaign is paused.
campaign.completedA campaign finishes all sends.
campaign.createdA campaign is created.
campaign.updatedA campaign's settings change.
campaign.deletedA campaign is deleted.
campaign.email_sentA campaign email is dispatched by a worker.yes
campaign.email_deliveredA campaign email is accepted by the recipient's mail server.yes
campaign.email_openedA tracked open is recorded.yes
campaign.email_clickedA tracked link click is recorded.yes
campaign.email_bouncedA campaign email bounces.
campaign.reply_receivedA human reply lands for a campaign thread.
campaign.unsubscribedA recipient clicks the unsubscribe link or requests removal.
campaign.deliverability_warningThe campaign's rolling bounce or complaint rate enters the early-warning band.
campaign.actionA sequence flow notify action node fires.

Warmup

EventDescriptionFirehose
warmup.email_sentA warmup message is sent.yes
warmup.health_changedA mailbox's warmup health state transitions.
warmup.placement_in_spamA warmup message is observed landing in the spam folder.
warmup.quarantinedA mailbox is quarantined from the shared warmup pool.
warmup.blockedA mailbox is blocked from the shared warmup pool.

Deliverability

EventDescriptionFirehose
deliverability.bounceA bounce signal is processed (hard or soft).
deliverability.complaintA complaint (spam report) is processed.

Inbox

EventDescriptionFirehose
inbox.email_receivedA new email arrives in a connected mailbox.yes
inbox.email_updatedAn inbox email's state changes (labels, read status).yes
inbox.email_deletedAn inbox email is deleted or expunged.yes
inbox.reply_receivedA human reply arrives on a tracked thread (distinct from campaign.reply_received).

Mailbox

EventDescriptionFirehose
email_account.connectedA mailbox is connected to the organization.
email_account.disconnectedA mailbox loses its connection (auth failure, token revoked).
email_account.removedA mailbox is removed from the organization.
email_account.errorA mailbox encounters a sync or sending error.
email_account.syncedA mailbox sync cycle completes.yes
email_account.health_changedA mailbox's health state transitions.

Contact

EventDescriptionFirehose
contact.createdA contact is created.
contact.updatedA contact's fields change.
contact.deletedA contact is deleted.

CRM

EventDescriptionFirehose
crm.deal_createdA deal is created.
crm.deal_updatedA deal's fields or stage change.
crm.deal_deletedA deal is deleted.
crm.task_createdA CRM task is created.
crm.task_updatedA CRM task changes.
crm.note_createdA note is added to a contact or deal.
crm.pipeline_updatedA pipeline or its stages change.

Automation

EventDescriptionFirehose
automation.createdAn automation is created.
automation.updatedAn automation's definition or enabled state changes.
automation.deletedAn automation is deleted.
automation.runAn automation completes a run for a contact.

Meeting

EventDescriptionFirehose
meeting.bookedA call is booked through a connected scheduler (Calendly or Cal.com).
meeting.rescheduledA booked call is rescheduled.
meeting.canceledA booked call is canceled.

Team and access

EventDescriptionFirehose
team.member_invitedA team member invitation is sent.
team.member_removedA member is removed from the organization.
role.createdA custom role is created.
role.updatedA custom role's permissions change.
role.deletedA custom role is deleted.

Bulk operations

EventDescriptionFirehose
bulk_operation.startedA bulk import, export, or deletion begins.
bulk_operation.completedA bulk operation finishes successfully.
bulk_operation.failedA bulk operation fails or is aborted.

Workspace

EventDescriptionFirehose
lead_sync_source.updatedA lead sync source (CSV import, Google Sheets) is updated.
settings.updatedOrganization settings change.
subscription.updatedThe billing subscription changes (plan, seats, status).

Developer

EventDescriptionFirehose
custom.eventA custom event emitted by an automation or campaign sequence Fire Event step.

Trigger-only (not delivered outbound)

inbound.webhook is an automation trigger for receiving inbound HTTP requests. It is never delivered outbound as a webhook event.

OAuth app-level subscriptions

OAuth apps can receive events without registering per-org endpoints manually. Set webhook_url and webhook_events on the app once, and Warmbly automatically materializes a managed endpoint for every org that authorizes the app. Events are gated by the intersection of the app's declared events and the org's granted scopes. The app signs all deliveries with a single shared whsec_ secret, verifiable with the same scheme documented in Verifying signatures.

Four management endpoints let you inspect and maintain the app-level delivery pipeline: reveal the secret, rotate it, list per-org endpoints with health, and page through the cross-org delivery log. All four require Scope API_KEYS and org permission manage_api_keys. See OAuth: receiving events as an app for the full reference, including the scope-to-event gating table and a comparison with the realtime gateway.

OAuth app webhook domains

OAuth applications that register webhook endpoints on behalf of their users must declare the domains those endpoints may use. The field is allowed_webhook_domains (a string array) on the OAuth app object.

Any webhook endpoint registered by an OAuth app must have its URL host within the app's allowed_webhook_domains list. This is enforced at registration time and re-checked at delivery time. An app with an empty allowed_webhook_domains list cannot register any webhook endpoints.

Matching rules:

  • A bare entry (acme.com) matches that exact host only.
  • A leading-dot entry (.acme.com) matches the apex domain and any subdomain: acme.com, hooks.acme.com, prod.hooks.acme.com.

Example: if allowed_webhook_domains is [".acme.com"], then https://hooks.acme.com/warmbly is allowed and https://otherdomain.com/warmbly is rejected.

Set this field when creating or updating your OAuth app:

{
  "name": "My App",
  "allowed_webhook_domains": [".acme.com"]
}

See OAuth for the full app registration flow and field reference.

Pushing custom data from a flow

To push custom data from a campaign or automation flow into your endpoints, add a "Fire event" action step: it emits a custom.event that every endpoint subscribed to it receives, signed with that endpoint's own secret and retried like any other delivery. That keeps a single, consistent verification model: one secret per endpoint, with signed and retried delivery.

Errors

Webhook endpoints use the standard error envelope with stable code and request_id fields (see error codes). Common cases:

  • 400 invalid payload, a non-HTTPS or non-routable url, an unknown event type, or a limit outside 1 to 200.
  • 400 an invalid endpoint id or delivery id in the path.
  • 403 an OAuth app attempting to register an endpoint whose URL host is not in allowed_webhook_domains.
  • 404 the endpoint or delivery does not exist or does not belong to your organization.
{
  "error": "Bad Request",
  "message": "unknown event type: campaign.exploded",
  "code": "bad_request",
  "request_id": "req_7Yc2pK1nQ4"
}

On this page