> ## Documentation Index
> Fetch the complete documentation index at: https://docs.traceten.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Paddle

> Attribute Paddle revenue to AI-referred sessions.

## What this lets you do

Connect Paddle to Traceten and see which AI sources drive paying customers, not just pageviews. When a transaction completes, Traceten matches it back to the originating session and attributes the revenue to the correct AI source.

<Note>
  Paddle is the only integration that **requires** you to pass a value through checkout. Paddle is
  the merchant of record, and its transaction webhooks carry no buyer email, so there is no fallback
  for Traceten to match on. Read [Passing the visitor ID](#passing-the-visitor-id-through-checkout)
  before you connect.
</Note>

## Before you start

* A Traceten account with at least one site tracked.
* A Paddle Billing account, and permission to create API keys on it.
* Your Paddle **Seller ID**. It is shown in your Paddle account settings.
* The Traceten snippet installed and verified on the site your checkout runs from.
* Workspace admin in Traceten. Connecting and disconnecting a revenue source affects the whole workspace, so only admins can do it.

## Connect Paddle

You connect by pasting an **API key** and your **Seller ID**. There is no OAuth redirect, and there is no notification destination to configure: Traceten creates it on your Paddle account for you.

<Steps>
  <Step title="Create an API key in Paddle">
    Open [Paddle → Developer tools → Authentication](https://vendors.paddle.com/authentication-v2) and create an API key.

    Give it a name (for example, `Traceten`) and these permissions:

    | Permission                   | Why                                           |
    | ---------------------------- | --------------------------------------------- |
    | `transaction.read`           | Reading the revenue Traceten attributes       |
    | `notification_setting.write` | Creating the destination Traceten receives on |

    Copy the key. It starts with `pdl_live_`.
  </Step>

  <Step title="Find your Seller ID">
    It is the number shown in your Paddle account settings.

    Traceten asks for it because **Paddle's API has no endpoint that reports which account a key belongs to**. Every other integration reads its own identity from the provider; for Paddle there is nothing to read, so Traceten records the Seller ID you enter and treats it as the identity of this connection. Traceten cannot verify that claim against Paddle. It does not affect the security of your data: every delivery is still checked against this connection's own signing secret.
  </Step>

  <Step title="Paste both into Traceten">
    Go to [app.traceten.com/dashboard](https://app.traceten.com/dashboard), open **Settings → Integrations**, and choose the site your checkout runs from at the top of the page.

    Pick **Paddle** from the list of payment providers, then paste the API key and the Seller ID.

    Click **Connect**.
  </Step>

  <Step title="Confirm the connection">
    Paddle shows as **Connected**, with the last four characters of the key.

    Traceten stores the key encrypted. After this point it is never shown again, in the dashboard or in the API, and only those last four characters are readable back.
  </Step>
</Steps>

### What Traceten does with the key

In the same request, Traceten:

1. Reads one transaction, to prove the key works and can actually see the revenue it is being connected for.
2. Creates a notification destination on your Paddle account pointed at Traceten, subscribed to three events.
3. Stores the key and the destination's signing secret, both encrypted.
4. Starts importing your last 90 days of completed transactions.

You can see the destination Traceten created under **Developer tools → Notifications**.

Traceten asks Paddle not to include sensitive fields in those notifications, because it needs none of them.

These are the three events:

| Event                   | What Traceten does with it                                         |
| ----------------------- | ------------------------------------------------------------------ |
| `transaction.completed` | Attributes the transaction. This is the revenue.                   |
| `adjustment.created`    | Subtracts an approved **refund** from the source that earned it.   |
| `adjustment.updated`    | Catches a refund reaching `approved` after it was created pending. |

**Subscription revenue arrives on `transaction.completed`.** Paddle bills a renewal as a transaction, so every renewal is attributed to the AI source that won the customer, and it carries the subscription id into the per-subscription [lifetime value report](/api/revenue). Traceten does not subscribe to Paddle's `subscription.*` events, because the money is already on the transaction.

### Refunds and net revenue

Your attributed revenue is **net of refunds**. Paddle reports a refund as an **adjustment**, and Traceten subtracts it from the AI source the original transaction was attributed to.

* **Only adjustments whose action is `refund` are subtracted.** Paddle uses the same adjustment object for credits and chargebacks, and Traceten ignores both. A **credit** reduces a future invoice rather than returning money, so taking it off revenue already recorded would be wrong. A **chargeback** is a dispute, and Traceten does not act on disputes today.
* **A Paddle refund needs Paddle's approval before the money moves.** The figure in Traceten changes when the adjustment reaches `approved`, not when your customer or your team requests it. A `pending_approval` refund leaves your revenue alone.
* **The refund is dated on the day the adjustment was issued**, not the day of the original transaction. A past day's revenue figure never changes after the fact, and a day on which you refunded more than you took in reads as a negative number.
* **Conversion counts stay gross.** A refunded transaction still counts as a conversion. The money is reported separately as a **refunded** figure beside the net one.
* **Partial refunds subtract exactly what the adjustment returned.**
* **A cancelled subscription is not a refund.** Cancelling stops future charges. The charges already paid were earned and stay in your totals, so cancelling changes no figure in Traceten. Only an approved refund adjustment takes money back off.

### The amount Traceten records

Traceten records the **tax-inclusive total** your customer was charged, matching what a Stripe charge records.

It does not record your net earnings. Two reasons: net is not comparable with the gross figures every other integration writes into the same column, so an otherwise identical Paddle site would read quietly smaller; and Paddle's `fee` and `earnings` fields are empty until it balances the transaction, so at webhook time there is often no net figure to record at all.

### Keys Traceten will not accept

* **Sandbox keys** (starting `pdl_sdbx_`) are rejected in production. Sandbox transactions carry synthetic amounts that would corrupt your revenue totals. This mirrors the rule for Stripe test-mode keys.
* **Stripe keys.** Pasting one names the mistake rather than failing on a network call.
* **A key with whitespace or a line break in it.** Copy it again from Developer tools → Authentication.

### Replay protection

Paddle signs each delivery with a timestamp, and Traceten rejects any delivery signed more than **30 seconds** ago. Paddle's own SDKs default to five seconds; Traceten allows a little more because it looks your connection up before it verifies, and that lookup can take a few seconds when its database has been idle. Paddle re-signs every retry, so a rejected delivery arrives again and succeeds.

## How attribution works

When a transaction completes, Paddle sends a `transaction.completed` webhook to Traceten. Traceten then tries to tie the payment back to the originating session:

1. **Checkout custom data:** if you passed the visitor's `_traceten_vid` cookie as `traceten_visitor_id`, Traceten matches it to the originating session exactly.
2. **No match:** the payment is still recorded so revenue totals stay complete, but it shows as **unattributed** (no AI source).

There is no email step, unlike every other integration. Paddle is the merchant of record and does not send the buyer's email on a transaction, so custom data is the only signal there is.

## Passing the visitor ID through checkout

Do this before you connect, or your revenue will all land as unattributed.

<Tip>
  In the dashboard, open **Settings → Integrations**, pick **Paddle** and click **Set up with AI**.
  It copies a prompt for your coding assistant that wires the visitor ID into your checkout. The
  same card shows this site's **Visitor cookie name**.
</Tip>

With Paddle.js, use `window.traceten.getVisitorId()`. It is the robust way to get the value, since it always resolves whatever the current cookie is rather than depending on a specific name:

```typescript theme={null}
// Client-side: read the visitor id and pass it into the checkout
const visitorId = window.traceten.getVisitorId() ?? ""; // null before the first pageview resolves

Paddle.Checkout.open({
  items: [{ priceId: "pri_...", quantity: 1 }],
  customData: { traceten_visitor_id: visitorId },
});
```

Or when you create a transaction server-side, forward the value your client already read via `getVisitorId()`:

```typescript theme={null}
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_...", quantity: 1 }],
    custom_data: { traceten_visitor_id: visitorIdFromClient ?? "" },
  }),
});
```

If you'd rather not change your client-side request shape, read the cookie directly instead:

<Warning>
  Do not hardcode this cookie name. It is per-site and only correct on your configured domain (not
  on localhost). Find the real one under **Sites → Settings → Cookies → Visitor cookie
  name**.
</Warning>

```typescript theme={null}
// Client-side fallback: read the cookie by name instead of getVisitorId()
const visitorId = document.cookie.match(/(?:^|;\s*)_traceten_vid=([^;]+)/)?.[1] ?? "";
```

Paddle returns the value as `custom_data` on the webhook, and Traceten matches it back to the AI session that produced the visit. No personal data leaves your server on this path: the cookie value is pseudonymous and is not PII.

For a **subscription**, set `custom_data` on the original checkout. Paddle carries it onto each renewal transaction, so renewals attribute to the AI source that won the customer in the first place.

## What the 90-day import can and cannot match

Traceten imports your last 90 days of completed transactions when you connect, so revenue from before the destination existed is not lost. The 90-day bound is part of the request Traceten makes to Paddle, so older transactions are never returned to us at all.

A historical transaction matches only if it already carries `custom_data.traceten_visitor_id`. Transactions from before you added that (which is most of them, on a first connect) are recorded as unattributed. They still count toward your revenue totals; they just carry no AI source.

## Verify it worked

1. Paddle reads **Connected** in the dashboard.
2. The import banner finishes, and the **Revenue** page shows historical transactions.
3. Place a real transaction through a checkout that sets `custom_data`. It appears in **Revenue** with an AI source within a minute or two.

## Troubleshooting

**"Paddle would not let this key read transactions."** Add the `transaction.read` permission to the key and reconnect.

**"Paddle would not let this key create a notification destination."** Add the `notification_setting.write` permission and reconnect.

**"That is a Paddle sandbox key."** Traceten accepts live keys in production. Create a key from your live Paddle account.

**Connected, but revenue is not appearing.** Check the destination under **Developer tools → Notifications** is still there, active, and pointed at Traceten. If you deleted it, reconnect from the Traceten dashboard.

**Everything shows as unattributed.** Your checkout is not passing `custom_data.traceten_visitor_id`. For Paddle there is no fallback, so this is the whole of attribution.

## Next

* [How attribution works](/getting-started/how-it-works)
* [Currencies](/integrations/currencies)
