Skip to main content

Paddle

Checkout API

You build the transaction on your server, then hand it to a checkout.

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.

Next.js
// 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.

Was this page helpful?

Last updated September 8, 2026