Billing Tutorial

A hands-on walkthrough for adding subscription billing to your app with AuthGate and Stripe. By the end you will have a working pricing page, checkout flow, and feature gating — all backed by real Stripe test-mode payments.


What you will build

This tutorial takes you from zero to a complete billing integration:

  1. Stripe connection — link your Stripe account to AuthGate
  2. Product catalog — create Free, Pro, and Enterprise plans with monthly and yearly prices
  3. Pricing page — render a <PricingTable /> so users can pick a plan
  4. Checkout — collect payment details with <CheckoutForm /> and create a subscription
  5. Feature gating — show or hide features based on the user's active plan
  6. Webhooks — react to billing events like successful payments and cancellations

Everything runs in Stripe test mode, so no real money changes hands while you develop.


Prerequisites

Before you start, make sure you have:

  • An AuthGate project with authentication already working (see Quickstart if you haven't set this up yet)
  • A Stripe account — the free tier is fine
  • The @auth-gate/react or @auth-gate/nextjs SDK installed in your app

Step 1: Connect Stripe

AuthGate needs your Stripe API keys to process payments on your behalf. You will create a webhook endpoint in Stripe, then enter all three keys in the AuthGate dashboard.

1a. Get your API keys from Stripe

Open the Stripe Dashboard and copy:

  • Secret key — starts with sk_test_
  • Publishable key — starts with pk_test_

Make sure the toggle in the top-right of the Stripe dashboard says Test mode. You will switch to live keys later.

1b. Create a Stripe webhook endpoint

Stripe needs to tell AuthGate when payments succeed or fail. In the Stripe Webhooks page, click Add endpoint and set:

  • Endpoint URL: https://your-authgate-instance.com/api/webhooks/stripe
  • Events to send: payment_intent.succeeded, payment_intent.payment_failed, payment_method.updated

After saving, Stripe reveals the Signing secret (whsec_...). Copy it — you need it next.

1c. Enter keys in AuthGate

  1. Open your project in the AuthGate dashboard
  2. Click the Billing tab in the sidebar
  3. Enter your Stripe secret key, publishable key, and webhook signing secret
  4. Choose your currency (defaults to USD)
  5. Click Connect

AuthGate validates your secret key with a test API call. If everything checks out, you will see a green Connected badge with the key suffix and connection date.


Step 2: Create your first product

Products represent the plans users can subscribe to. Each product has one or more prices that define billing intervals and amounts.

2a. Create products in the dashboard

Navigate to Billing → Products and click New Product. Create three plans:

ProductDescriptionFeatures
FreeGet started at no costbasic_access
ProFor growing teamsbasic_access, advanced_analytics, priority_support
EnterpriseUnlimited everythingbasic_access, advanced_analytics, priority_support, custom_integrations, sla

The Features field is a comma-separated list of feature flags. You will use these later to gate access in your app.

2b. Add prices to each product

Click a product row to open the prices panel, then click Add Price. Create prices for each plan:

ProductPrice nameAmountInterval
FreeFree Monthly$0Monthly
ProPro Monthly$29Monthly
ProPro Yearly$290Yearly
EnterpriseEnterprise Monthly$99Monthly
EnterpriseEnterprise Yearly$990Yearly

2c. Verify your catalog

Your Products page should now show three products, each with its price(s) listed. The Active badge confirms they are available for subscription.


Step 3: Add pricing to your app

AuthGate provides a drop-in <PricingTable /> component that fetches your product catalog and renders it as an interactive pricing page.

Using the React SDK

src/pages/pricing.tsx

import { PricingTable } from "@auth-gate/react";

export default function PricingPage() {
  return (
    <div className="max-w-5xl mx-auto py-16 px-4">
      <h1 className="text-4xl font-bold text-center mb-4">
        Simple, transparent pricing
      </h1>
      <p className="text-center text-gray-500 mb-12">
        Start free. Upgrade when you are ready.
      </p>

      <PricingTable
        onSelectPlan={(priceId) => {
          // Navigate to checkout with the selected price
          window.location.href = `/checkout?price=${priceId}`;
        }}
      />
    </div>
  );
}

The component automatically fetches products and prices from GET /api/proxy/billing/products, renders them in a responsive grid, and handles monthly/yearly toggle switching. When a user clicks a plan, onSelectPlan fires with the selected priceId.

Using the Next.js SDK

src/app/pricing/page.tsx

import { PricingTable } from "@auth-gate/nextjs/components";

export default function PricingPage() {
  return (
    <div className="max-w-5xl mx-auto py-16 px-4">
      <h1 className="text-4xl font-bold text-center mb-4">
        Simple, transparent pricing
      </h1>
      <p className="text-center text-gray-500 mb-12">
        Start free. Upgrade when you are ready.
      </p>

      <PricingTable />
    </div>
  );
}

Step 4: Handle checkout

When a user selects a plan, you need to collect their payment details and create a subscription. The <CheckoutForm /> component handles card collection via Stripe Elements, then calls AuthGate to start the subscription.

4a. Build the checkout page

src/pages/checkout.tsx

import { CheckoutForm } from "@auth-gate/react";
import { useSearchParams, useNavigate } from "react-router";

export default function CheckoutPage() {
  const [params] = useSearchParams();
  const navigate = useNavigate();
  const priceId = params.get("price");

  if (!priceId) return <p>No plan selected.</p>;

  return (
    <div className="max-w-md mx-auto py-16 px-4">
      <h1 className="text-2xl font-bold mb-6">Complete your subscription</h1>

      <CheckoutForm
        priceId={priceId}
        onSuccess={(subscription) => {
          // Subscription created! Redirect to the dashboard.
          navigate("/dashboard");
        }}
        onError={(error) => {
          console.error("Checkout failed:", error);
        }}
      />
    </div>
  );
}

4b. What happens under the hood

When the user submits their card details, the <CheckoutForm /> component:

  1. Creates a Stripe SetupIntent via POST /api/proxy/billing/setup-intent
  2. Confirms the card using Stripe.js (card details never touch your server)
  3. Calls POST /api/proxy/billing/subscribe with the priceId and paymentMethodId
  4. AuthGate creates the subscription, charges the card, and returns the subscription object
  5. onSuccess fires with the active subscription

The user's card is saved for future renewals. AuthGate handles all renewal charges, retries, and dunning automatically.


Step 5: Gate features by plan

Now that users can subscribe, you need to show or hide features based on their plan. AuthGate provides two approaches: a component wrapper and a hook.

Option A: The <BillingGate> component

Wrap any UI that should only appear for certain plans or features:

src/components/analytics-panel.tsx

import { BillingGate } from "@auth-gate/react";
import { billing } from "../authgate.billing";

export function AnalyticsPanel() {
  return (
    <BillingGate
      feature={billing.features.advanced_analytics}
      fallback={
        <div className="rounded-lg border border-dashed p-8 text-center">
          <p className="text-gray-500">Advanced analytics is available on the Pro plan.</p>
          <a href="/pricing" className="text-blue-600 underline mt-2 inline-block">
            Upgrade now
          </a>
        </div>
      }
    >
      {/* This only renders when the user has the advanced_analytics feature */}
      <div>
        <h2 className="text-xl font-bold mb-4">Analytics Dashboard</h2>
        {/* ... your analytics UI ... */}
      </div>
    </BillingGate>
  );
}

Option B: The useEntitlements hook

For more control, use the hook directly:

src/components/feature-list.tsx

import { useEntitlements } from "@auth-gate/react";

export function FeatureList() {
  const { entitlements, loading } = useEntitlements();

  if (loading) return <p>Loading...</p>;

  const hasPriority = entitlements?.features.includes("priority_support");
  const hasSla = entitlements?.features.includes("sla");

  return (
    <ul>
      <li>Basic access</li>
      {hasPriority && <li>Priority support</li>}
      {hasSla && <li>99.9% SLA guarantee</li>}
    </ul>
  );
}

Server-side gating with Next.js

For API routes or server components, use the Next.js helper:

src/lib/billing.ts

import { createBillingHelpers } from "@auth-gate/nextjs";

export const billing = createBillingHelpers({
  baseUrl: process.env.AUTHGATE_URL!,
  apiKey: process.env.AUTHGATE_API_KEY!,
});

src/app/api/reports/route.ts

import { billing } from "@/lib/billing";
import { billing as billingConfig } from "../../authgate.billing";
import { getSession } from "@/lib/auth";
import { NextResponse } from "next/server";

export async function GET() {
  const session = await getSession();
  if (!session) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });

  const hasAccess = await billing.checkEntitlement({
    userId: session.userId,
    projectId: process.env.AUTHGATE_PROJECT_ID!,
    feature: billingConfig.features.advanced_analytics,  // type-safe — no .key needed
  });

  if (!hasAccess) {
    return NextResponse.json({ error: "Upgrade to Pro to access reports" }, { status: 403 });
  }

  // ... generate and return the report
}

