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.paidis a legacy alias only - not emitted astype. 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 || "")));
}