Build usage-based billing systems for SaaS platforms in Webflow using Stripe | Webflow blog

Build usage-based billing systems for SaaS platforms in Webflow using Stripe | Webflow blog

Implement consumption-based billing in Webflow using Stripe meters, webhooks, and serverless functions.

Usage-based billing allows SaaS platforms to bill customers for actual usage instead of flat subscription fees. This guide explains the architectural patterns and implementation concepts for connecting Webflow frontends to Stripe’s metered billing infrastructure via middleware.

Webflow works as a client-side-only platform with no ability to execute code on the server. Stripe requires server-side operations for checkout sessions, customer portal sessions, and webhook processing. This limitation ensures that external middleware (such as Vercel Functions, Cloudflare employeesor AWS Lambda) required for any production deployment.

Requirements

Before deploying this integration, make sure you have the following:

  • A Stripe account with configured API keys
  • A Webflow site with access to custom code sections
  • A middleware hosting platform (Vercel, Cloudflare Workers, AWS Lambda or similar)
  • Basic knowledge of webbing And REST APIs

You need these login details:

  • Stripe publishable key (pk_test_* or pk_live_*) for frontend initialization
  • Stripe secret key (sk_test_* or sk_live_*) for server-side operations
  • Stripe webhook signing secret (whsec_*) for signature verification

Architecture overview

The integration requires a three-tier architecture because Webflow’s client-side-only environment cannot execute server-side code or securely store secret API keys.

At a high level you will:

  • Configure Webflow to load Stripe.js and handle UI events
  • Implement middleware to create sessions, handle webhooks, and store API keys
  • Set up Stripe products with metered prices and billing meters

Data flow summary:

  1. Webflow handles the UI presentation and initializes Stripe.js with publishable keys
  2. Middleware receives requests from Webflow, authenticates with Stripe using secret keys, and processes webhook events
  3. Stripe manages payment processing, hosted payment pages, and billing meter aggregation
  4. External storage tracks usage events and customer subscription status

Striped billing concepts

Stripe offers two complementary APIs for reporting usage data, each suited for different volume requirements: the Usage records API for subscription item-specific usage tracking and the higher throughput Billing Meter API for recording high-volume usage events.

At a high level you will:

  • Make one billing meter immediately event_name And default_aggregation configuration
  • Send meter events to POST /v1/billing/meter_events with customer ID and usage value
  • Configure measured prices on your subscription products

The meter event payload structure:

{
  "event_name": "api_requests",
  "payload": {
    "value": "150",
    "stripe_customer_id": "cus_NciAYcXfLnqBoz"
  },
  "timestamp": 1680210639
}

Stripe rate limits limit the frequency of API requests. Pre-collect usage events in your middleware before submitting to stay within limits.

Webflow frontend implementation

Webflow’s client environment supports Stripe.js initialization and UI event handling, but both Checkout Sessions and webhook processing require server-side implementation. See the Security Requirements section for guidelines for handling API keys.

Custom code injection allows loading Stripe.js in the main section of the entire site. Webflow supports up to 50,000 characters custom code via Site Settings, Page Settings, Code Embed elementsand CMS Rich Text fields.

At a high level you will:

  • Loading Streep.js in the main code section of Webflow
  • Initialize with your publishable key (pk_test_* or pk_live_*)
  • Add event listeners that retrieve checkout sessions from your backend middleware endpoint
  • Use the returned session ID with stripe.redirectToCheckout({ sessionId }) to forward to the Stripe-hosted checkout
>
<script>
  const stripe = Stripe('pk_test_YOUR_PUBLISHABLE_KEY');

  document.getElementById('subscribe-btn').addEventListener('click', async () => {
    const response = await fetch('https://your-middleware.com/create-checkout', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ priceId: 'price_metered_123' })
    });
    const { sessionId } = await response.json();
    await stripe.redirectToCheckout({ sessionId });
  });
script>

Handling middleware webhooks

Webhook processing requires server-side code with signature verification. Stripe signs each webhook payloadand your endpoint must verify this signature before processing events.

At a high level you will:

  • Create a publicly accessible HTTPS endpoint
  • Save the raw request body before JSON parsing (required for signature verification)
  • Check the signature with stripe.webhooks.constructEvent() with the raw body and the webhook signing secret
  • Immediately return a 2xx status and then process asynchronously

Example webhook handler (Node.js with ES modules):

import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);

