Webhooks

01Outbound webhooks

Create Custom Webhook connections in Settings → Integration → API (under Custom Webhooks).

Zapier and Make webhooks are managed on their own settings pages (Settings → Integration → Zapier / Make). They use the same events and payloads but appear in separate connection lists - not under Custom Webhooks.

You can also open webhook setup from a checkout's Delivery tab; the checkout is pre-selected under When should this fire?.

Funnels.cm POSTs JSON to your HTTPS URL with retries (exponential backoff, up to 10 attempts). Works with any backend, queue, or automation tool that accepts webhooks.

Event types

Event When it fires
form.submitted A form is submitted (live page or embed)
scheduling.booked A calendar booking is created
checkout.purchased First successful payment
checkout.rebilled Subscription or split-pay renewal
checkout.refunded Payment refunded from Customers
checkout.subscription_cancel_scheduled Triggers immediately when the buyer (or you) cancels. Access may continue until the period ends.
checkout.subscription_cancelled Triggers when the subscription is actually over and access should stop

checkout.paid is a legacy alias only - not emitted as type. Use New purchase + Renewal payment for new webhooks.

Subscription filters

Scope which resources fire a webhook. Pick items under Exact scope in the app, or send filters when creating a subscription via POST /api/v1/webhooks/subscriptions.

Filter Applies to Empty means __none__ means
formIds form.submitted All forms
eventTypeIds scheduling.booked All calendars
productIds All checkout.* All checkouts
pricingTierIds checkout.purchased, checkout.rebilled, checkout.refunded All pricing options on the selected checkout(s) Do not filter by pricing option (use with specific bumpIds for bump-only delivery)
bumpIds Same checkout events All bump offers (with or without bumps) Only purchases with no bumps selected (base plan only)

In the app, Exact scope offers Every / Only specific / None for pricing options and bumps. Examples: specific pricing + bumps None → base plan only; pricing None + specific bumps → that bump on any pricing option.

Each delivery still includes the resource id in the payload (data.formId, data.eventTypeId, data.product.id, data.pricing.pricingTierId) so you can filter in your code even when scope is set to all.

Account-specific ids

List checkouts, forms, calendars, and tags from the API (Zapier dropdowns) or look them up in the Funnels.cm app:

What you need API In the app
Checkouts GET /api/v1/products Settings → Integration → API → Custom Webhooks → Exact scope
Forms GET /api/v1/forms Custom Webhooks → Exact scope
Calendars GET /api/v1/scheduling/event-types Custom Webhooks → Exact scope
Tags GET /api/v1/tags Settings → Integration → API
Secret API key Settings → Integration → API or Connect Zapier

Envelope shape

{
  "id": "1740000000000_abc123",
  "type": "form.submitted",
  "createdAt": "2026-03-25T12:34:56.789Z",
  "data": { }
}

Headers: Content-Type: application/json · X-Funnels-Event · X-Funnels-Delivery · X-Funnels-Signature (optional HMAC-SHA256)

form.submitted payload (data)

{
  "formId": "formId123",
  "formName": "Contact Form",
  "submissionId": "subId456",
  "submittedAt": "2026-03-25T12:34:56.789Z",
  "submissionData": { "field-name": "Jane Doe", "field-email": "[email protected]" },
  "fields": [{ "id": "name", "label": "Full Name", "type": "text", "contactField": "builtin:fullName", "value": "Jane Doe" }],
  "contact": { "contactId": "contactId123", "email": "[email protected]", "firstName": "Jane", "lastName": "Doe", "phone": "", "customFields": {}, "customFieldsLabeled": [] }
}

scheduling.booked payload (data)

{
  "eventTypeId": "evt123",
  "eventTypeName": "Strategy Call",
  "bookingId": "booking456",
  "when": "2026-03-25T14:00:00.000Z",
  "meetingUrl": "https://zoom.us/j/...",
  "attendee": { "contactFields": { "builtin:email": "[email protected]" }, "answers": { "q123": "Growing my SaaS" } },
  "contact": { "contactId": "contactId123", "email": "[email protected]", "firstName": "Jane", "lastName": "Doe" }
}

Checkout payloads (data)

Shared for checkout.purchased and checkout.rebilled:

{
  "isRebill": false,
  "product": { "id": "productId123", "name": "Pro Plan", "internalName": "pro-plan" },
  "transaction": { "id": "txnDocId", "txnId": "pi_abc123", "amount": 9900, "currency": "USD", "status": "succeeded", "processor": "stripe", "subscriptionId": "sub_xyz" },
  "pricing": { "tierIndex": 0, "tierName": "Monthly", "pricingTierId": "tier_monthly", "bumpIndices": [], "bumps": [] },
  "customer": { "email": "[email protected]", "name": "Jane Doe", "contactId": "contactId123" },
  "contact": { "contactId": "contactId123", "email": "[email protected]", "firstName": "Jane", "lastName": "Doe" }
}

First purchase with SaaS Signup may include activation: { token, url, expiresAt }.

checkout.refunded adds refund: { amountCents, fullyRefunded, refundedAt }.

checkout.subscription_cancel_scheduled includes subscription: { id, status, processor, cancelAtPeriodEnd, currentPeriodEnd, scheduledAt }.

checkout.subscription_cancelled includes subscription: { id, status, processor, cancelledAt }.

Signature verification

X-Funnels-Signature = HMAC_SHA256(secret, rawRequestBody) as lowercase hex.

const crypto = require("crypto");
function verifySignature(secret, rawBody, headerHex) {
  const expected = crypto.createHmac("sha256", secret).update(rawBody, "utf8").digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(String(headerHex || "")));
}