SaaS Patterns

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):

  1. Open Custom Webhooks and add your server URL.
  2. Enable New purchase (add Renewal payment, Purchase refunded, Subscription cancelled if you need them).
  3. 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.id instead.
  4. Click Test and confirm your server receives a sample checkout.purchased payload.

On your server (when the webhook fires):

  1. Read data.customer.email and data.product.id from the JSON body.
  2. Map data.product.id to a plan or role in your database (keep this mapping in your code).
  3. Create or update the user and enable access.
  4. Optionally verify X-Funnels-Signature if 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

  1. Set checkout forwarding to SaaS Signup.
  2. On checkout.purchased, read data.activation.token or call GET /api/v1/checkout/activations/:token.
  3. Create the user in your app, then POST /api/v1/checkout/activations/:token/complete with optional externalUserId.

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

  1. Build each plan the normal way: one checkout with pricing tiers, or separate checkouts per plan (Starter / Pro / Agency).
  2. On checkout.purchased, set the user’s plan from data.product.id and data.pricing.tierIndex (or pricingTierId / tierName). Upsert by email so a second purchase upgrades access in your app.
  3. 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.
  4. 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 cancelled data.subscription.id. Use checkout.subscription_cancel_scheduled only 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.