Integrating Stripe with Stigg: A Deep Dive into Decoupled Monetization

Stop letting hard-coded billing logic slow down your growth. Learn how to decouple pricing from billing and move faster.

Stigg and Stripe Integration

TL;DR

  • Problem: Hard-coding monetization logic directly into an application and coupling it with a billing provider like Stripe makes pricing changes slow, complex, and risky for engineering teams.
  • Solution: Stigg provides a monetization API and dedicated SDKs that integrate with your billing provider (like Stripe) and decouple the logic of "what" (plans, features, entitlements) a customer is entitled to from the "how" (collecting money, generating invoices, taxes) of billing mechanics.
  • How it Works: Using Stigg's SDKs, developers check entitlements or render a checkout UI with just a few lines of code. Stigg's platform handles the rest, automatically mapping pricing plans to Stripe and provisioning subscriptions upon payment.
  • Benefits: Business teams can change pricing in the Stigg dashboard, and the changes automatically propagate through the application, sync with Stripe, and allow for rapid experimentation without requiring a rewrite of the application's subscription or feature-gating logic.

For SaaS companies, the ability to rapidly make pricing and packaging changes is not a luxury; it is a core driver of growth. But for many engineering teams, even small pricing changes can spark a chain of complex, time-consuming, and non-core development work. When monetization logic is hard-coded directly into an application and tightly coupled with a billing provider like Stripe, even simple adjustments can become major engineering projects, creating brittle systems that stifle go-to-market agility.

The challenge lies in the conflation of two distinct domains: the logic of monetization and the mechanics of payment processing. Monetization logic defines what a customer is entitled to and how they are billed for it. This encompasses plans, features, usage-based tiers, trials, promotions, and the like. Billing mechanics, on the other hand, involve the secure collection, processing, and movement of funds, the generation of invoices, and orchestration of financial workflows like refunds and tax compliance.

In this post we’ll explore how to combine Stigg with Stripe, giving you a flexible, dynamic monetization control plane alongside a robust billing and payments engine. Stigg acts as the "monetization operating system," providing a flexible, API-driven layer for defining and managing pricing, packaging, and entitlements. Your application code interacts with Stigg's SDKs, abstracting away the underlying complexity of the billing system. Stigg, in turn, orchestrates Stripe, translating your high-level pricing models into the necessary Product, Price, and Subscription objects required to process and collect payments.

Let’s dive in!

Stigg 🤝 Stripe

Stigg is not a replacement for Stripe; it is an abstraction and orchestration layer that sits between your application and your billing provider, designed to give you maximum flexibility and control over your monetization strategy.

Stigg as the Source of Truth

Your application code should be concerned with entitlements, not billing specifics. A developer should be able to ask a simple question: "Does this customer have access to feature-x?" The answer should be a straightforward yes or no, without necessarily needing to know if the customer is on a "Pro Plan," a "30-day trial," or has a special promotional grant.

This is the problem Stigg solves. Developers integrate their application with Stigg's frontend and backend SDKs to handle all logic related to entitlements, pricing, packaging, and feature access. When a change is made in Stigg, such as creating a new plan, updating a price, or defining a new usage based metered feature, Stigg automatically propagates these changes to Stripe.

This model provides a powerful, strategic advantage: billing provider independence. While in this post we’re focusing on Stripe, Stigg's architecture is designed to be provider-agnostic, with support for other providers like Zuora as well as the ability to manage multiple billing integrations simultaneously. 

By building your monetization logic against the Stigg API, your core application code remains stable and decoupled from the specific implementation details of any single billing provider. If business requirements evolve such as a new market requiring a local billing provider, the billing provider can be swapped or augmented at the Stigg layer without requiring a rewrite of your application's feature-gating and subscription management code. This future-proofs your monetization stack, prevents billing vendor lock-in, and turns what would be a massive re-engineering effort into a simple configuration change.

Initial Setup and Environment Management

Integrating Stripe with Stigg can be done directly from the Stigg dashboard. The process involves a secure OAuth flow where you connect your Stigg account to your Stripe account and choose which account you’d like to connect to, or even create a new one. You’ll of course need an active Stripe account to make this integration work.

  1. Navigate to the Integrations > Apps section within the Stigg dashboard.
  2. Select the Stripe connector.
  3. Click "Connect with Stripe". You will be redirected to Stripe to authorize the connection.
  4. That’s it!

