---
id: "brand/tracking/payment-tracking-stripe"
type: "doc"
url: "https://affitor.com/docs/brand/tracking/payment-tracking-stripe"
kind: "guide"
updated: "2026-10-18"
---
# Payment Tracking

> Track completed sales with server-side tracking or Stripe integration

Payment tracking records revenue after a customer pays. Affitor supports two sale-tracking paths:

1. **Server-side tracking**: your backend calls POST /api/v1/track/sale when revenue is finalized
2. **Stripe integration**: you keep charging in your own Stripe account, connect it to the program, and Affitor attributes sales from the payment events its own Stripe Connect endpoint receives

---

## Choose the Right Path

| Path | Best for |
|------|----------|
| **Server-side tracking** | custom backend, any payment provider, Paddle/LemonSqueezy/manual revenue events |
| **Stripe integration** | existing Stripe Checkout integration |

Use server-side tracking when you control the exact moment revenue is finalized on your server. Use Stripe integration when you take payments on Stripe: connect your Stripe account under **Program settings** › **Payments**, and Affitor receives its payment events through Stripe Connect, with no webhook for you to add.

---

## Option A — Server-side tracking

### Endpoint

```bash
POST https://api.affitor.com/api/v1/track/sale
Authorization: Bearer YOUR_PROGRAM_API_KEY
Content-Type: application/json
```

### Minimum real-sale payload

```json
{
  "transaction_id": "txn_abc123",
  "customer_key": "user_123",
  "amount_cents": 9999,
  "currency": "USD"
}
```

### Full example

```js
await fetch('https://api.affitor.com/api/v1/track/sale', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.AFFITOR_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    transaction_id: order.id,
    customer_key: currentUser.id,
    click_id: req.cookies.affitor_click_id,
    amount_cents: order.totalCents,
    currency: 'USD',
    sale_type: 'payment',
    line_items: [
      { name: 'Pro Plan', amount_cents: order.totalCents, quantity: 1 },
    ],
  }),
});
```

### Runtime rules

- `Authorization: Bearer <program_api_key>` is required
- `transaction_id` is required and must be unique
- `amount_cents` is required and must be a positive number
- Affitor must be able to resolve the customer via:
  - `customer_key`
  - or `click_id`
- If the same `transaction_id` is sent twice, Affitor returns `409 Conflict`

### Response

```json
{
  "success": true,
  "sale_id": 42,
  "commission_id": 18,
  "message": "Sale tracked successfully"
}
```

:::tip

Use server-side tracking if you want one server-controlled source of truth for revenue events across all payment providers.

:::

---

## Option B — Stripe integration

With the Stripe integration, you keep charging customers through your own Stripe account; Affitor does **not** become merchant of record. Connect the account under **Program settings** › **Payments** ([Connect a Stripe account](/docs/brand/tracking/connect-stripe-account)). Stripe then sends the account's payment events to Affitor's own Connect endpoint, so you do not add a webhook. Affitor attributes the conversion from those events and the metadata on each payment, then invoices you through its invoice billing workflow.

### Recommended metadata fields

| Field | Required | Description |
|-------|----------|-------------|
| `affitor_click_id` | Recommended | tracked click ID from the browser cookie |
| `affitor_customer_key` | Recommended | your internal customer/user ID |
| `program_id` | Recommended | your Affitor program ID. Without it, the sale goes to the program of the customer Affitor matches. With it, only a customer of that program can be credited, which matters when one Stripe account serves several programs |

### One-time payment example

```js
function getCookie(name) {
  const value = `; ${document.cookie}`;
  const parts = value.split(`; ${name}=`);
  if (parts.length === 2) return parts.pop().split(';').shift();
  return null;
}

const clickId = getCookie('affitor_click_id');

const session = await stripe.checkout.sessions.create({
  line_items: [{ price: 'price_xxx', quantity: 1 }],
  mode: 'payment',
  success_url: 'https://yoursite.com/success',
  cancel_url: 'https://yoursite.com/cancel',
  metadata: {
    affitor_click_id: clickId,
    affitor_customer_key: currentUser.id,
    program_id: 'YOUR_PROGRAM_ID',
  },
});
```

### Subscription example

