Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions content/docs/api-reference/attribution.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: Attribution Mechanics
description: How Affitor assigns credit to partners — the cookie, the click ID, the attribution windows, and what happens on re-click.
kind: concept
last_updated: 2026-10-17
last_updated: 2026-10-18
---

> **Beta** — The Affitor SDKs and CLI are in active development. The documented happy-path works; edge cases may change. Report issues on GitHub.
Expand Down Expand Up @@ -104,7 +104,7 @@ For subscription products, every recurring charge can generate a commission with
| `cps_lifetime` | `null` | Commission paid on every renewal indefinitely |
| `cps_one_time` | `0` | Commission paid once on the first payment only |

With Stripe connected, renewals need no call from you: Affitor records each renewal from `invoice.payment_succeeded` and matches it to the partner through the click in the subscription's metadata, or else by the buyer's email or Stripe customer ID (saved when the first payment was attributed). If you report sales yourself with `/api/v1/track/sale`, a renewal is attributed like any sale, by `customer_key` and then `click_id`; `is_recurring: true` and `subscription_id` mark it as a subscription charge.
With Stripe connected, renewals need no call from you: Affitor records each renewal from the invoice events (`invoice.payment_succeeded`, `invoice.paid`) and matches it to the partner through the click in the subscription's metadata, or else by the buyer's email or Stripe customer ID (saved when the first payment was attributed). If you report sales yourself with `/api/v1/track/sale`, a renewal is attributed like any sale, by `customer_key` and then `click_id`; `is_recurring: true` and `subscription_id` mark it as a subscription charge.

---

