VoidhashDocs
DocumentationAPI reference
Dashboard
Get started
Introduction
Quickstart
Setup
Setup overview
Create a project
Products and perks
Paywall locations
CLI
Paywall components
Components
Develop a component
Editor panels
Component definition
Publishing
Installing and capabilities
SDKs
Swift
Kotlin
React Native
Web
Store setup
Connect the App Store
Connect Google Play
Connect Stripe
API libraries
Node.js
PHP
Rust
Go
Resources
MCP server
Meta ads
Migrate in observer mode
Delete a person's data
Web SDK
Web quickstart
Identify users
Track events
Page views and attribution
Stripe revenue
Web-to-app handoff
First-party proxy
Feature flags
Troubleshooting

Stripe revenue

Tie Stripe purchases to the web visitor, so revenue is credited to the campaign that brought them.

Voidhash records Stripe purchases from Stripe itself: subscriptions, renewals, one-time payments, refunds, and disputes arrive through your Stripe connection, whether or not the SDK is on the page. The SDK's part is to tell Stripe who the buyer is. Give each purchase the visitor's Voidhash ID, and Voidhash puts the purchase on that person, so their revenue, their renewals, and their refunds are credited to the campaign, ad set, and ad that first brought them.

Before you start

  • Connect Stripe to your project, and connect your Stripe prices to Voidhash products.
  • Install the web SDK on the pages your buyers visit, including the landing pages your ads link to.

Choose where to put the ID

Voidhash reads the buyer's ID from the first of these places that has one: the Checkout Session's client_reference_id, then the session's or subscription's metadata.voidhash_distinct_id, then the Stripe customer's metadata.voidhash_distinct_id. Pick the one that matches how you sell.

WhereUse it when
Checkout Session client_reference_idYour server creates a Checkout Session. The simplest option.
Checkout Session or subscription metadata.voidhash_distinct_idYou do not set client_reference_id. When you set it, it must be the Voidhash ID, because it wins.
Stripe customer metadata.voidhash_distinct_idYou keep a Stripe customer per user, create subscriptions through the API, or need to link past purchases.

For a subscription, also put the ID on the subscription with subscription_data.metadata.voidhash_distinct_id. A subscription's payments, the first one included, are recorded from its invoices, and the subscription's metadata reaches each of them. Without it, the session's ID links the Stripe customer when the checkout completes, so the invoices follow it; see Without a server for when that is not enough. Setting the ID on the customer covers every subscription and invoice of that customer at once.

Send the ID from the browser

Read the visitor's ID and send it with your checkout request. When the visitor is signed in, call identify() first, so the purchase lands on your user rather than the anonymous visitor.

Browser
import { voidhash } from "./lib/voidhash";

export async function startCheckout(plan: string) {
  const distinctId = voidhash.getDistinctId();
  await voidhash.track("checkout_started", { plan });

  const response = await fetch("/api/checkout", {
    body: JSON.stringify({ distinctId, plan }),
    headers: { "content-type": "application/json" },
    method: "POST",
  });
  const { url } = await response.json();
  window.location.assign(url);
}

In React, read it from useVoidhash().distinctId.

A visitor who buys before signing up is fine too. The purchase lands on the anonymous visitor, and when they later sign up and you call identify(), it moves to the user with the rest of their history.

Create the Checkout Session

Pass the ID to Stripe on your server. With client_reference_id:

Server
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

export async function createCheckout(input: { distinctId: string; priceId: string }) {
  const session = await stripe.checkout.sessions.create({
    client_reference_id: input.distinctId,
    line_items: [{ price: input.priceId, quantity: 1 }],
    mode: "subscription",
    subscription_data: {
      metadata: { voidhash_distinct_id: input.distinctId },
    },
    success_url: "https://example.com/welcome",
  });
  return session.url;
}

Or with metadata, leaving client_reference_id free for your own use:

await stripe.checkout.sessions.create({
  line_items: [{ price: input.priceId, quantity: 1 }],
  metadata: { voidhash_distinct_id: input.distinctId },
  mode: "subscription",
  subscription_data: {
    metadata: { voidhash_distinct_id: input.distinctId },
  },
  success_url: "https://example.com/welcome",
});