For subscriptions, `subscription_data.metadata` is the one that counts: Affitor records a subscription's payments from its invoices, which carry the subscription's metadata, not the Checkout Session's `metadata`.

```js
const session = await stripe.checkout.sessions.create({
  line_items: [{ price: 'price_xxx', quantity: 1 }],
  mode: 'subscription',
  success_url: 'https://yoursite.com/success',
  cancel_url: 'https://yoursite.com/cancel',
  metadata: {
    affitor_click_id: clickId,
    affitor_customer_key: currentUser.id,
    program_id: 'YOUR_PROGRAM_ID',
  },
  subscription_data: {
    metadata: {
      affitor_click_id: clickId,
      affitor_customer_key: currentUser.id,
      program_id: 'YOUR_PROGRAM_ID',
    },
  },
});
```

### Why subscriptions need `subscription_data.metadata`

- One-time payments are handled from Stripe checkout session completion
- Subscription commissions, for the first payment and each renewal, are created from the invoice events (`invoice.payment_succeeded`, `invoice.paid`)
- An invoice carries the subscription's metadata (`subscription_data.metadata`), not the Checkout Session's `metadata`, so that is where the click reaches the subscription
- Renewals do not depend on it: once the first payment is attributed, Affitor saves the buyer's Stripe customer ID and matches each renewal by the buyer's email or that Stripe customer ID
- Without it, the subscription is attributed only if the buyer's email or Stripe customer ID matches a customer Affitor already has

---

## Pay-as-you-go / Repeated One-Time Purchases

Some products charge per usage event rather than via a subscription — independent checkout sessions for things like API credit top-ups, usage-based batches, or metered add-ons.

### How it works

Every `checkout.session.completed` event with `mode: 'payment'` is processed independently by Affitor. Each one goes through the full attribution and commission calculation flow.

:::note

**The invoice events are not involved here.**
Affitor uses the invoice events (`invoice.payment_succeeded`, `invoice.paid`) only for subscriptions: a subscription's first payment and each renewal. Pay-as-you-go purchases use `checkout.session.completed`, one event per purchase, each attributed and commissioned separately.

:::

### Attribution across multiple purchases

The first purchase is straightforward — if the customer clicked an affiliate link recently, `affitor_click_id` is still in the browser cookie.

Later purchases may arrive weeks or months after that click, and the cookie may be gone. Attribution still works if you pass a stable `affitor_customer_key` in every checkout's metadata. Affitor finds the customer by that key (saved on the customer when its signup lead settled) and links all subsequent purchases to the same partner.

**Metadata to set on every checkout, including repeat purchases:**

```js
const session = await stripe.checkout.sessions.create({
  line_items: [{ price: 'price_xxx', quantity: 1 }],
  mode: 'payment',
  success_url: 'https://yoursite.com/success',
  cancel_url: 'https://yoursite.com/cancel',
  metadata: {
    affitor_click_id: getCookie('affitor_click_id') || '', // may be empty, that is expected
    affitor_customer_key: currentUser.id,                  // links repeat purchases; must be stable
    program_id: 'YOUR_PROGRAM_ID',
  },
});
```

:::tip

`affitor_click_id` may be absent for repeat purchases — that is expected and not an error. Affitor will fall back to `affitor_customer_key` for attribution. Still send it when it is available.

:::

### Pre-requisite: signup tracking

Affitor creates the customer record when the partner click is tracked. Signup tracking saves your `customer_key` on it, so a later checkout that carries only `affitor_customer_key` finds the customer. Send the lead from your server with the `click_id` and your user ID ([Track Lead](/docs/api-reference/track-lead)), or call the browser helper at user registration:

```js
await window.affitor.signup(currentUser.id, currentUser.email);
```

