> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.partnerpage.io/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.partnerpage.io/_mcp/server.

# Partner tracking

Add one script to your site and report signups, sales and refunds. Each partner then gets credit, and commission, for the customers they bring you. Set it up in the five steps below, choosing your stack in step 2.

## How it works

1. A visitor arrives through a partner's link: the short link `/r/{key}`, or your own page with `?via={key}`.
2. The script keeps that click in the `pp_click` cookie on your domain.
3. The visitor signs up, and you report a **lead** with the click.
4. The customer pays, and your server reports a **sale**.
5. Each sale earns the partner a **commission**.

Only clicks and leads come from the browser. Sales and refunds come from your server, with your API key.

## Before you begin

* **Tracking ID**: in the PartnerPage dashboard, open Settings › Partner program › Partner links and click **Copy** next to the Tracking ID. It starts with `pp_trk_`. Replace `pp_trk_…` in every sample below with it. The Tracking ID is safe to put on your site.
* **API key**: create one under [Settings › Developers](https://directory.partnerpage.io/developers). The server samples read it from `$PARTNERPAGE_API_KEY`. A read-only key cannot report. See [Authentication](/get-started/authentication).
* Amounts are in the currency's minor unit: cents for USD, yen for JPY. For three-decimal currencies (KWD, BHD, …), send a multiple of 10.

## Set it up

#### Turn on partner links

In Settings › Partner program › Partner links, switch **Partner links** on, set your signup page in **Sends visitors to** and add your site to **Allowed hostnames**. Then save. See [Partner links settings](#partner-links-settings) for each setting.

#### Add the script to every page

Choose your stack to see where the script goes.

```html
<script async src="https://sdk.partnerpage.io/tracking.js"
  data-tracking-id="pp_trk_…"></script>
```

#### HTML

Put this in the `<head>` of every page. It keeps a visitor's partner click for the days set in **A click counts for** (90 by default) and passes it to Stripe Payment Links and Pricing Tables by itself.

#### Google Tag Manager

In Google Tag Manager, add a Custom HTML tag with this code, set it to fire on All Pages, then submit and publish the container.

#### Webflow

In Site settings › Custom code, paste this into Head code, save, and publish the site. Add your webflow\.io address to Allowed hostnames if you test there.

#### WordPress

Paste this into your site's header, with a header-code plugin or in your theme's `header.php` before `</head>`.

#### Stripe Payment Links

Put this on every page that shows a Payment Link (`buy.stripe.com`), Pricing Table or Buy Button. It adds the partner click to them as `client_reference_id`. A custom Stripe domain is not recognised. Connect Stripe in step 4.

Every hostname the script runs on must be in Allowed hostnames. Old `data-directory` tags keep working.

Check the install: open `https://<your hostname>/?via=test` and look for the `pp_click` cookie, or use **Verify installation** in Settings › Partner program › Partner links. For a hostname written as `*.yoursite.com`, check `www.yoursite.com`.

#### Track leads

After a customer signs up, report the lead. Read `clickId` from the `pp_click` cookie. Report the lead only when the cookie holds a click UUID. A customer with no cookie, or with a value that is not a UUID (such as the `test` that `?via=test` writes), has no partner to credit, so skip the call. It would answer `400` on `clickId`.

To report it from the browser, call `trackLead` once the script has loaded. It sends nothing when there is no click. Read [Security](#security) first.

```js
PartnerPage.trackLead({ customerId: "user_123", email: "jane@example.com", name: "Jane Doe" });
```

If this can run before the script has loaded, such as inline on a page that has just opened, use the queue instead:

```js
(window.PartnerPageLinks = window.PartnerPageLinks || []).push(['signup', { customerId: "user_123", email: "jane@example.com", name: "Jane Doe" }]);
```

Or from your server, with your API key. This is more reliable. See [Track a lead](/api-reference/partner-tracking/track-a-lead).

```bash
curl -X POST https://v1.api.partnerpage.io/api/deal-registration/private/v1alpha/tracking/lead/ \
  -H "Authorization: Token $PARTNERPAGE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "trackingId": "pp_trk_…",
  "clickId": "<pp_click cookie>",
  "customerId": "user_123",
  "email": "jane@example.com",
  "name": "Jane Doe",
  "eventName": "Sign up"
}'
```

| Field        | Type                                  | Required           | Description                                                        |
| ------------ | ------------------------------------- | ------------------ | ------------------------------------------------------------------ |
| `trackingId` | string                                | Yes                | Your directory's Tracking ID.                                      |
| `clickId`    | UUID or null                          | For a new customer | The value of the `pp_click` cookie.                                |
| `customerId` | string, up to 255 characters          | Yes                | Your own ID for the customer. One lead per customer per directory. |
| `email`      | email, up to 80 characters            | Yes                | The customer's email address.                                      |
| `name`       | string, up to 101 characters, or null | No                 | The customer's name.                                               |
| `company`    | string, up to 255 characters, or null | No                 | The customer's company.                                            |
| `eventName`  | string, up to 255 characters          | No                 | Your own name for the event, for example `Sign up`.                |

The answer is `201` for a new lead and `200` for a customer you already reported.

| Response field   | Type   | Description                                                             |
| ---------------- | ------ | ----------------------------------------------------------------------- |
| `id`             | UUID   | The lead.                                                               |
| `customerId`     | string | Your ID for the customer.                                               |
| `state`          | string | The lead's state: `submitted`, `approved`, `declined`, `won` or `lost`. |
| `partnerEntryId` | UUID   | The partner the lead is credited to.                                    |

#### Track sales

With Stripe connected in Settings › Partner program › Stripe, there is nothing to do. Otherwise report each paid invoice of a customer who has a lead from your server. One sale per `invoiceId`. See [Track a sale](/api-reference/partner-tracking/track-a-sale).

```bash
curl -X POST https://v1.api.partnerpage.io/api/deal-registration/private/v1alpha/tracking/sale/ \
  -H "Authorization: Token $PARTNERPAGE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "trackingId": "pp_trk_…",
  "clickId": "<pp_click cookie>",
  "customerId": "user_123",
  "amount": 5000,
  "currency": "usd",
  "invoiceId": "inv_123",
  "eventName": "Invoice paid"
}'
```

Send the `clickId` from the `pp_click` cookie whenever you have it. A sale without it, for a customer only the script on your site reported, is held until you release its commission. Approving the lead does not release it.

If you send all your invoices anyway, those of customers with no lead answer `404` `unknown_customer`. That answer is expected and safe to ignore, and it writes nothing. Those calls still count against the [rate limit](/get-started/authentication#rate-limits).

Every call is real: there is no test mode, and a sale you report can earn a commission that gets paid out. Try these calls on a test directory first.

| Field               | Type                         | Required                                         | Description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| ------------------- | ---------------------------- | ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `trackingId`        | string                       | Yes                                              | Your directory's Tracking ID.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        |
| `customerId`        | string, up to 255 characters | Yes                                              | Your ID for the customer, as in the lead.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `amount`            | integer, 1 to 10^15          | Yes                                              | What the customer paid, tax included, in the currency's minor unit.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `currency`          | string                       | Yes                                              | The three-letter ISO 4217 code, for example `usd`. Codes whose minor unit cannot be read safely are refused: ISK, UGX, UYI, CLF, UYW, withdrawn currencies, and metal, fund and test codes such as XAU and XXX.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `invoiceId`         | string, up to 255 characters | Yes                                              | Your own ID for the invoice. One sale per invoice.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `taxAmount`         | integer, 0 up to `amount`    | No                                               | The tax part of `amount`, in the same unit. Defaults to 0.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `paidAt`            | ISO 8601 datetime or null    | No                                               | When the customer paid; defaults to now. Send it when you report late or import past invoices, and send past invoices oldest first: the first sale reported starts the earning window. An earlier sale sent later moves the start back to it while none of the lead's commissions is paid or on a payout, and the result matches an oldest-first import: sales after the moved window earn nothing, and earlier sales it now covers earn. If that would cancel commissions later sales already earned, the call is refused with `409` `paid_at_voids_earnings`, so a mistyped date can't cancel them; send `voidLaterEarnings: true` to move it anyway. After that it is recorded and earns nothing. |
| `voidLaterEarnings` | boolean                      | No                                               | Set `true` to let an earlier `paidAt` move the earning window back even though it cancels the commissions of later sales. Defaults to `false`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `eventName`         | string, up to 255 characters | No                                               | Your own name for the event, for example `Invoice paid`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `clickId`           | UUID or null                 | For a new customer; send it whenever you have it | The value of the `pp_click` cookie. With a `clickId`, a sale for a customer you have not reported creates the lead, and the lead's own click confirms a lead the browser reported.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |

The answer is `201` for a new sale and `200` for an invoice you already reported with the same values. A retry may leave out `paidAt` and `eventName`; if it sends either with another value than the first call, it answers `409` `invoice_id_reused` and changes nothing.

| Response field | Type           | Description                                                                                                                                    |
| -------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `id`           | UUID           | The sale.                                                                                                                                      |
| `leadId`       | UUID           | The customer's lead.                                                                                                                           |
| `invoiceId`    | string         | Your ID for the invoice.                                                                                                                       |
| `amount`       | integer        | The amount, in the currency's minor unit.                                                                                                      |
| `currency`     | string         | The currency code, in capitals.                                                                                                                |
| `taxAmount`    | integer        | The tax, in the same unit.                                                                                                                     |
| `commission`   | object or null | The commission this sale earned: `id`, `amount` (a decimal string, such as `"5.00"`), `currency` and `status`. Null when the sale earned none. |

To report a refund, send the `invoiceId` of the sale. See [Refund a sale](/api-reference/partner-tracking/refund-a-sale). Because `amount` is a total, you can safely send a call again: the same total adds nothing, and a higher total adds the difference.

```bash
curl -X POST https://v1.api.partnerpage.io/api/deal-registration/private/v1alpha/tracking/refund/ \
  -H "Authorization: Token $PARTNERPAGE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"trackingId":"pp_trk_…","invoiceId":"inv_123","amount":5000}'
```

| Field        | Type                         | Required                                | Description                                                                                                                                                                                                                                                                  |
| ------------ | ---------------------------- | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `trackingId` | string                       | Yes                                     | Your directory's Tracking ID.                                                                                                                                                                                                                                                |
| `invoiceId`  | string, up to 255 characters | Yes                                     | The invoice ID of the sale.                                                                                                                                                                                                                                                  |
| `amount`     | integer, 1 to 10^15, or null | No                                      | The total refunded on this invoice so far, in the sale's minor unit; omit for the whole sale.                                                                                                                                                                                |
| `taxAmount`  | integer, 0 to 10^15, or null | For a partial refund of a sale with tax | The total tax refunded on this invoice so far, in the same unit; required for a partial refund of a sale with tax. It is kept for your records only: the commission goes down by the refunded share of the sale (`amount` over the sale's `amount`), whatever the tax split. |

The answer is `200`.

| Response field   | Type         | Description                                                       |
| ---------------- | ------------ | ----------------------------------------------------------------- |
| `saleId`         | UUID         | The sale.                                                         |
| `invoiceId`      | string       | Your ID for the invoice.                                          |
| `refundedAmount` | integer      | The total refunded on this invoice, in the currency's minor unit. |
| `refundId`       | UUID or null | The new refund. Null when this total was already recorded.        |

#### Check your figures

Clicks, leads, conversions, revenue and commissions per partner appear in the dashboard under Partner program › Partners.

## Partner links settings

These settings are in Settings › Partner program › Partner links.

* **Partner links**: on or off. Partners earn on the leads and sales their link brings. To switch them on, set the signup page and save.
* **Link format**: the short link `yourdirectory.com/r/{partner-key}` (recommended), or the query parameter `yoursite.com/signup?via={partner-key}`. The query parameter needs your signup page's site in Allowed hostnames, and the script verified there with Verify installation.
* **Sends visitors to**: your signup page, the destination of every partner link. It must be a public `https://` address.
* **Partner key**: what `{partner-key}` is for each partner: the partner's name (such as `blue-fern`) or the partner's ID.
* **A click counts for**: 1 to 365 days, 90 by default. A customer who signs up within this window is credited to the partner.
* **Tracking ID**: needed by the script and by the API calls. Click **Copy** to copy it. **Regenerate…** makes a new one: your script and API calls stop working until you replace the old ID everywhere.

### Allowed hostnames

The script tracks clicks only on these hostnames.

* Add each one as `example.com` or `*.example.com`, up to 20 hostnames.
* `*.example.com` covers the subdomains of `example.com`, not `example.com` itself.
* A wildcard over a shared suffix, such as `*.co.uk` or `*.webflow.io`, is refused because it covers other companies' sites. Use your own domain, like `*.yoursite.webflow.io`.
* IP addresses and local names (`localhost`, `.local`, `.internal`) are refused.

### Verify installation

Choose a saved hostname and click **Verify installation**. PartnerPage reads that hostname's home page and looks for the script with your Tracking ID. A `*.yoursite.com` hostname is checked at `www.yoursite.com`. The check tracks nothing.

| Result          | Meaning                                                                                                                  |
| --------------- | ------------------------------------------------------------------------------------------------------------------------ |
| Installed       | The script with your Tracking ID is on the page.                                                                         |
| Old Tracking ID | The page has a script with another Tracking ID. Copy the current one.                                                    |
| Old script      | The page has the old script. Replace it with the tag in step 2.                                                          |
| Not found       | No script with your Tracking ID on the home page. A script added through Google Tag Manager can't be seen by this check. |
| No answer       | The site did not answer.                                                                                                 |

## Security

When leads come from the browser (`trackLead`), send `clickId` on the customer's sales, or report the lead from your server with its `clickId`: a lead the browser reported is confirmed by its own click, and a lead planted from the browser is moved to the real click. The move happens only while the lead still awaits your approval, has no sale reported from your server and has not started earning, and only while partner links are on; after that the lead stays with its partner, and a call naming another click changes nothing. Until a call from your server names the lead's click, the commissions on its sales stay on hold, even after you approve the lead; you release them on the commission. Use a `customerId` nobody can guess (not an email or a sequential number).

Keep your API key on your server, never in browser code. See [Authentication](/get-started/authentication).

## Rules

* Every sale inside the earning window earns at the rate locked when the lead was enrolled, at its first sale, except:
  * a declined lead earns nothing;
  * a lead that already earned on its won deal value keeps that commission and its sales earn nothing more;
  * a partner whose listing was removed earns nothing on later sales;
  * a lead is enrolled only by a sale reported while the commission program and commissions are on; sales reported before that earn nothing.
* An `invoiceId` is counted once.
* A refund before payout lowers the commission, and after payout becomes a recovery.
* A won deal without a sale is a conversion but not revenue.
* Read-only keys cannot report. See [Full-access and read-only keys](/get-started/authentication#full-access-and-read-only-keys).
* There is no test mode. Every call is real, so try these calls on a test directory first.

## Errors

Errors have three shapes, so check the status first:

* A problem with a field answers with that field, as `{"amount": ["…"]}`. A body that is not a JSON object answers `{"nonFieldErrors": ["…"]}`.
* An error with a code in the table answers `{"code": "…", "message": "…"}`.
* A 401, a 403, a 429 and a body that cannot be parsed answer `{"detail": "…"}`.

| Status | Call                              | Field or code                                 | Meaning                                                                                                                                                                            |
| ------ | --------------------------------- | --------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400    | All                               | `{"<field>": ["…"]}`                          | A field is missing or malformed.                                                                                                                                                   |
| 400    | All                               | `{"nonFieldErrors": ["Send a JSON object."]}` | The body is not a JSON object.                                                                                                                                                     |
| 400    | All                               | `{"detail": "…"}`                             | The body cannot be parsed as JSON.                                                                                                                                                 |
| 400    | Lead                              | `clickId`                                     | Send clickId for a customer PartnerPage does not know yet. A customer with no click is not reported.                                                                               |
| 400    | Lead, and sale for a new customer | `clickId`                                     | The click is unknown, belongs to another directory, or is older than the directory's click window.                                                                                 |
| 400    | Lead, and sale for a new customer | `partner_links_off`                           | Partner links are off.                                                                                                                                                             |
| 400    | Sale, refund                      | `amount`, `taxAmount`                         | This amount is too large.                                                                                                                                                          |
| 400    | Sale                              | `taxAmount`                                   | taxAmount can't be more than amount.                                                                                                                                               |
| 400    | Sale, refund                      | `amount`, `taxAmount`                         | For this currency, send a multiple of 10.                                                                                                                                          |
| 400    | Sale                              | `currency`                                    | Send a supported three-letter ISO 4217 currency code.                                                                                                                              |
| 400    | Sale                              | `paidAt`                                      | paidAt can't be in the future or before the customer's click and lead.                                                                                                             |
| 400    | Refund                            | `amount`                                      | Refunds can't add up to more than the sale.                                                                                                                                        |
| 400    | Refund                            | `taxAmount`                                   | Send taxAmount for a partial refund of a sale with tax.                                                                                                                            |
| 400    | Refund                            | `taxAmount`                                   | The refunded tax can't be less than already refunded, more than the sale's tax or more than the refunded amount.                                                                   |
| 401    | All                               | `{"detail": "…"}`                             | The API key is missing or invalid.                                                                                                                                                 |
| 403    | All                               | `{"detail": "…"}`                             | The Tracking ID belongs to another organization, the key cannot report, or Partner leads are off for this directory.                                                               |
| 404    | All                               | `unknown_tracking_id`                         | No directory has this tracking ID.                                                                                                                                                 |
| 404    | Sale                              | `unknown_customer`                            | No lead carries this customer ID. Send clickId to create it. Expected for a customer no partner brought; nothing is written.                                                       |
| 404    | Refund                            | `unknown_invoice`                             | No sale carries this invoiceId.                                                                                                                                                    |
| 409    | Sale                              | `invoice_id_reused`                           | This invoiceId was reported with other values.                                                                                                                                     |
| 409    | Sale                              | `lead_paid_through_stripe`                    | Stripe reports this customer's payments.                                                                                                                                           |
| 409    | Sale                              | `salesforce_push_running`                     | This Partner lead is being sent to Salesforce. Report the payment again in a minute.                                                                                               |
| 409    | Refund                            | `refund_below_recorded`                       | More than this was already refunded on this invoice.                                                                                                                               |
| 409    | Sale                              | `payout_sending`                              | An earlier sale would move the earning window while a payout to the partner is being sent. Report the payment again in a minute.                                                   |
| 409    | Sale                              | `paid_at_voids_earnings`                      | This `paidAt` would move the earning window back and cancel commissions later sales earned. Check the date, or send it again with `voidLaterEarnings: true`. Nothing was recorded. |
| 409    | Refund                            | `payout_sending`                              | A payout to this partner is being sent. Report the refund again in a minute.                                                                                                       |
| 429    | All                               | `{"detail": "…"}`                             | Over the limit of 1,000 requests per hour per API key per endpoint. Wait and try again. See [Rate limits](/get-started/authentication#rate-limits).                                |