A best practice for any production system is separation of environments. Stigg natively supports this concept, allowing you to create multiple, isolated environments such as Development, Staging, and Production. In our example, we’ll use a staging environment within Stigg and connect to a test mode account in Stripe. This will allow us to test our implementation without affecting real world customer data.

The Glue: Entity Mapping and Metadata

The "magic" of the Stripe integration lies in how Stigg's conceptual objects are mapped to Stripe's concrete billing entities. Understanding this mapping is key for developers to build a clear mental model of the system. For example, a “Plan” in Stigg, maps to a “Product” in Stripe, and a “Customer” maps to a “Customer”, but a “Feature” in Stigg does not directly map to anything in Stripe. Check out the entity mapping table in the docs for the full breakdown.

To maintain a clear link between the two systems, Stigg employs a bi-directional metadata strategy. When an entity is synced to Stripe, the Stigg object is updated with a billingId property containing the relevant Stripe ID (e.g., cus_... for a customer, sub_... for a subscription). Conversely, the corresponding Stripe object is populated with a stiggEntityUrl in its metadata section. This allows developers and support staff to easily navigate from a customer in the Stigg UI directly to their record in the Stripe dashboard, and vice versa, which is invaluable for debugging and operational tasks.

Building the Checkout Experience

With Stripe and Stigg integrated, we can now walk through the workflow of subscribing a new customer and collecting payment. We can easily show the user our offerings using the Stigg Pricing table widget. The pricing table widget is mapped to our product offerings in Stigg, so any change is automatically reflected in the pricing table. With a plan selected, Stigg offers various checkout solutions to balance speed of implementation with customization:

  • Stigg Checkout Widget: A low-code, embeddable component that is highly customizable via CSS and a no-code designer. It automatically reflects all pricing models configured in Stigg, including complex ones like tiered and pay-as-you-go pricing. This is the fastest way to get a fully-featured checkout running.
  • Stripe-hosted Checkout: Redirects the user to a checkout page hosted by Stripe. This is also a low-effort integration but offers very limited customization. 
  • Custom Checkout with Stripe Elements: Involves building your own checkout form using Stripe's flexible UI components. This approach offers maximum control over the user experience and brand consistency. It is the most development-intensive option but provides the greatest flexibility.

For this post, we will focus on the Stigg Checkout Widget. We will use a Next.js application for our code examples.

Stigg Checkout Widget

The Stigg Checkout widget is engineered to be embedded directly into a company's application UI, whether on a dedicated page, within a modal, or within an existing user interface, eliminating the need to redirect the user to an external domain for payment.

A user going through the Stigg Checkout widget can choose their plan, any add-ons, and checkout all in one go. With Stripe integrated, the Stigg Checkout Widget will be able to collect the payment information and update the users entitlements as soon as it receives a success message. The simplified code to make this happen looks like:

import { StiggProvider, Paywall, Checkout, useStiggContext } from '@stigg/react-sdk';