On a program that requires server-verified leads (the default), a browser signup is provisional: the key is saved only when your server sends the lead or a Stripe Checkout payment for that customer arrives ([Lead auth policy](/docs/api-reference/track-lead#lead-auth-policy)). If the key was never saved, a later purchase that carries no click is attributed only if the buyer's email or Stripe customer ID matches a customer Affitor already has.

### Attribution does not expire

Once an affiliate-customer record is linked to a partner, that link persists indefinitely — there is no time-based cut-off on the customer-to-partner relationship.

If your commission **Type** is **Recurring**, the months in **For** (**Program settings** › **Commission**) limit how long a partner earns on a customer. They count from the customer's first click on the partner link, not from signup or the first payment. Later payments are still recorded as sales, with a $0 commission: the limit controls what a partner earns, not whether purchases are recognized.

### Troubleshooting pay-as-you-go

| Symptom | Likely cause |
|---------|--------------|
| First purchase attributed, later purchases are not | `affitor_customer_key` is missing or inconsistent across checkouts |
| No purchases attributed at all | Checkouts carry no `affitor_click_id` (the tracker is missing on the checkout page, or your Checkout Session does not set it), or Stripe is not connected to the program |
| Commissions are $0 after N months | The commission is **Recurring** with **For** set to N months, counted from the customer's first click: later payments are still recorded, and earn $0. Expected, not a bug |

---

## How Affitor Resolves Attribution from Stripe

When a Stripe event arrives, Affitor resolves attribution through a fallback chain.

### Simplified lookup order

1. click metadata (`affitor_click_id`)
2. customer email matching
3. Stripe customer ID
4. the click in `client_reference_id`, used only when the metadata has no click (the Affitor tracker writes it on Payment Links, the Buy Button and the Pricing Table)
5. customer key (`affitor_customer_key`)

This is why the best production setup is:
- install the tracker first
- send a stable internal customer ID at signup
- pass the same internal ID into Stripe metadata later

---

## Compatibility Notes

The runtime accepts legacy metadata aliases for backward compatibility, such as older click/customer-key field names. Do **not** use them in new integrations.

For new implementations, use only:
- `affitor_click_id`
- `affitor_customer_key`
- `program_id`

---

## Test Mode for Server-side tracking

Send a test sale event without creating a real commission or platform fee.

```bash
curl -X POST https://api.affitor.com/api/v1/track/sale \
  -H "Authorization: Bearer YOUR_PROGRAM_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "additional_data": { "test_mode": true },
    "amount_cents": 9999,
    "currency": "USD",
    "sale_type": "payment"
  }'
```

### Test mode behavior

- still requires the Bearer token
- creates a test sale event with `is_test: true`
- does **not** create commissions, platform fees, or production metrics
- does not require an existing customer record

---

## Troubleshooting

### Server-side tracking returns `401`

Check:
- the `Authorization` header is present
- the value is `Bearer YOUR_PROGRAM_API_KEY`
- the API key belongs to the same program you intend to track

### Server-side tracking returns `409`

Check:
- `transaction_id` is unique
- retries are not resending the same completed sale without idempotency control on your side

### Stripe one-time sale not attributed

Check:
- `program_id` in metadata, if you set it, is this program's ID: with another ID, this program's customers are not credited
- `affitor_click_id` and `affitor_customer_key` are passed when available
- signup tracking used the same internal customer ID earlier
- the program's Stripe card under **Program settings** › **Payments** reads **Connected**. Affitor receives the events through Stripe Connect, so there is no webhook of yours to check ([Connect a Stripe account](/docs/brand/tracking/connect-stripe-account))

### Stripe pay-as-you-go second/third purchase not attributed

Check:
- `affitor_customer_key` is present in the metadata of every checkout session (not just the first)
- the value is exactly the same stable ID used at signup — any mismatch means the customer record cannot be found
- the customer's first lead or purchase was attributed through a click: Affitor saves `affitor_customer_key` on the customer only then, so a later checkout with only the key finds no customer if the first one carried no click

### Stripe renewals not attributed

Check:
- the subscription's first payment was attributed: renewals follow it through the buyer's email or Stripe customer ID
- `subscription_data.metadata` contains all three fields
- the program's Stripe card under **Program settings** › **Payments** reads **Connected**, and the subscription bills on that Stripe account. Affitor receives the invoice events (`invoice.payment_succeeded`, `invoice.paid`) through Stripe Connect; you do not add a webhook
- the same customer key was used at signup and in Stripe metadata

---

## Related Guides

- [3-Step Integration Guide](/docs/brand/tracking/quickstart-integration/)
- [Payment Flow](/docs/brand/tracking/payment-flow/)
- [Testing Integration](/docs/brand/tracking/testing-integration/)
