RefCampaign/docs

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 and stripe.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. 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_sid cookie, or localStorage, 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. 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. 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 })
    }

    getStripeMetadata returns { refcampaign_session: '...' } for a valid session id and {} otherwise — pass the result directly, no conditional needed.

  4. 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.succeeded on 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

On this page