export default function Pricing() {
  const [selectedPlan, setSelectedPlan] = useState<any>(null);

  const handlePlanSelected = async ({ plan, customer }: { plan: any; customer: any }) => {
    // Use Stigg Checkout widget, passing in the user selected plan
    setSelectedPlan(plan);
  };

  const handleCheckoutCompleted = async () => {
    setSelectedPlan(null);
    // Handle what happens such as redirecting the user to the app, showing a success message, etc.
  };
  
  return (
    <StiggProvider 
      apiKey={process.env.NEXT_PUBLIC_STIGG_CLIENT_API_KEY} 
      customerId={user.id}
    >
      <Paywall
        productId="product-stigg"
        onPlanSelected={handlePlanSelected}
      />

      {selectedPlan && (
      <Checkout
        planId={selectedPlan.refId || selectedPlan.id}
        onCheckoutCompleted={handleCheckoutCompleted}
        />
      )}
    </StiggProvider>
)

It is important to note that Stigg itself does not store any of this sensitive payment data. It is passed directly to and stored within the integrated billing provider (e.g., Stripe), ensuring that PCI compliance is handled by the specialized billing processor.

This fully integrated approach presents a strategic trade-off. By keeping the user within the application's environment, the Stigg Checkout Widget avoids the jarring user journey disruption of a redirect. Some users report that being sent to an external domain can feel "weird" or "sketchy," breaking the flow and brand consistency. With Stigg, you don’t have to choose though, as redirecting a user to a Stripe hosted checkout page can be done just as easily.

Stripe Hosted Checkout 

Stripe Hosted Checkout offers a different but equally compelling value proposition. It is a mature, battle-tested solution that prioritizes security, global reach, and brings a familiar experience that many users are already familiar with. With the same Stripe integration, we can opt to redirect the user to the Stripe hosted checkout page. This does require a little bit more work, but based on your needs could be the better solution. We could update our existing code from above to:

'use client';

import { useAuth } from '@/contexts/auth-context';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { StiggProvider, Paywall, Checkout, useStiggContext } from '@stigg/react-sdk';

export default function HomePage() {
  const { user, loading, logout } = useAuth();
  const router = useRouter();
  const [selectedPlan, setSelectedPlan] = useState<any>(null);
  const [useStripeCheckout, setUseStripeCheckout] = useState(false);

  useEffect(() => {
    if (!loading && !user) {
      router.push('/login');
    }
  }, [user, loading, router]);

  if (loading || !user) {
    return <div className="min-h-screen flex items-center justify-center text-gray-500">Loading...</div>;
  }

  const handlePlanSelected = async ({ plan, customer }: { plan: any; customer: any }) => {
      try {
        const response = await fetch('/api/stripe/create-checkout-session', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json',
          },
          credentials: 'include',
          body: JSON.stringify({
            planId: plan.refId || plan.id,
            planData: plan, // Send the complete plan data including pricePoints
          }),
        });

        if (response.ok) {
          const { checkoutUrl } = await response.json();

          // Redirect to Stripe Checkout
          window.location.href = checkoutUrl;
        } else {
          console.error('Failed to create checkout session');
        }
      } catch (error) {
        console.error('Error creating checkout session:', error);
      }
  };

  return (
    <StiggProvider 
      apiKey={process.env.NEXT_PUBLIC_STIGG_CLIENT_API_KEY || ''} 
      customerId={user.id}
    >
      <Paywall
        productId="product-stigg"
        onPlanSelected={handlePlanSelected}
       />
    </StiggProvider>
  );
}

As you can see, we’ll also need to implement an API endpoint that will generate the data for our Stripe Checkout page. That code might look something like:

import { NextRequest, NextResponse } from 'next/server';
import { getCurrentUser } from '@/lib/auth';
import Stripe from 'stripe';

export async function POST(request: NextRequest) {
  try {
    const user = await getCurrentUser();
    
    if (!user) {
      return NextResponse.json(
        { error: 'Not authenticated' },
        { status: 401 }
      );
    }

    const { planId, planData } = await request.json();

    if (!planId) {
      return NextResponse.json(
        { error: 'Plan ID is required' },
        { status: 400 }
      );
    }

    console.log('Received plan data:', JSON.stringify(planData, null, 2));

    const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
      apiVersion: '2025-06-30.basil',
    });

    // Extract pricing information from Stigg plan data
    let lineItem: Stripe.Checkout.SessionCreateParams.LineItem;
    
    // Check if we have a Stripe Price ID in the plan data
    const stripePriceId = planData?.metadata?.stripePriceId || planData?.stripeConfig?.priceId;
    
    if (stripePriceId) {
      // Use existing Stripe Price ID from Stigg configuration
      lineItem = {
        price: stripePriceId,
        quantity: 1,
      };
    } else {
      // Fallback if no pricing data is available
      return NextResponse.json(
        { error: 'No pricing information found for this plan' },
        { status: 400 }
      );
    }

    const session = await stripe.checkout.sessions.create({
      customer_email: user.email,
      line_items: [lineItem],
      mode: 'subscription',
      success_url: `${process.env.NEXT_PUBLIC_DOMAIN || 'http://localhost:3000'}/success?session_id={CHECKOUT_SESSION_ID}`,
      cancel_url: `${process.env.NEXT_PUBLIC_DOMAIN || 'http://localhost:3000'}`,
      metadata: {
        planId: planId,
        userId: user.id,
        planName: planData?.displayName || planData?.name || planId,
        source: 'stigg-integration',
      },
    });

    return NextResponse.json({
      checkoutUrl: session.url,
      sessionId: session.id
    });
    

  } catch (error) {
    return NextResponse.json(
      { error: 'Failed to create checkout session' },
      { status: 500 }
    );
  }
}

