Track sales

Payment Tracking

Track completed sales with server-side tracking or Stripe integration

View as Markdown

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

PathBest for
Server-side trackingcustom backend, any payment provider, Paddle/LemonSqueezy/manual revenue events
Stripe integrationexisting 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/json

Minimum 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 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

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

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.

FieldRequiredDescription
affitor_click_idRecommendedtracked click ID from the browser cookie
affitor_customer_keyRecommendedyour internal customer/user ID
program_idRecommendedyour 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 invoice.payment_succeeded
  • 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.

Info

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.

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',
  },
});
Info

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

SymptomLikely cause
First purchase attributed, later purchases are notaffitor_customer_key is missing or inconsistent across checkouts
No purchases attributed at allCheckouts 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 monthsThe 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.

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)

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 invoice.payment_succeeded through Stripe Connect; you do not add a webhook
  • the same customer key was used at signup and in Stripe metadata