Stripe Payment Element
Inject the RefCampaign session id into a PaymentIntent when you build a custom checkout with Stripe Elements.
Stripe Elements lets you embed Stripe's payment form in your own UI instead of redirecting to Stripe Checkout. RefCampaign attributes conversions from Elements the same way it attributes Checkout: by reading the refcampaign_session value you stamp onto the Stripe metadata. There is no Elements-specific webhook to register and no extra setup on the Stripe side.
When to use this guide
Use this guide if your checkout is built around:
- Payment Element — the unified drop-in form.
- Express Checkout Element — Apple Pay, Google Pay, Link.
- A custom flow that calls
stripe.paymentIntents.create()server-side andstripe.confirmPayment()client-side.
If you redirect to Stripe Checkout, see the SDK guide instead — the integration pattern is identical, only the placement of the metadata call differs.
Quickstart
- 1
Capture the session client-side
Call
RefCampaignBrowser.captureSession()once per page load — typically in your root layout. It reads the session from the URL (?_rcid=or?rcsid=), the_rc_sidcookie, orlocalStorage, in that order, and persists it for later reads.'use client' import { useEffect } from 'react' import { RefCampaignBrowser } from '@refcampaign/sdk' export default function RootLayout({ children }: { children: React.ReactNode }) { useEffect(() => { RefCampaignBrowser.captureSession() }, []) return ( <html> <body>{children}</body> </html> ) }If you use the CDN snippet (
<script src="https://sdk.refcampaign.com/v1.js" async>), this step happens automatically. - 2
Send the session id to your backend
Read the captured id from the SDK and pass it alongside the checkout request. If the visitor came from an affiliate link, the value is non-null; if not, the request still goes through with empty metadata.
'use client' import { RefCampaignBrowser } from '@refcampaign/sdk' async function startCheckout(amount: number, currency: string) { const sessionId = RefCampaignBrowser.getSessionId() const res = await fetch('/api/payment-intent', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ amount, currency, sessionId }), }) const { clientSecret } = await res.json() return clientSecret } - 3
Create the PaymentIntent with RefCampaign metadata
On the server, attach
rc.getStripeMetadata(sessionId)to the PaymentIntent. Stripe copies metadata from the PaymentIntent onto the resulting Charge, which is what RefCampaign reads for attribution.// app/api/payment-intent/route.ts import { NextRequest, NextResponse } from 'next/server' import { RefCampaignServer } from '@refcampaign/sdk' import Stripe from 'stripe' const rc = new RefCampaignServer(process.env.REFCAMPAIGN_SECRET_KEY!) const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!) export async function POST(req: NextRequest) { const { amount, currency, sessionId } = await req.json() const paymentIntent = await stripe.paymentIntents.create({ amount, currency, automatic_payment_methods: { enabled: true }, metadata: rc.getStripeMetadata(sessionId), }) return NextResponse.json({ clientSecret: paymentIntent.client_secret }) }getStripeMetadatareturns{ refcampaign_session: '...' }for a valid session id and{}otherwise — pass the result directly, no conditional needed. - 4
Mount Payment Element and confirm
Standard Stripe Elements code — nothing RefCampaign-specific. The metadata is already on the PaymentIntent.
'use client' import { Elements, PaymentElement, useElements, useStripe } from '@stripe/react-stripe-js' import { loadStripe } from '@stripe/stripe-js' const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!) export function CheckoutForm({ clientSecret }: { clientSecret: string }) { return ( <Elements stripe={stripePromise} options={{ clientSecret }}> <PayForm /> </Elements> ) } function PayForm() { const stripe = useStripe() const elements = useElements() async function handleSubmit(e: React.FormEvent) { e.preventDefault() if (!stripe || !elements) return await stripe.confirmPayment({ elements, confirmParams: { return_url: `${window.location.origin}/checkout/success` }, }) } return ( <form onSubmit={handleSubmit}> <PaymentElement /> <button type="submit">Pay</button> </form> ) }
How attribution fires
Once the payment succeeds, Stripe creates a Charge whose metadata carries refcampaign_session (copied from the PaymentIntent). RefCampaign picks it up via one of two paths, depending on how the merchant account is connected:
- OAuth-connected accounts receive
charge.succeededon the RefCampaign webhook within seconds. - API key accounts are polled every 30 minutes — the conversion appears in the dashboard at the next polling cycle.
You don't register webhooks on your Stripe account and you don't call the RefCampaign API directly. The metadata is the entire contract.
Subscriptions via Elements
If you create the Subscription server-side after collecting payment with Elements, place the metadata on the Subscription, not on the Customer:
const subscription = await stripe.subscriptions.create({
customer: customer.id,
items: [{ price: priceId }],
metadata: rc.getStripeMetadata(sessionId),
})Each subscription is tied to one affiliate campaign. Future renewals credit the same affiliate; if the customer later resubscribes via a different link, the new subscription attributes to the new affiliate.
Fallback for lost sessions
If RefCampaignBrowser.getSessionId() returns null at checkout time (Safari ITP, cross-device, server-only flow), call RefCampaignBrowser.identify(customer.email) after login or before payment. RefCampaign then attributes via the SHA-256 email hash on the most recent click within the campaign's attribution window.
'use client'
import { RefCampaignBrowser } from '@refcampaign/sdk'
// After login or signup, when the email is known
await RefCampaignBrowser.identify(currentUser.email)The email is hashed in the browser via Web Crypto — the plaintext value never leaves the visitor's device.
Charge object required for webhook attribution
RefCampaign reads refcampaign_session from the Charge metadata, which Stripe creates as soon as the PaymentIntent succeeds in a standard confirmation flow. Custom flows that confirm without producing a Charge — for example, manual capture held indefinitely, ACH micro-deposit verification before final capture, or PaymentIntents that bypass Charges via Treasury — won't trigger webhook attribution until the Charge lands. For those flows, fire the conversion via the server-to-server postback from your own success signal.
Next steps
- SDK integration — full reference for
RefCampaignBrowserandRefCampaignServer. - Server-to-server postback — non-JS stacks, custom payment processors, or fallback for non-standard Stripe flows.