With this code, we should now be redirected to a Stripe hosted checkout page when we select a plan from the Stigg Pricing Widget. Clicking on the “Upgrade” button now will take us to a page that looks like this:

In addition to creating an API endpoint to generate the Stripe hosted checkout page, you’ll also need to create an API endpoint to handle the success or failure of the checkout operation. If everything went well, you’ll want to update the customer and provision their subscription in Stigg. It may look something like:


import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
import { getUserById } from '@/lib/auth';
import { getStiggServerClient } from '@/lib/stigg-server';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: '2025-06-30.basil',
});

const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;

export async function POST(request: NextRequest) {
  const body = await request.text();
  const signature = request.headers.get('stripe-signature');

  let event: Stripe.Event;

  try {
    if (!endpointSecret) {
      console.warn('STRIPE_WEBHOOK_SECRET not configured - skipping signature verification');
      event = JSON.parse(body);
    } else {
      event = stripe.webhooks.constructEvent(body, signature!, endpointSecret);
    }
  } catch (err) {
    console.error('Webhook signature verification failed:', err);
    return NextResponse.json(
      { error: 'Webhook signature verification failed' },
      { status: 400 }
    );
  }

  console.log('Received webhook event:', event.type);

  try {
    switch (event.type) {
      case 'checkout.session.completed':
        await handleCheckoutSessionCompleted(event.data.object as Stripe.Checkout.Session);
        break;
      case 'customer.subscription.updated':
        await handleSubscriptionUpdated(event.data.object as Stripe.Subscription);
        break;
      default:
        console.log(`Unhandled event type: ${event.type}`);
    }

    return NextResponse.json({ received: true });
  } catch (error) {
    console.error('Webhook handler error:', error);
    return NextResponse.json(
      { error: 'Webhook handler failed' },
      { status: 500 }
    );
  }
}

async function handleCheckoutSessionCompleted(session: Stripe.Checkout.Session) {
  console.log('Processing checkout.session.completed:', session.id);
  
  const userId = session.metadata?.userId;
  const planId = session.metadata?.planId;
  const planName = session.metadata?.planName;
  
  if (!userId || !planId) {
    console.error('Missing userId or planId in session metadata:', session.metadata);
    return;
  }

  // Get user from our system
  const user = getUserById(userId);
  if (!user) {
    console.error('User not found:', userId);
    return;
  }

  // Get Stigg client
  const stiggClient = getStiggServerClient();
  if (!stiggClient) {
    console.error('Stigg client not initialized');
    return;
  }

  try {
    // Get the subscription from Stripe to get more details
    if (session.subscription) {
      const subscription = await stripe.subscriptions.retrieve(session.subscription as string);
      
      // Update the customer's subscription in Stigg by re-provisioning with the new plan
      await stiggClient.provisionCustomer({
        customerId: userId,
        email: user.email,
        name: user.name,
        subscriptionParams: {
          planId: planId,
        },
        metadata: {
          stripeSubscriptionId: subscription.id,
          stripeCustomerId: typeof subscription.customer === 'string' ? subscription.customer : '',
          planName: planName || '',
          source: 'stripe-webhook'
        }
      });

      console.log(`Successfully updated subscription for user ${userId} to plan ${planId}`);
    } else {
      console.warn('No subscription found in checkout session:', session.id);
    }
  } catch (error) {
    console.error('Failed to update subscription in Stigg:', error);
    throw error;
  }
}

As you can see, the Stripe hosted checkout page is a little more involved than the Stigg Checkout Widget, but depending on your requirements may be the right solution.

Conclusion: Future-Proofing Your Monetization Stack

In this post, we saw how combining Stigg's flexible monetization operating system with Stripe's robust billing infrastructure creates a powerful, modern monetization stack. Stigg acts as the central source of truth for your pricing models, seamlessly orchestrating the necessary entities in Stripe and keeping your application code clean and decoupled. We walked through two different checkout implementations: the embeddable Stigg Checkout Widget, which offers a frictionless, in-app user journey with minimal code, and the more traditional Stripe-hosted checkout, which provides a familiar but redirected experience. Each path offers a trade-off between customization and development effort, but both lead to the same powerful outcome: a system where pricing can be iterated upon rapidly and safely. By separating the "what" and "how" of monetization, you give your engineering teams the ability to focus on core product offerings while your business gains the agility it demands to win in a competitive SaaS landscape.

If you’d like to learn more about how Stigg and Stripe work together, book a demo.