Track sales
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:
- Server-side tracking: your backend calls POST /api/v1/track/sale when revenue is finalized
- 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
POST https://api.affitor.com/api/v1/track/sale
Authorization: Bearer YOUR_PROGRAM_API_KEY
Content-Type: application/jsonMinimum real-sale payload
{
"transaction_id": "txn_abc123",
"customer_key": "user_123",
"amount_cents": 9999,
"currency": "USD"
}Full example
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 requiredtransaction_idis required and must be uniqueamount_centsis 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_idis sent twice, Affitor returns409 Conflict
Response
{
"success": true,
"sale_id": 42,
"commission_id": 18,
"message": "Sale tracked successfully"
}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). 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
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.
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'smetadata, 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.
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:
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',
},
});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), or call the browser helper at user registration:
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). 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
- click metadata (
affitor_click_id) - customer email matching
- Stripe customer ID
- 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) - 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_idaffitor_customer_keyprogram_id
Test Mode for Server-side tracking
Send a test sale event without creating a real commission or platform fee.
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
Authorizationheader 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_idis unique- retries are not resending the same completed sale without idempotency control on your side
Stripe one-time sale not attributed
Check:
program_idin metadata, if you set it, is this program's ID: with another ID, this program's customers are not creditedaffitor_click_idandaffitor_customer_keyare 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)
Stripe pay-as-you-go second/third purchase not attributed
Check:
affitor_customer_keyis 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_keyon 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.metadatacontains 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