Integration Guide

Step-by-step setup for any Next.js, Node, CLI, or browser extension project.

No Stripe SDK neededNo subscriptions tableWorks from any runtime
How it works
SparkPay owns the full Stripe integration. Your app queries subscription status via a simple API call. Email is the universal key across the entire flow. No mapping, no syncing, no local database required.

Credentials

Two values provided by the operator out-of-band.

ValueEnv varDescription
app_id
NEXT_PUBLIC_SPARK_APP_IDPublic app identifier
Webhook secret
SPARK_PAY_WEBHOOK_SECRETHMAC signature verification (push mode only)

Payment Flow

End-to-end flow from your app to Stripe and back.

1

User clicks "Upgrade" in your app

2

Redirect to sparkpay.dev pricing page with email & return_url

3

User picks plan, pays on Stripe Checkout

4

SparkPay records subscription in its database

5

(Push mode) SparkPay fires outgoing webhook to your app

6

User redirected to your return_url

7

Your success page polls status until active

8

Gate features via status endpoint

Email is the universal key
The email your app knows (NextAuth, Firebase, Supabase, chrome.identity, CLI config) is the same email SparkPay uses. No mapping or syncing required.

Step 0: Define Your Plans

Configure plans in the dashboard or via API. Each plan becomes a card on your pricing page.

FieldTypeDescription
namestringDisplay name on the pricing card
tierstringURL-safe slug (auto-generated from name)
payment_typestringsubscription | one_time | free | contact
featuresstring[]Bullet points on the pricing card
limitsRecord<string, number>Machine-readable caps your app enforces
recommendedbooleanHighlights as the default choice
trial_daysnumberPer-plan trial override (0 = no trial)

Step 1: Download Your Integration Kit

Pre-configured files generated from your live plan configuration.

Next.js

NEXT_PUBLIC_ env vars + fetch revalidation

Node / CLI

Plain SPARK_APP_ID + in-memory cache

Extension

Hardcoded APP_ID + in-memory cache

Step 2: Send Users to the Pricing Page

Redirect unauthenticated or unpaid users to your hosted pricing page.

Next.js

import { pricingUrl } from '@/lib/SparkPay-Integration';
import { redirect } from 'next/navigation';
redirect(pricingUrl({
email: user.email,
returnUrl: `${process.env.NEXT_PUBLIC_APP_URL}/success`
}));

Step 3: Handle the Success Page

Poll the status endpoint every 2s until the subscription is active, then redirect.

app/success/page.tsx

import { PaymentPoller } from './PaymentPoller';
export default async function SuccessPage({
searchParams,
}: {
searchParams: Promise<{ email?: string }>;
}) {
const { email } = await searchParams;
if (!email) redirect('/dashboard');
return <PaymentPoller email={email} />;
}

app/success/PaymentPoller.tsx

'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { getStatus, isPaid } from '@/lib/SparkPay-Integration';
const POLL_INTERVAL_MS = 2000;
const MAX_ATTEMPTS = 8; // 16 seconds total
export function PaymentPoller({ email }: { email: string }) {
const router = useRouter();
const [timedOut, setTimedOut] = useState(false);
useEffect(() => {
let attempts = 0;
let cancelled = false;
async function poll() {
if (cancelled) return;
try {
const data = await getStatus(email);
if (isPaid(data)) { router.replace('/dashboard'); return; }
} catch {}
if (++attempts >= MAX_ATTEMPTS) { setTimedOut(true); return; }
setTimeout(poll, POLL_INTERVAL_MS);
}
poll();
return () => { cancelled = true; };
}, [email, router]);
if (timedOut) return <p>Payment received. Access may take a moment to activate.</p>;
return <p>Processing your payment...</p>;
}
Note
Stripe confirms payment before redirecting the user. The user's browser load + JS init + first poll takes 2-4 seconds, by which point the status is available.

Step 4: Gate Features

The gating helpers work identically across all platforms.

import { auth } from '@/auth';
import { getStatusCached, isPaid, pricingUrl } from '@/lib/SparkPay-Integration';
import { redirect } from 'next/navigation';
export default async function DashboardPage() {
const session = await auth();
if (!session?.user?.email) redirect('/login');
const data = await getStatusCached(session.user.email);
if (!isPaid(data)) redirect(pricingUrl({ email: session.user.email }));
return <div>Welcome to your dashboard</div>;
}

Step 5: Badge Component

Drop-in React component showing plan status and upgrade CTA.

import { SparkBadge } from '@/lib/SparkPay-Badge';
<SparkBadge email={user.email} />
Tip
The badge auto-refetches status on tab focus (no polling loops). A 30-second per-email throttle prevents API hammering. The coupons fetch is module-scope cached so multiple badge instances dedupe to a single network call.

Step 6: Webhooks (Push Mode)

Optional. React to events in real time. Requires a server endpoint.

Note
If you chose poll mode, skip this step entirely. Webhooks are for real-time reactions: sending welcome emails, provisioning resources, or caching status locally.
{
"event": "subscription.activated",
"app_id": "your-app-id",
"email": "user@example.com",
"timestamp": "2026-03-14T12:00:00.000Z",
"subscription": {
"status": "active",
"payment_type": "subscription",
"price_id": "price_xxx",
"current_period_end": "2026-04-14T12:00:00.000Z"
}
}
EventWhen it fires
checkout.completedPayment captured; first record created
subscription.activatedRecurring sub is active (also fires on trial convert)
subscription.trial_startedUser began a free trial
subscription.past_duePayment failed; grace period started
subscription.canceledSubscription ended
referral.completedReferral processed; reward coupon created

Key Points

Gate on access.is_paid or access.tier - not on current_period_end. Server-computed access flags are authoritative.

is_lifetime: true means permanent access - no cancellation event will ever fire.

No database tables needed - SparkPay is your single source of truth.

Free-tier users have subscription: null - but receive plan with features and limits if a free plan is configured.

Tier resolution happens server-side - plan tells you what they bought; access tells you what they can do now.

What NOT to Build

Do not create subscriptions, payments, or plans tables in your database.

Do not store status, price_id, or current_period_end locally.

Do not try to sync or mirror SparkPay's subscription state.

If you need faster reads, use getStatusCached() (60s revalidation). That is all.

Ready to ship?

Add Stripe payments to your app in minutes. $99, one-time purchase.