export default async function handler(req, res) {
  const sig = req.headers['stripe-signature'];

  let event;
  try {
    // Raw body required for signature verification
    // Critical: Use express.raw({type: 'application/json'}) instead of express.json()
    event = stripe.webhooks.constructEvent(
      req.body,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET
    );
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  // Acknowledge immediately, process asynchronously
  res.status(200).json({ received: true });

  // Handle events asynchronously (see Stripe docs for event types)
  processEventAsync(event);
}

Critical webhook events for usage-based billing:

Event

Goal

invoice.created

Track the generation of invoices for the billing period

invoice.payment_successful

Confirm payment and grant access

customer.subscription.updated

Detect plan changes or cancellations

v1.billing.meter.error_report_triggered

Monitor errors in meter reading

Stripe retries failed webhooks for up to three days using exponential backoff. Implement idempotency in two ways: (1) take the Idempotency-Key header with unique values ​​(recommended: UUID) when making outbound API requests to Stripe, and (2) tracking processed webhook event.id values ​​in your database to avoid duplicate processing of incoming webhook events.

Usage tracking and reporting architecture

Your middleware should capture, aggregate, and report usage data to Stripe before closing each billing cycle. By default, Stripe offers a 1-hour invoice completion grace period after the billing period ends, which can be configured up to 72 hours (3 days).

Data transformation example:

Application captures raw usage events:

{
  "user_id": "user_123",
  "action": "api_call",
  "endpoint": "/search",
  "timestamp": 1704824589
}

The aggregation service combines events per customer:

{
  "customer_id": "cus_ABC123",
  "api_calls_count": 1500,
  "period_start": 1704067200,
  "period_end": 1704758400
}

Submitted to Stripe as a meter event:

{
  "event_name": "api_requests",
  "payload": {
    "value": "1500",
    "stripe_customer_id": "cus_ABC123"
  },
  "timestamp": 1704824589
}

Timestamping restrictions by Usage documentation for Stripe recordings:

Implementation of Idempotency prevents double billing during retries. According to Idempotency documentation from Stripeincluding a unique one Idempotency-Key header on each usage record submission:

await stripe.billing.meterEvents.create({
  event_name: 'api_requests',
  payload: {
    stripe_customer_id: customerId,
    value: aggregatedUsage.toString()
  }
}, {
  idempotencyKey: `${customerId}-${billingPeriodStart}-${timestamp}`
});

Idempotency keys must be generated as unique IDs (using UUIDs or deterministically based on customer ID, billing period, and event timestamp) and included with POST requests to prevent double processing. According to Idempotency documentation from Stripekeys automatically expire after 24 hours, and Stripe returns the same result for subsequent requests with identical keys within this window, preventing double billing in distributed systems.

Middleware platform options

Different platforms can serve as a middleware layer between Webflow and Stripe.

Serverless functions allow full customization of the webhook processing logic, although this requires custom code implementation.

Platform

Transit

Best for

Cloudflare employees

No overall limit on requests per second (Stripe API limits apply)

Low latency, global distribution at edge locations

AWS Lambda

Feature URLs with default rate limits

AWS ecosystem integration

Vercel functions

API routes with Git-based deployment

Modern JavaScript applications

Platforms without code visual workflow builders provide:

  • Make.com provides native Stripe webhook triggers with visual data mapping, but requires custom code modules for proper signature verification
  • Zapier uses Stripe’s API triggers instead of raw webhooks; for webhook-based integrations, external signature verification middleware is recommended

For production systems that process financial data, webhook handlers must implement explicit signature verification using platform-specific methods (Stripe’s constructEvent or constructEventAsync) and maintain idempotent processing patterns with event ID tracking, database transaction atomicity, and asynchronous event handling to ensure reliable billing operations.

Webflow CMS synchronization

Syncing Stripe subscription status with Webflow CMS enables dynamic content gating and personalization.

At a high level you will:

  • Create a CMS collection to store customer subscription information
  • Assign Stripe webhook events to Webflow CMS API operations
  • Lever rate limits when making CMS API requests

Data shape illustration:

Webhook payload from Stripe subscription (source):

{
  "id": "sub_1234567890",
  "customer": "cus_ABC123",
  "status": "active",
  "items": {
    "data": [{
      "price": { "id": "price_XYZ", "unit_amount": 2000 }
    }]
  },
  "current_period_end": 1735689600
}

Webflow CMS field structure (target):

{
  "fieldData": {
    "stripe-customer-id": "cus_ABC123",
    "subscription-status": "active",
    "plan-name": "Pro Plan",
    "renewal-date": "2025-01-31"
  }
}

Field mapping transformation:

  • subscription.customerstripe-customer-id (direct mapping)
  • subscription.statussubscription-status (direct mapping)
  • subscription.items.data[0].price.idplan-name (requires lookup table)
  • subscription.current_period_endrenewal-date (timestamp to date conversion)

Example sync pattern:

// Server-side webhook handler - calls Webflow CMS API after signature verification
if (event.type === 'customer.subscription.updated') {
  const subscription = event.data.object;

  await fetch(`https://api.webflow.com/v2/collections/${collectionId}/items`, {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.WEBFLOW_API_TOKEN}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      fieldData: {
        'stripe-customer-id': subscription.customer,
        'subscription-status': subscription.status,
        'renewal-date': new Date(subscription.current_period_end * 1000).toISOString().split('T')[0]
      }
    })
  });
}

The Webflow CMS API supports bulk operations of up to 100 items per request, enabling efficient synchronization of large customer datasets by creating, updating, and deleting operations in batches in single API calls while staying within limits rate limit restrictions.

#Build #usagebased #billing #systems #SaaS #platforms #Webflow #Stripe #Webflow #blog

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *