# Paddle

Connect Paddle Billing with a read-only API key and a notification destination so ProductLense records transactions and adjustments net of Paddle's cut.

ProductLense reads your Paddle Billing customers, subscriptions, completed transactions and adjustments. Paddle is a merchant of record, so ProductLense records what the customer paid and treats everything Paddle keeps (its fee and the tax it remits) as the fee. ProductLense only reads: it never writes to your Paddle account, never refunds, cancels or changes anything.

## What you need

| Field | Secret | Where to find it |
|---|---|---|
| API key | Yes | Paddle → Developer tools → Authentication. |
| Webhook secret key | Yes | The `pdl_ntfset_...` secret shown when you create the notification destination pointing at ProductLense. |

Every plan includes one billing connection per site, Free too; Growth and Scale allow any number. See [plans and limits](/docs/account/plans-and-limits).

> [!NOTE]
> ProductLense connects to the Paddle Billing production API (`api.paddle.com`). Paddle Classic and sandbox accounts are not supported.

## Permissions

The API key needs read access to customers, subscriptions, transactions and adjustments. ProductLense calls `GET /customers`, `/subscriptions`, `/transactions` and `/adjustments` and nothing else.

## Set up

1. In Paddle, open Developer tools → Authentication and create an API key with read access to the four resources above.
2. In ProductLense, open your site's Settings → Billing connections → Paddle, paste the key and click Connect and import history.
3. Copy the webhook URL from the connection card.
4. In Paddle, open Developer tools → Notifications → New destination, paste the URL and subscribe to the events below.
5. Copy the destination's secret key into the Paddle connection in ProductLense.

## Webhook

URL: `https://api.productlense.app/api/webhooks/paddle/<connection-id>` (ProductLense shows the exact URL after you save).

Events ProductLense uses:

- `transaction.completed`, `transaction.updated`
- `adjustment.created`, `adjustment.updated`
- `subscription.created`, `subscription.updated`, `subscription.canceled`, `subscription.activated`, `subscription.paused`, `subscription.resumed`, `subscription.past_due`
- `customer.created`, `customer.updated`

Verification: the `Paddle-Signature` header (timestamp plus `h1` HMAC-SHA256 with your secret key). Deliveries older than 5 minutes are rejected.

## What ProductLense reads

- **Customers**: email, name and custom data.
- **Subscriptions**: status, billing interval and amount.
- **Payments**: completed transactions.
- **Refunds and chargebacks**: refund and chargeback adjustments.
- **History**: the last 12 months on first connect, then a nightly reconcile at 03:00 UTC from the last sync minus 48 hours.

## How fees are derived

```text
fee = grand_total − earnings
```

That is Paddle's fee plus the tax it collects and remits, so net revenue equals what Paddle pays out to you. When a transaction has no earnings figure, ProductLense uses Paddle's fee plus tax. Refund and chargeback adjustments are netted at your earnings share, and a chargeback adds its chargeback fee, so a full refund cancels the original charge exactly. Fees are reported by Paddle, not estimated.

## Match payments to visitors

ProductLense reads the user id from the customer's `custom_data`: `pl_user_id`, `user_id`, `userId` or `uid`. Use the same id you pass to `identify`. A ProductLense visitor id in `pl_visitor_id` or `visitor_id` is also accepted. Without either, ProductLense falls back to the customer email, matched to the `email` trait sent with `identify`, or to a user id that is that email.

```js title="When you create the Paddle customer (Node, Paddle API)" {6}
await fetch('https://api.paddle.com/customers', {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.PADDLE_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    email: user.email,
    custom_data: { pl_user_id: user.id },
  }),
});
```

> [!NOTE]
> ProductLense reads the user id from the Paddle customer, not from checkout `customData` on a transaction. Set it on the customer, or ProductLense falls back to the email.

See [how payments are attributed](/docs/revenue/attribution).
