VoidhashDocs
DocumentationAPI reference
Dashboard
Get started
Introduction
Quickstart
Setup
Setup overview
Create a project
Environments
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
Record payments from other sources
API libraries
Node.js
PHP
Rust
Go
Resources
MCP server
Ad measurement
Meta ads
Migrate in observer mode
Delete a person's data
Web SDK
Web quickstart
Script tag
Identify users
Track events
Privacy and consent
Page views and attribution
Sell on the web
Stripe revenue
Web-to-app handoff
First-party proxy
Tracking domain
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.

How it works

Your app and Stripe run the checkout. Your server chooses the price, whether by currency, region or a pricing experiment, creates the Checkout Session and sends the buyer to it. Voidhash learns what was sold from Stripe's events afterwards, and grants the Voidhash product the sold price is connected to. Connect as many prices to one product as you sell it under.

The one thing to add is the buyer's ID, as voidhash_distinct_id in Stripe metadata. Voidhash reads it from the first of these that has one:

WhereUse it when
Checkout Session metadataYour server creates a Checkout Session. Always set it there.
Subscription metadata, set with subscription_dataThe session starts a subscription. Its renewals carry it.
Stripe customer metadataYou keep a Stripe customer per user, create subscriptions through the API, or need to link past purchases.

Metadata takes any characters, so an anonymous ID and an email address pass through unchanged. Voidhash does not read client_reference_id: keep it for your own use, such as an order or cart ID.

Send the ID from the browser

Read the visitor's ID with getDistinctId() and send it with your checkout request. It is the anonymous visitor's ID, or your user ID once you called identify(). 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) {
  // Resolves at once when the client has started; before that, there is no ID yet.
  await voidhash.initialize();
  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 signed-in user's ID is your own user ID, so your server often has it already: use it instead of the one from the request.

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

Put the ID on the session, and for a subscription on the subscription too, so its renewals carry it:

Server
import Stripe from "stripe";

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

export async function createCheckoutSession(input: { distinctId: string; priceId: string }) {
  const session = 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",
  });
  return session.url;
}

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

A subscription's payments, the first one included, are recorded from its invoices, which carry the subscription's metadata. Without subscription_data.metadata, the session's ID still links the Stripe customer to the buyer when the checkout completes, and later invoices follow it. Stripe can deliver the first invoice before that, though, and an anonymous visitor's first payment then stays on a placeholder for the Stripe customer. Set both.

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({
    line_items: [{ price: PRICES[plan], quantity: 1 }],
    metadata: { voidhash_distinct_id: distinctId },
    mode: "subscription",
    subscription_data: { metadata: { voidhash_distinct_id: distinctId } },
    success_url: `${new URL(request.url).origin}/welcome`,
  });
  return Response.json({ url: session.url });
}

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.

Payment Links, the pricing table and the Buy Button

Payment Links, the pricing table and the Buy Button cannot carry metadata for each buyer. Purchases made through them are still recorded, but on a placeholder person for the Stripe customer. Once your backend sets the customer's voidhash_distinct_id to your user ID, for example after sign-up, the customer's next event, such as a renewal, moves the placeholder's purchases, subscriptions and access to that user.

Nothing links such a purchase to the browser that opened the link, so an anonymous visitor's campaign is not credited with it. To tie anonymous visitors to their purchases, for example for ad attribution, create Checkout Sessions on your server.

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 of the person's last campaign visit before they signed up.

Purchases in Stripe test mode are recorded as test: Studio shows them under Test, and only test keys read them. A staging site that sells in test mode uses the test vh_pk_test_… publishable key, and its server creates Checkout Sessions with a Stripe test key. See Environments.

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 signup is credited to their last campaign visit before it, within your project's attribution window (7 days unless you change it). Every purchase, renewal, and refund of the person counts toward that campaign, ad set, and ad. See Ad measurement.
  • 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.

PreviousSell on the webNextWeb-to-app handoff

On this page

Before you startHow it worksSend the ID from the browserCreate the Checkout SessionIn Next.jsPut the ID on the Stripe customerPayment Links, the pricing table and the Buy ButtonCheck the resultHow revenue is credited