Step 6: Listen to billing events

AuthGate fires webhooks to your application when billing state changes. This lets you sync subscription data, send transactional emails, or update external systems.

6a. Configure a webhook endpoint

In the AuthGate dashboard, go to Billing → Webhooks and click Add Endpoint. Set the URL to your app's webhook handler:

https://your-app.com/api/billing/webhook

Select the events you care about. The most common ones are:

EventWhen it fires
subscription.createdA new subscription is created
subscription.canceledA subscription is canceled
subscription.updatedA plan change, pause, or resume occurs
invoice.paidA payment succeeds (initial or renewal)
payment.failedA payment attempt fails

After creating the endpoint, AuthGate shows the signing secret once. Copy it and store it in your environment variables.

6b. Build the webhook handler

src/app/api/billing/webhook/route.ts

import crypto from "node:crypto";
import { NextResponse } from "next/server";

const WEBHOOK_SECRET = process.env.AUTHGATE_BILLING_WEBHOOK_SECRET!;

export async function POST(request: Request) {
  const body = await request.text();
  const signature = request.headers.get("x-authgate-signature");
  const timestamp = request.headers.get("x-authgate-timestamp");

  if (!signature || !timestamp) {
    return NextResponse.json({ error: "Missing signature" }, { status: 400 });
  }

  // Verify the HMAC-SHA256 signature
  const signedPayload = `${timestamp}.${body}`;
  const expected = `v1=${crypto
    .createHmac("sha256", WEBHOOK_SECRET)
    .update(signedPayload)
    .digest("hex")}`;

  if (signature !== expected) {
    return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
  }

  // Reject events older than 5 minutes to prevent replay attacks
  const age = Date.now() - parseInt(timestamp, 10);
  if (age > 5 * 60 * 1000) {
    return NextResponse.json({ error: "Event too old" }, { status: 400 });
  }

  const event = JSON.parse(body);

  switch (event.type) {
    case "subscription.created":
      console.log("New subscription:", event.subscriptionId);
      // e.g. send a welcome email, provision resources
      break;

    case "invoice.paid":
      console.log("Payment received:", event.amount, event.currency);
      // e.g. update your CRM, extend access
      break;

    case "payment.failed":
      console.log("Payment failed for subscription:", event.subscriptionId);
      // e.g. send a dunning email, show an in-app banner
      break;

    case "subscription.canceled":
      console.log("Subscription canceled:", event.subscriptionId);
      // e.g. revoke access, send a win-back email
      break;
  }

  return NextResponse.json({ received: true });
}