For a one-time payment (mode: "payment"), client_reference_id or the session's metadata is all you need.

In Next.js

A route handler that the browser code above calls:

app/api/checkout/route.ts
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const PRICES: Record<string, string> = { yearly: "price_…" };

export async function POST(request: Request) {
  const { distinctId, plan } = await request.json();
  const session = await stripe.checkout.sessions.create({
    client_reference_id: distinctId,
    line_items: [{ price: PRICES[plan], quantity: 1 }],
    mode: "subscription",
    subscription_data: { metadata: { voidhash_distinct_id: distinctId } },
    success_url: `${new URL(request.url).origin}/welcome`,
  });
  return Response.json({ url: session.url });
}

If your server already knows the signed-in user, use their user ID, the one you pass to identify(), instead of the ID from the request.

Put the ID on the Stripe customer

If you keep one Stripe customer per user, set the ID on the customer when you create it:

const customer = await stripe.customers.create({
  email: user.email,
  metadata: { voidhash_distinct_id: user.id },
});

Or add it to an existing customer:

await stripe.customers.update(customerId, {
  metadata: { voidhash_distinct_id: user.id },
});

The customer's ID applies to every purchase of that customer that carries no ID of its own, including subscriptions created through the API, purchases in the customer portal, and past purchases once you import your Stripe history again. A change applies from the customer's next event.

Use your user ID here, not an anonymous ID: a customer outlives a browser.

Without a server

Stripe Payment Links and the pricing table take a client_reference_id too, as a URL parameter and an attribute. They accept only letters, digits, dashes, and underscores and drop anything else, so use getStripeClientReferenceId(). It returns an anonymous visitor's ID in a form they accept, vh_anon_…, which Voidhash reads back, and a user ID as is.

Payment Link
const reference = voidhash.getStripeClientReferenceId();
const link = new URL("https://buy.stripe.com/…");
if (reference) link.searchParams.set("client_reference_id", reference);
document.querySelector<HTMLAnchorElement>("#buy")!.href = link.toString();
Pricing table
const { client } = useVoidhash();
const reference = client.getStripeClientReferenceId();

<stripe-pricing-table
  pricing-table-id="prctbl_…"
  publishable-key="pk_live_…"
  client-reference-id={reference}
/>;

For a user ID Stripe would drop, such as an email address, getStripeClientReferenceId() returns undefined; use a Checkout Session on a server for those users.

A subscription bought this way is linked through the Stripe customer when its checkout completes, and its invoices, renewals included, then follow the buyer. Stripe can deliver the first invoice before the checkout, or both at once; the subscription reaches the buyer in either case, with its access and renewals. For a signed-in user, the first payment is theirs as well. For an anonymous visitor, it stays recorded on a placeholder for the Stripe customer, so revenue reports do not count it toward the visitor's campaign. A Stripe customer that already belongs to a person is never moved by a subscription checkout. To attribute every payment from the first one, create the Checkout Session on a server with subscription_data.metadata.

Check the result

After a test purchase, open the person in Studio: the purchase appears in their history, next to their page views and campaign traits. Ads reports count the purchase toward the campaign in the person's first touch.

To act on the purchase on your server, check access with the Node.js library, using the same ID:

const hasPremium = await voidhash.entitlements.hasActivePerk({
  distinctId: user.id,
  perkSlug: "premium",
});

How revenue is credited

  • A purchase belongs to the person behind the ID it carries. Without an ID anywhere, Voidhash records it for a person tied to the Stripe customer, and merges that person into your user once the customer's metadata names them.
  • A person's acquisition is their first touch, the $initial_* traits the SDK recorded on their first campaign visit. Every purchase, renewal, and refund of the person counts toward that campaign, ad set, and ad.
  • When the same user also signs in to your app, purchases from the App Store and Google Play land on the same person. See Keep web and app users the same person.

See Stripe setup for what each purchase grants and how renewals, upgrades, and refunds are recorded.

PreviousPage views and attributionNextWeb-to-app handoff

On this page

Before you startChoose where to put the IDSend the ID from the browserCreate the Checkout SessionIn Next.jsPut the ID on the Stripe customerWithout a serverCheck the resultHow revenue is credited