01SaaS integration patterns
Grant access when someone buys a specific checkout
This is the most common custom-backend flow. Setup is split between the Funnels.cm app (webhook + scope) and your server (grant access).
In Funnels.cm (Settings → Integration → API):
- Open Custom Webhooks and add your server URL.
- Enable New purchase (add Renewal payment, Purchase refunded, Subscription cancelled if you need them).
- Under Exact scope → Which checkouts / products?, select only the checkout(s) that should trigger this integration. Leave on All if your code will branch on
data.product.idinstead. - Click Test and confirm your server receives a sample
checkout.purchasedpayload.
On your server (when the webhook fires):
- Read
data.customer.emailanddata.product.idfrom the JSON body. - Map
data.product.idto a plan or role in your database (keep this mapping in your code). - Create or update the user and enable access.
- Optionally verify
X-Funnels-Signatureif you set a signing secret on the webhook.
// Example handler (pseudo-code)
app.post("/webhooks/funnels", (req, res) => {
const { type, data } = req.body;
if (type !== "checkout.purchased") return res.sendStatus(200);
const email = data.customer.email;
const productId = data.product.id;
const plan = {
"productId_pro": "pro",
"productId_starter": "starter",
}[productId];
if (!plan) return res.sendStatus(200);
grantAccess(email, plan);
res.sendStatus(200);
});
Use the checkout id from Exact scope in the app - it matches data.product.id in live deliveries.
SaaS signup after purchase
- Set checkout forwarding to SaaS Signup.
- On
checkout.purchased, readdata.activation.tokenor callGET /api/v1/checkout/activations/:token. - Create the user in your app, then
POST /api/v1/checkout/activations/:token/completewith optionalexternalUserId.
Get activation - GET /api/v1/checkout/activations/:token
{
"success": true,
"activation": {
"status": "pending",
"email": "[email protected]",
"product": { "id": "productId", "name": "Pro Plan" },
"expiresAt": "2026-04-01T12:00:00.000Z",
"activationUrl": "https://your-app.com/signup?token=..."
}
}
Complete - POST /api/v1/checkout/activations/:token/complete · { "externalUserId": "your-user-id" } · Tokens expire after 7 days.
Provision on purchase
Same as Grant access when someone buys a specific checkout above. On checkout.purchased: read data.customer.email + data.product.id → upsert user → map product id to plan tier. Prefer data.pricing.tierIndex / data.pricing.pricingTierId when one checkout has multiple tiers.
Manage plans and tiers (upgrade / downgrade)
There is no “change plan on an existing subscription” API today. Funnels.cm checkouts create subscriptions; your SaaS maps purchases to entitlements via webhooks.
Recommended pattern: cancel & rebuy
- Build each plan the normal way: one checkout with pricing tiers, or separate checkouts per plan (Starter / Pro / Agency).
- On
checkout.purchased, set the user’s plan fromdata.product.idanddata.pricing.tierIndex(orpricingTierId/tierName). Upsert by email so a second purchase upgrades access in your app. - To change billing to the new plan, cancel the old subscription (customer billing portal, Funnels.cm Customers UI, or Stripe), then have them complete the higher/lower checkout — or buy first, then cancel the old sub so they are not double-billed.
- On
checkout.subscription_cancelled, do not blindly revoke if the customer just upgraded: if they already have a newer active purchase for a higher (or different) plan, keep that entitlement and only drop access tied to the cancelleddata.subscription.id. Usecheckout.subscription_cancel_scheduledonly for soft flags or win-back before access ends (it triggers immediately when cancel is clicked).
// Pseudo-code: treat a new purchase as the source of truth for plan
app.post("/webhooks/funnels", (req, res) => {
const { type, data } = req.body;
const email = data.customer?.email;
if (!email) return res.sendStatus(200);
if (type === "checkout.purchased") {
upsertPlan(email, {
productId: data.product.id,
tierIndex: data.pricing?.tierIndex,
pricingTierId: data.pricing?.pricingTierId,
subscriptionId: data.transaction?.subscriptionId,
});
return res.sendStatus(200);
}
if (type === "checkout.subscription_cancelled") {
// Only revoke if this subscription is still their current plan in your DB
revokeIfCurrentSubscription(email, data.subscription?.id);
return res.sendStatus(200);
}
res.sendStatus(200);
});
Store on each purchase: subscriptionId, product.id, and tier fields so cancels and upgrades stay unambiguous.
Not supported via API: mid-subscription price swap, proration preview, or “move sub X to tier Y.” Use cancel & rebuy (or change the subscription directly in Stripe outside Funnels.cm).
Extend on rebill
On checkout.rebilled: extend subscription period or log renewal. Match the user with data.customer.email and prefer data.transaction.subscriptionId when they may have switched plans.
Revoke on cancel or refund
| Event | Action |
|---|---|
checkout.subscription_cancel_scheduled |
Optional soft flag / win-back. Do not revoke yet. Access usually continues until the period ends. |
checkout.subscription_cancelled |
Downgrade or disable access for that subscription. See Manage plans and tiers if the buyer may have upgraded. |
checkout.refunded |
Revoke access for that purchase |
Sync form data to your database
On form.submitted: sync data.contact and data.fields.
Push leads into Funnels.cm
POST /api/v1/contacts with email plus tagNames or tagIds - tag automations run (emails, CRM moves, campaigns). Prefer tagIds when tag names might change.