6c. Test the webhook

Back in Billing → Webhooks, click the Test button next to your endpoint. AuthGate sends a signed test event and shows whether your handler responded with a 200 status.


Step 7: Test the full flow

Now let's walk through the complete billing experience as a user would see it.

7a. Visit your pricing page

Open your app's pricing page. You should see the three plans with their prices. Toggle between monthly and yearly to see the different amounts.

7b. Select a plan and check out

Click on the Pro plan. You should be redirected to the checkout page with the <CheckoutForm />. Enter the Stripe test card:

FieldValue
Card number4242 4242 4242 4242
ExpiryAny future date (e.g. 12/30)
CVCAny 3 digits (e.g. 123)

Click Subscribe. If everything works, you will be redirected to your dashboard.

7c. Verify in the AuthGate dashboard

Open Billing → Subscriptions in the AuthGate dashboard. You should see the new subscription with status active, the correct plan, and a billing period.

Click the subscription row to see payment history, including the invoice for the first charge.

7d. Test feature gating

Navigate to a feature protected by <BillingGate> or useEntitlements. It should now be visible since you are subscribed to Pro.

7e. Test a plan change

Use the <SubscriptionManager /> component or call the API directly to upgrade to Enterprise:

curl -X POST https://your-authgate-instance.com/api/proxy/billing/subscription/change \
  -H "Authorization: Bearer <session_token>" \
  -H "Content-Type: application/json" \
  -d '{ "new_price_id": "<enterprise_monthly_price_id>" }'

Check the Subscriptions page again — you should see the plan updated and a prorated invoice for the price difference.


Going live

When you are ready to accept real payments, switch from test to live keys:

  1. Get live Stripe keys — In the Stripe dashboard, toggle off Test mode and copy your live sk_live_ and pk_live_ keys
  2. Create a live webhook endpoint — Same URL as before, but created in Stripe's live mode
  3. Update AuthGate — Go to Billing → Setup, disconnect the test connection, and reconnect with live keys
  4. Verify currency — Make sure the currency matches your Stripe account's settlement currency

Go-live checklist

  • Live Stripe keys entered and validated
  • Live Stripe webhook endpoint created and pointing to your AuthGate instance
  • Webhook handler deployed and responding to test events
  • Pricing page renders correct live prices
  • Checkout completes successfully with a real card
  • Feature gating works for all plans
  • Cancel, pause, and resume flows tested
  • Webhook handler processes all critical event types

Next steps

You now have a working billing integration. Here's where to go from here:

  • Usage-Based Billing — Add metered pricing for API calls, storage, or any trackable metric
  • Coupons & Promo Codes — Create discounts for launches, partnerships, or win-back campaigns
  • SDK Components — Explore all available React components including <SubscriptionManager /> for self-service plan management
  • Entitlements — Deep dive into server-side and client-side feature gating patterns
  • Webhooks — Full reference for all billing event types and payload shapes
  • API Endpoints — REST API reference for building custom billing UIs

Was this page helpful?