Use this when your server creates the transaction with Paddle's API and passes its id to the browser. It is the most common shape for a custom pricing page.
The visitor id is in a cookie your server can read, so it goes in as
custom_data when the transaction is created — before the customer sees a
checkout at all.
// app/api/create-checkout/route.js
import { cookies } from "next/headers";
export async function POST() {
// `cookies()` is only readable on the server — this is why the transaction
// is created here rather than in the browser.
const cookieStore = await cookies();
const ts_vid = cookieStore.get("ts_vid")?.value;
const ts_vs = cookieStore.get("ts_vs")?.value;
// Left out entirely when absent: a visitor with cookies blocked simply has
// none, and an empty string is not the same as no value.
const attribution = {};
if (ts_vid) attribution.ts_vid = ts_vid;
if (ts_vs) attribution.ts_vs = ts_vs;
const response = await fetch("https://api.paddle.com/transactions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PADDLE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
items: [{ price_id: "pri_01…", quantity: 1 }],
custom_data: attribution,
}),
});
const { data } = await response.json();
// Hand `data.id` to Paddle.js, or send the customer to `data.checkout.url`.
return Response.json({ transactionId: data.id });
}custom_data here, customData in Paddle.js. The REST API takes
snake_case and the browser SDK takes camelCase. Neither rejects the other
spelling — it is simply ignored, and the payment arrives unattributed with
nothing on screen to say why. If you create the transaction on the server and
open it with Paddle.js, only the server call needs the field.
Checking it worked
Open the transaction in Paddle and look for custom_data. If it is empty
there, it never reached us — the problem is in this code rather than in the
integration.
If it is populated but the payment still shows as unattributed in TrueStat, the cookie was missing when the transaction was created. That is normal for a visitor with cookies blocked, and for a customer who reaches checkout without ever loading a page that carries the script.
What happens on renewals
Nothing further. Paddle copies custom_data onto every renewal transaction of
that subscription, so the second month is attributed to the same visit as the
first — with no code from you.