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:
- Stripe connection — link your Stripe account to AuthGate
- Product catalog — create Free, Pro, and Enterprise plans with monthly and yearly prices
- Pricing page — render a
<PricingTable />so users can pick a plan - Checkout — collect payment details with
<CheckoutForm />and create a subscription - Feature gating — show or hide features based on the user's active plan
- 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/reactor@auth-gate/nextjsSDK installed in your app
This tutorial uses the React SDK for code examples. The Next.js SDK works the same way — just import from @auth-gate/nextjs instead. Both SDKs share identical billing components and hooks.
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.
AuthGate manages subscriptions internally, so it only needs a small set of Stripe events. You do not need to forward subscription or invoice events — AuthGate handles those itself.
1c. Enter keys in AuthGate
- Open your project in the AuthGate dashboard
- Click the Billing tab in the sidebar
- Enter your Stripe secret key, publishable key, and webhook signing secret
- Choose your currency (defaults to USD)
- 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:
| Product | Description | Features |
|---|---|---|
| Free | Get started at no cost | basic_access |
| Pro | For growing teams | basic_access, advanced_analytics, priority_support |
| Enterprise | Unlimited everything | basic_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:
| Product | Price name | Amount | Interval |
|---|---|---|---|
| Free | Free Monthly | $0 | Monthly |
| Pro | Pro Monthly | $29 | Monthly |
| Pro | Pro Yearly | $290 | Yearly |
| Enterprise | Enterprise Monthly | $99 | Monthly |
| Enterprise | Enterprise Yearly | $990 | Yearly |
Amounts are entered in cents in the API but the dashboard accepts dollar values. When you type 29, AuthGate stores 2900 cents internally. The yearly prices above give users roughly two months free compared to paying monthly — a common SaaS pricing pattern.
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:
- Creates a Stripe
SetupIntentviaPOST /api/proxy/billing/setup-intent - Confirms the card using Stripe.js (card details never touch your server)
- Calls
POST /api/proxy/billing/subscribewith thepriceIdandpaymentMethodId - AuthGate creates the subscription, charges the card, and returns the subscription object
onSuccessfires with the active subscription
The user's card is saved for future renewals. AuthGate handles all renewal charges, retries, and dunning automatically.
In Stripe test mode, use the card number 4242 4242 4242 4242 with any future expiry and any CVC. This always succeeds. See Stripe testing docs for cards that simulate failures.
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:
| Event | When it fires |
|---|---|
subscription.created | A new subscription is created |
subscription.canceled | A subscription is canceled |
subscription.updated | A plan change, pause, or resume occurs |
invoice.paid | A payment succeeds (initial or renewal) |
payment.failed | A 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:
| Field | Value |
|---|---|
| Card number | 4242 4242 4242 4242 |
| Expiry | Any future date (e.g. 12/30) |
| CVC | Any 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:
- Get live Stripe keys — In the Stripe dashboard, toggle off Test mode and copy your live
sk_live_andpk_live_keys - Create a live webhook endpoint — Same URL as before, but created in Stripe's live mode
- Update AuthGate — Go to Billing → Setup, disconnect the test connection, and reconnect with live keys
- Verify currency — Make sure the currency matches your Stripe account's settlement currency
Currency cannot be changed after the first live subscription is created. Double-check this before your first real customer subscribes.
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