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:

  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 and Affitor attributes sales from metadata plus webhook events

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 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/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. Affitor attributes the conversion through Stripe metadata and webhook processing, 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_idYesyour 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.metadata is 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.

Info

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

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

SymptomLikely cause
First purchase attributed, later purchases are notaffitor_customer_key is missing or inconsistent across checkouts
No purchases attributed at allSignup tracker never fired — no customer record exists
Commissions created but stop after N monthsTier 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

  1. click metadata (affitor_click_id)
  2. customer email matching
  3. Stripe customer ID
  4. 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 is present in metadata
  • affitor_click_id and affitor_customer_key are 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_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 signup tracker fired for this user — without it, no customer record exists and attribution fails silently

Stripe renewals not attributed

Check:

  • subscription_data.metadata contains all three fields
  • invoice.payment_succeeded is enabled and delivered
  • the same customer key was used at signup and in Stripe metadata

Edit on GitHub
© 2026 Affitor