Expand Down
4 changes: 2 additions & 2 deletions content/docs/api-reference/errors.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ When you contact support about a failed request, include the `X-Request-ID`. It
| Status | Error message | Condition | Fix |
|--------|---------------|-----------|-----|
| `401` | `Missing or malformed Authorization header. Expected: Bearer <token>` | `Authorization` header missing or does not start with `Bearer ` | Add `Authorization: Bearer YOUR_PROGRAM_API_KEY` to the request |
| `401` | `Missing API token` | Header present but the token is empty after trimming (sale only; refund answers `Invalid API token`) | Check for trailing spaces or an empty env variable |
| `401` | `Missing API token` (sale), `Empty Bearer token` (refund) | Header present but the token is empty after trimming | Check for trailing spaces or an empty env variable |
| `401` | `Invalid API token` | The token matches no program: a wrong key, a replaced key, or a key from a different program | Compare what you are sending against the full key you stored when it was issued. If you no longer hold that value, ask a workspace owner to [regenerate the key](/docs/brand/settings#step-3-regenerate-the-key) and deploy the new one — the dashboard only ever shows it masked afterwards |

The rows above apply to the endpoints that require a Bearer token: `POST /api/v1/track/sale` and `POST /api/v1/track/refund`.
Expand All @@ -140,7 +140,7 @@ The CLI's program-key routes word these differently; see [CLI endpoint errors](#

The success body is identical in both modes, so a `2xx` on its own does not prove your key was read. The one exception is a program configured with `lead_auth: server_required`, where a browser-mode lead comes back with `"provisional": true`.

Such a lead settles when your backend sends the server-side lead. A qualifying Stripe payment webhook can also settle it, through the same path the lead endpoint uses, but only when Affitor can resolve the customer from that event — one-off payments through `checkout.session.completed`, and subscriptions through `invoice.payment_succeeded`, because subscription checkouts are deliberately left to the invoice event. Send the server-side lead rather than relying on a payment to settle it.
Such a lead settles when your backend sends the server-side lead. A qualifying Stripe payment webhook can also settle it, through the same path the lead endpoint uses, but only when Affitor can resolve the customer from that event: one-off payments through `checkout.session.completed`, and subscriptions through the invoice events (`invoice.payment_succeeded`, `invoice.paid`), because subscription checkouts are deliberately left to the invoice event. Send the server-side lead rather than relying on a payment to settle it.

---

Expand Down
4 changes: 2 additions & 2 deletions content/docs/api-reference/integrations/node-express.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: "Node / Express"
description: "Server-side Affitor integration with @affitor/sdk/server — track signups and sales from your Express backend."
kind: guide
last_updated: 2026-10-17
last_updated: 2026-10-18
---

This guide wires Affitor into a Node.js / Express app. Because Express is a pure server runtime, the click is captured by a frontend script tag, then the `affitor_click_id` is forwarded to your server so all tracking calls — lead and sale — happen server-side with `@affitor/sdk/server`.
Expand Down Expand Up @@ -111,7 +111,7 @@ Sales must always be tracked server-side — never from the browser.

### Option A — Stripe integration (recommended for Stripe Checkout)

Connect your Stripe account under **Program settings** › **Payments**, then attach Affitor metadata when you create the Checkout Session. Affitor receives the account's `checkout.session.completed` and `invoice.payment_succeeded` events through Stripe Connect (you add no webhook) and attributes the sale automatically, with no extra `trackSale` call needed.
Connect your Stripe account under **Program settings** › **Payments**, then attach Affitor metadata when you create the Checkout Session. Affitor receives the account's `checkout.session.completed` and invoice events (`invoice.payment_succeeded`, `invoice.paid`) through Stripe Connect (you add no webhook) and attributes the sale automatically, with no extra `trackSale` call needed.

```js
// server — creating the Checkout Session
Expand Down
8 changes: 4 additions & 4 deletions content/docs/api-reference/integrations/stripe.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: "Stripe"
description: "Canonical recipe for attributing Stripe payments to Affitor partners — one-time and recurring."
kind: guide
last_updated: 2026-10-17
last_updated: 2026-10-18
---

This guide is the **canonical reference for the Stripe sale path**. Other framework guides (Next.js, Express, etc.) link here for Step 3. If you need click capture or signup tracking first, start with the [Next.js guide](/docs/api-reference/integrations/nextjs) or the framework guide for your stack.
Expand Down Expand Up @@ -58,7 +58,7 @@ Affitor listens to the `checkout.session.completed` webhook event and attributes

### Subscription (`mode: 'subscription'`)

For subscriptions, put the same fields in `subscription_data.metadata`. Affitor records a subscription's first payment and each renewal from `invoice.payment_succeeded`, and an invoice carries the subscription's metadata, not the Checkout Session's. 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.
For subscriptions, put the same fields in `subscription_data.metadata`. Affitor records a subscription's first payment and each renewal from the invoice events (`invoice.payment_succeeded`, `invoice.paid`), and an invoice carries the subscription's metadata, not the Checkout Session's. 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.

```ts
const session = await stripe.checkout.sessions.create({
Expand Down Expand Up @@ -90,12 +90,12 @@ const session = await stripe.checkout.sessions.create({

## 3. How Affitor attributes payments

Affitor processes two Stripe webhook events:
Affitor processes these Stripe webhook events for sales:

| Event | When it fires | What Affitor does |
|---|---|---|
| `checkout.session.completed` | A one-time payment | Records the sale. A subscription checkout records no sale here: its first invoice does |
| `invoice.payment_succeeded` | A subscription's first payment and every renewal | Records the subscription sale |
| `invoice.payment_succeeded`, `invoice.paid` | A subscription's first payment and every renewal | Records the subscription sale. Both events go to the same handler, which records each invoice at most once |

**Attribution fallback chain.** Affitor resolves the partner in this order:

Expand Down
4 changes: 2 additions & 2 deletions content/docs/api-reference/track-refund.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: Track Refund
description: Reverse a commission when a sale is refunded
kind: reference
last_updated: 2026-10-17
last_updated: 2026-10-18
---

`POST /api/v1/track/refund`
Expand Down Expand Up @@ -151,7 +151,7 @@ Errors use the [platform envelope](/docs/api-reference/errors#the-platform-envel
| 400 | `invalid_request` | `transaction_id` is missing, or `reason` is not `refund` or `chargeback`. |
| 401 | `unauthorized` | `Authorization` header is missing, malformed, or the API token is invalid. |
| 404 | `not_found` | No sale record found for the given `transaction_id` in the authenticated program. |
| 500 | `internal_error` | Internal error during commission reversal. |
| 500 | `internal_error` | Internal error during commission reversal. Retry the same request with the same `refund_id`: a refund that was already applied is skipped. |

---

Expand Down
4 changes: 2 additions & 2 deletions content/docs/brand/tracking/connect-stripe-account.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: Connect a new Stripe account
description: "Create a separate Stripe account under the login you already have, connect it to an Affitor program, test it with Stripe test mode, and check that the connection is right."
kind: guide
last_updated: 2026-10-17
last_updated: 2026-10-18
---

Create a second Stripe account under your existing Stripe login, activate it, connect it to your Affitor program, and test it before partners send real customers. Affitor connects through Stripe Connect, so you do not create an API key or a webhook for it.
Expand Down Expand Up @@ -72,7 +72,7 @@ If Stripe is connected and your server also sends sales through the Server API,
| Stripe event | What Affitor does |
|---|---|
| `checkout.session.completed` | Records a one-time purchase |
| `invoice.payment_succeeded` | Records a subscription's first payment and each renewal |
| `invoice.payment_succeeded`, `invoice.paid` | Records a subscription's first payment and each renewal, at most once per invoice |
| `charge.refunded` | Reverses the commission ([refund handling](/docs/brand/quickstart/commission-approval-cash-flow#refund-handling)) |
| `charge.dispute.funds_withdrawn` | Reverses the commission when a chargeback takes the money |
| `charge.dispute.funds_reinstated`, `charge.dispute.closed` (won) | Sends you a notification to review the commission. It stays reversed: Affitor does not re-credit the partner on its own |
Expand Down
10 changes: 5 additions & 5 deletions content/docs/brand/tracking/payment-tracking-stripe.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: Payment Tracking
description: Track completed sales with server-side tracking or Stripe integration
kind: guide
last_updated: 2026-10-17
last_updated: 2026-10-18
---

Payment tracking records revenue after a customer pays. Affitor supports two sale-tracking paths:
Expand Down Expand Up @@ -161,7 +161,7 @@ const session = await stripe.checkout.sessions.create({
### 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 `invoice.payment_succeeded`
- 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
Expand All @@ -178,8 +178,8 @@ Every `checkout.session.completed` event with `mode: 'payment'` is processed ind

:::note

**`invoice.payment_succeeded` is not involved here.**
Affitor uses `invoice.payment_succeeded` 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.
**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.

:::

Expand Down Expand Up @@ -327,7 +327,7 @@ Check:
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 `invoice.payment_succeeded` through Stripe Connect; you do not add a webhook
- 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

---
Expand Down
4 changes: 2 additions & 2 deletions content/docs/brand/tracking/quickstart-integration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: 3-Step Integration Guide
description: Get Affitor tracking live with the supported click, signup, and server-side tracking flows
kind: quickstart
last_updated: 2026-10-17
last_updated: 2026-10-18
---

Three steps get Affitor tracking live: clicks → signups → sales. Steps 1 and 2 establish attribution; step 3 records revenue.
Expand Down Expand Up @@ -279,7 +279,7 @@ const session = await stripe.checkout.sessions.create({
### Stripe integration rules

- **One-time payments** are attributed from Stripe checkout metadata and webhook processing
- **Subscriptions and renewals** rely on `invoice.payment_succeeded`
- **Subscriptions and renewals** rely on the invoice events (`invoice.payment_succeeded`, `invoice.paid`)
- If `subscription_data.metadata` is missing, the click does not reach the subscription's invoices (first payment and renewals), and Affitor attributes the subscription only by the buyer's email or Stripe customer ID
- Metadata fields to set on every checkout (recommended; none is strictly required):
- `affitor_click_id`
Expand Down
4 changes: 2 additions & 2 deletions content/docs/use-cases/subscriptions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: Subscriptions, credit partners on renewals
description: Stripe tells Affitor about each paid invoice of a subscription, and each one earns the partner who referred the customer for as long as your commission type says. Refunds and disputes take the commission back.
kind: guide
last_updated: 2026-10-17
last_updated: 2026-10-18
use_case:
name: Subscriptions
strip: [Partner link, Subscription, Renewals]
Expand Down Expand Up @@ -44,7 +44,7 @@ sequenceDiagram

Each paid invoice of the subscription is a sale: the first one, then every renewal. Renewals go to the partner of the first payment, through the click passed at checkout or the buyer's email or Stripe customer ID. The attribution window applies only to that first payment: once the customer has paid, renewals are credited however old the click is. The rate is the same for every plan, price and billing interval.

Canceling, pausing or changing the plan writes nothing in Affitor: a canceled subscription stops earning because it stops paying, and after a plan change each paid invoice earns at the same rate. A refund or dispute is matched to its invoice, except when one payment paid several invoices or one invoice was paid by several charges: those are not taken back automatically. If you bill subscriptions outside Stripe, your server reports each payment and each refund ([Any payment provider](/docs/use-cases/any-payment-provider)).
Canceling, pausing or changing the plan writes nothing in Affitor: a canceled subscription stops earning because it stops paying, and after a plan change each paid invoice earns at the same rate. A refund or dispute is matched to its invoice and reverses the commission; some older sales, such as one payment shared by several invoices, are held for manual reconciliation instead. A new invoice whose payment was already refunded, even in part, before Affitor processed it is not recorded as a sale. A new invoice paid by several charges, or by one payment that also paid other invoices, is not recorded automatically. If you bill subscriptions outside Stripe, your server reports each payment and each refund ([Any payment provider](/docs/use-cases/any-payment-provider)).

## Steps

Expand Down
12 changes: 12 additions & 0 deletions scripts/retired-claims.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,5 +94,17 @@
"why": "CLI 0.4.5 reads stripe_account_bound for a certain runtime key. A true binding skips injection; unknown/false or uncertain env keeps unattended webhook unchanged. Existing calls are not removed.",
"since": "2026-10-18",
"allow": []
},
{
"pattern": "not taken back automatically",
"why": "Since CMS v3.16.139 a new invoice paid by several charges, or by a payment shared with other invoices, is not recorded as a sale, and a new invoice refunded before processing is not recorded; older such sales are held for manual reconciliation.",
"since": "2026-10-18",
"allow": []
},
{
"pattern": "refund answers `Invalid API token`",
"why": "An empty Bearer token on POST /api/v1/track/refund answers 401 `Empty Bearer token` (CMS v3.16.140).",
"since": "2026-10-18",
"allow": []
}
]
Loading