RefCampaignDocs

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

    Apply the merchant CMP decision

    Configure the passive SDK in a root client component, then forward the initial consent choice and every later change from your CMP. A refusal or withdrawal must pass false.

    'use client'
    import { useEffect } from 'react'
    import { RefCampaignBrowser } from '@refcampaign/sdk'
    
    export function RefCampaignConsent({ granted }: { granted: boolean | null }) {
      useEffect(() => {
        RefCampaignBrowser.configure({ siteToken: 'rcst_PUBLIC_SITE_TOKEN' })
      }, [])
    
      useEffect(() => {
        if (granted === null) return
        void RefCampaignBrowser.setConsent({ attribution: granted })
      }, [granted])
    
      return null
    }

    If you use the CDN bundle, load https://sdk.refcampaign.com/v4.js?s=rcst_PUBLIC_SITE_TOKEN; it is also passive and still requires the same CMP call. After a grant, _rc_sid lasts 90 days. Refusal sends no capture request and withdrawal deletes the session.

  2. 2

    Send the session id to your backend

    Read the consented id from the SDK and pass it alongside the checkout request. If no consented session exists, 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 refcampaign_session metadata 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 Stripe from 'stripe'
    
    const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
    
    export async function POST(req: NextRequest) {
      const { amount, currency, sessionId } = await req.json()
      const metadata = sessionId ? { refcampaign_session: sessionId } : {}
    
      const paymentIntent = await stripe.paymentIntents.create({
        amount,
        currency,
        automatic_payment_methods: { enabled: true },
        metadata,
      })
    
      return NextResponse.json({ clientSecret: paymentIntent.client_secret })
    }

    This Stripe attribution step does not require a RefCampaign secret key; the metadata is the contract.

  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: sessionId ? { refcampaign_session: 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.

If the visitor refuses browser attribution, do not recreate a browser session. You can still use a coupon, an explicit referral code, or an independently known server-side affiliate code when your checkout already has that information.

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