Stripe can carry RefCampaign attribution through metadata. The critical detail is where that metadata lives: subscription metadata for recurring revenue, Payment Intent metadata for one-time payments.
This guide assumes your frontend already captures the RefCampaign session with the browser SDK or CDN script.
Prerequisites
- A Stripe account with Checkout enabled.
- A RefCampaign secret key.
- The
@refcampaign/sdkpackage installed on your backend. - A server route that creates Stripe Checkout sessions.
pnpm add @refcampaign/sdk stripe
Store the secrets server-side only:
REFCAMPAIGN_SECRET_KEY=sk_live_your_refcampaign_secret
STRIPE_SECRET_KEY=sk_live_your_stripe_secret
APP_URL=https://yourapp.com
Initialize the server SDK
Create a small module that your checkout routes can import.
// lib/billing.ts
import Stripe from 'stripe'
import { RefCampaignServer } from '@refcampaign/sdk'
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
export const refcampaign = new RefCampaignServer(
process.env.REFCAMPAIGN_SECRET_KEY!
)
Subscription Checkout
For recurring products, put the RefCampaign metadata on subscription_data.metadata. This lets renewals keep crediting the same affiliate.
// app/api/stripe/checkout-subscription/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { refcampaign, stripe } from '@/lib/billing'
export async function POST(request: NextRequest) {
const { priceId, customerEmail } = await request.json()
const sessionId = request.cookies.get('_rc_sid')?.value
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
customer_email: customerEmail,
line_items: [{ price: priceId, quantity: 1 }],
subscription_data: {
metadata: refcampaign.getStripeMetadata(sessionId),
},
success_url: `${process.env.APP_URL}/checkout/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.APP_URL}/pricing`,
})
return NextResponse.json({ url: session.url })
}
One-time Checkout
For one-time products, put the metadata on payment_intent_data.metadata.
// app/api/stripe/checkout-payment/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { refcampaign, stripe } from '@/lib/billing'
export async function POST(request: NextRequest) {
const { priceId, customerEmail } = await request.json()
const sessionId = request.cookies.get('_rc_sid')?.value
const session = await stripe.checkout.sessions.create({
mode: 'payment',
customer_email: customerEmail,
line_items: [{ price: priceId, quantity: 1 }],
payment_intent_data: {
metadata: refcampaign.getStripeMetadata(sessionId),
},
success_url: `${process.env.APP_URL}/checkout/success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.APP_URL}/pricing`,
})
return NextResponse.json({ url: session.url })
}
Verify webhook coverage
RefCampaign processes Stripe-side revenue events through your merchant integration. Your job is to make sure the metadata exists before Stripe emits the paid event.
Use Stripe CLI to inspect a test checkout:
stripe listen --forward-to localhost:3000/api/webhooks/stripe
stripe trigger checkout.session.completed
For a real manual test, create a checkout through your UI after clicking an affiliate link. In the Stripe Dashboard:
- open the Checkout Session;
- open the Subscription or Payment Intent;
- verify
refcampaign_sessionappears in metadata.
Handle missing sessions safely
Not every buyer comes from an affiliate. getStripeMetadata() is safe to call with an undefined session ID, and checkout should continue normally.
const metadata = refcampaign.getStripeMetadata(sessionId)
const subscriptionData =
Object.keys(metadata).length > 0
? { metadata }
: undefined
Use this pattern if your Stripe helper rejects empty metadata objects.
Common mistakes
Do not put subscription attribution on Customer metadata only. Customer metadata is shared across future purchases and can credit the wrong affiliate when a customer re-subscribes later.
Do not attach one-time payment attribution to the Checkout Session only. RefCampaign needs metadata on the Stripe object that represents the paid event.
Do not log RefCampaign secret keys or full Stripe request bodies in production. Metadata is safe to inspect, secrets are not.
Next steps
Pair this setup with the Next.js affiliate tracking guide if your checkout lives in App Router. For broader implementation context, read how affiliate tracking works and keep the SDK reference nearby during review.