---
id: "api-reference/attribution"
type: "doc"
url: "https://affitor.com/docs/api-reference/attribution"
---
# Attribution Mechanics

> How Affitor assigns credit to partners — the cookie, the click ID, the attribution windows, and what happens on re-click.

> **Beta** — The Affitor SDKs and CLI are in active development. The documented happy-path works; edge cases may change. Report issues on GitHub.

When a visitor clicks an affiliate link, Affitor creates a customer record for that click and the SDK writes a first-party cookie with its `click_id`. By default, the partner whose click created the customer keeps that customer: a later click from another partner in the same browser is logged, but it does not take the customer over. Leads and sales for that customer resolve to that first partner.

---

## Attribution model

| Setting | Default | Range | Description |
|---------|---------|-------|-------------|
| Attribution window | `60` days | 1 – 365 | Maximum age of the click at the customer's signup. Also used as the cookie lifetime |

Each program has **one** attribution window, set in your program settings. The click endpoint returns it as `cookie_window_days`, and the SDK uses it as the lifetime of the cookie it writes, so no SDK update is needed when you change the value in the dashboard.

Program settings have no option for last-click or multi-touch (linear) credit. Credit goes to the partner whose click created the customer, as described above.

---

## The `affitor_click_id` cookie

When a visitor lands on a URL containing `?aff=<code>`, the SDK calls `POST /api/v1/track/click` and stores two first-party cookies:

| Cookie | Purpose |
|--------|---------|
| `affitor_click_id` | The primary attribution token. Passed to lead and sale calls. |
| `affitor_aff_url` | The full landing URL used to detect partner changes on re-click. |

A legacy `customer_code` cookie is written alongside `affitor_click_id` for backwards compatibility with older integrations. On read, if only `customer_code` exists, the SDK migrates its value into `affitor_click_id` automatically.

The cookie lifetime is the program's attribution window (default **60 days**), taken from the `cookie_window_days` value in the click response.

---

## Cross-subdomain cookie sharing

The SDK auto-detects the broadest writable domain so attribution follows a visitor across subdomains (e.g. `app.example.com` and `www.example.com` share the same cookie).

**Detection algorithm:**

1. Split `window.location.hostname` into parts.
2. Starting from the registrable domain (e.g. `.example.com`), attempt to write a probe cookie with each candidate domain.
3. The first domain that accepts the write becomes the cookie domain for the session and is cached in `cookieDomain`.
4. `localhost` and bare IP addresses are excluded — no domain is set, so the cookie is host-only.

You can skip auto-detection by setting the cookie domain on the tracker script tag:

```html
<script
  src="https://api.affitor.com/js/affitor-tracker.js"
  data-affitor-program-id="123"
  data-affitor-cookie-domain=".example.com"
></script>
```

The `@affitor/sdk` types do not include a cookie domain option (`loadAffitor()` takes `programId` plus `debug`, `env` and `scriptUrl`; `init()` takes only `programId` and `debug`), so use the script attribute when you need one.

---

## Re-click from another partner

If a visitor already has an `affitor_click_id` cookie and clicks a **different** partner's link, the SDK sends the new click together with the existing `click_id` (as `existing_click_id`). The server finds the customer that already owns that `click_id` and keeps it: the new click is still recorded and counted for the new partner (a click event, plus that link's and partner's click counts), but the customer, its `click_id` and its partner do not change, so later leads and sales stay with the original partner.

| Scenario | Outcome |
|----------|---------|
| No existing cookie, `?aff=` present | New click tracked, new customer created, new `affitor_click_id` written |
| Same partner link clicked again | Existing cookie reused — no new click event |
| Different partner link clicked | New click counted for the new partner; the customer and its `click_id` stay with the original partner |
| No `?aff=` in URL, cookie present | Existing attribution restored from cookie silently |
| No `?aff=` in URL, no cookie | No `click_id`. A lead call without a `click_id` or a known `customer_key` returns `400` and nothing is recorded |

---

## How the attribution window is checked

There is one window per program (default **60 days**). It is used in two places:

- **Cookie lifetime** — the `affitor_click_id` cookie expires after that many days.
- **Click → signup check** — when the customer's first payment is attributed, the click must be at most that many days older than the customer's signup. If no signup was recorded, the click's age is measured at that first payment instead.

The check runs once, at the customer's first payment. A customer who signed up inside the window can buy later and still be attributed, and once the first payment is attributed, later renewals and repeat purchases are not checked against the window again.

```
Click ──── at most N days ────► Signup ─────────────► First payment (window checked here)
                                                        │
                                                        └──► Renewals (not re-checked)
```

---

## Recurring revenue attribution

For subscription products, every recurring charge can generate a commission within the commission duration below. Once the first payment is attributed, the attribution window is not checked again for renewals.

| Commission type | `default_duration_months` | Behavior |
|-----------------|--------------------------|---------|
| `cps_recurring` | e.g. `12` | Commission paid for up to N months of renewals |
| `cps_lifetime` | `null` | Commission paid on every renewal indefinitely |
| `cps_one_time` | `0` | Commission paid once on the first payment only |

The Stripe webhook handler uses `is_recurring: true` and `subscription_id` on the `/api/v1/track/sale` call to match renewals back to the original attributed partner. No action is required from the SDK on renewal — Stripe autocapture handles it automatically when the webhook is connected.

---

## Program-level attribution fields

These program fields control attribution and commissions. The attribution window is set as one number in your program settings:

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `attribution_window_days` | `integer` | `60` | Maximum click age at signup, and the cookie lifetime (1–365 days) |
| `default_commission_type` | `enum` | — | `cpc`, `cpl`, `cps_one_time`, `cps_recurring`, `cps_lifetime` |
| `default_duration_months` | `integer` | — | Recurring commission duration; `null` = lifetime, `0` = one-time |
| `default_hold_period_days` | `integer` | `15` | Days a commission stays in hold before becoming payable (max 31) |

---

## Related pages

<FeatureCards>
  <FeatureCard icon="mouse-pointer-click" title="Track Click" href="/docs/api-reference/track-click">Endpoint that creates the click record and returns the click ID.</FeatureCard>
  <FeatureCard icon="credit-card" title="Track Sale" href="/docs/api-reference/track-sale">Record a sale and trigger commission creation against the attributed click.</FeatureCard>
  <FeatureCard icon="activity" title="Status" href="/docs/api-reference/status">Check whether tracking is wired up correctly for your program.</FeatureCard>
</FeatureCards>
