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 and Affitor attributes sales from metadata plus webhook events
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 already use Stripe Checkout and want Affitor to attribute revenue from webhook events.
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. Affitor attributes the conversion through Stripe metadata and webhook processing, 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 | Yes | your Affitor program ID |
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, set the same values in both metadata and subscription_data.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 both locations
- One-time payments are handled from Stripe checkout session completion
- Subscription commissions are created from
invoice.payment_succeeded - Renewals rely on subscription metadata still being present later
- If
subscription_data.metadatais missing, renewal attribution can fail
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.
invoice.payment_succeeded is not involved here.
Affitor only uses invoice.payment_succeeded for recurring subscription renewals. 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 looks up the customer record created at signup and links all subsequent purchases to the same partner.
Required metadata 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, // required and 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
Cross-purchase attribution requires an affiliate-customer record. That record is created when the signup tracker fires at user registration:
affitor.signup({ customer_key: currentUser.id });If signup tracking was not set up or did not fire for a user, no customer record exists and later purchases cannot be attributed.
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.
Your tier configuration may include a duration_months cap (for example, "pay commission for 12 months after signup"). That is a commission duration limit — it controls how long a partner earns on a customer, 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 | Signup tracker never fired — no customer record exists |
| Commissions created but stop after N months | Tier has a duration_months cap — this is expected behavior, 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
- 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_idis present in metadataaffitor_click_idandaffitor_customer_keyare passed when available- signup tracking used the same internal customer ID earlier
- the Stripe webhook was delivered successfully
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 signup tracker fired for this user — without it, no customer record exists and attribution fails silently
Stripe renewals not attributed
Check:
subscription_data.metadatacontains all three fieldsinvoice.payment_succeededis enabled and delivered- the same customer key was used at signup and in Stripe metadata