Track sales

3-Step Integration Guide

Get Affitor tracking live with the supported click, signup, and server-side tracking flows

View as Markdown

Three steps get Affitor tracking live: clicks → signups → sales. Steps 1 and 2 establish attribution; step 3 records revenue.

Using an AI coding agent? Set your program API key as the AFFITOR_API_KEY environment variable, then give your agent this line. It follows the agent skill to add all three steps and verify them. In Settings, API keys shows the same line with your program ID filled in, under Set up with your AI agent.

Follow https://affitor.com/docs/skill.md to integrate Affitor. Program <PROGRAM_ID> on https://api.affitor.com. Key in AFFITOR_API_KEY.

Before You Start

Make sure you have:

  • Your program ID
  • Your program API key for server-to-server calls
  • A decision on how you will track sales (server-side tracking vs Stripe integration):
    • Server-side tracking — your backend calls POST /api/v1/track/sale — for any backend or payment provider
    • Stripe integration — attach Affitor metadata to Stripe Checkout sessions
Info

Use one naming scheme consistently:

  • customerKey — browser helper argument (signup(customerKey, email))
  • customer_key — tracking API field
  • affitor_customer_key — Stripe metadata field

Step 1 — Install the Tracker

Add the tracker to every page where affiliate traffic can land.

Verifying the pipeline

Once the tracker is live on your public site, Affitor detects it automatically, with no debug mode needed. To verify the full pipeline, run the synthetic click → signup → sale chain with npx affitor onboard or the MCP affitor_run_verification tool. See Testing Integration.

<script
  src="https://api.affitor.com/js/affitor-tracker.js"
  data-affitor-program-id="YOUR_PROGRAM_ID"
  data-affitor-debug="false">
</script>

Replace YOUR_PROGRAM_ID with your program ID (program menu → Program settings → Business → Program ID, Copy).

What this does

  • Detects affiliate visits via the ?aff= parameter
  • Creates a tracked customer / click relationship
  • Stores affitor_click_id in a first-party cookie
  • Sends click/pageview tracking data to Affitor

Verify

Visit your site through a real affiliate link or enable debug mode while testing. Then review:

  • browser console / network requests
  • Affitor dashboard → tracking/integration pages

Step 2 — Track Signups

After signup succeeds, tell Affitor which internal user was created.

Option A — Browser-side helper

Use this when your signup flow runs in the browser and the tracker is already loaded.

<script>
await window.affitor.signup('user_123', 'user@example.com');
</script>

Or inside your signup flow:

<script>
document.getElementById('signup-form').addEventListener('submit', async function(e) {
  e.preventDefault();

  const email = document.getElementById('email').value;

  // Run your own signup logic first.
  // After account creation succeeds:
  await window.affitor.signup(currentUser.id, email);
});
</script>

Use this when:

  • you already installed the tracker on the page
  • signup happens in the browser
  • you want the simplest supported flow

By default a browser-only lead is provisional: it does not count as a signup until your server sends the lead (Option B) or a Stripe Checkout payment for that customer arrives through your connected Stripe account. See lead auth policy.

Option B — Server-side lead API

Use this when signup logic lives on your backend or you want the server to own the tracking call.

POST https://api.affitor.com/api/v1/track/lead
Authorization: Bearer YOUR_PROGRAM_API_KEY
Content-Type: application/json

{
  "click_id": "cust_42_1234567890",
  "customer_key": "user_123",
  "email": "user@example.com"
}
await fetch('https://api.affitor.com/api/v1/track/lead', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.AFFITOR_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    click_id: req.cookies.affitor_click_id,
    customer_key: newUser.id,
    email: newUser.email,
  }),
});

Lead tracking rules

  • Outside test mode, send the click_id from the affitor_click_id cookie: Affitor finds the customer through it. From your server, a first lead with only customer_key answers 400 Customer not found. Provide a valid click_id or customer_key.; from the browser, click_id is always required.
  • For reliable payment attribution later, always send a stable internal customer ID:
    • customerKey in signup()
    • customer_key in the API
  • Browser-side signup() is the easiest to add, but by default it stays provisional until the server-side lead or a Stripe Checkout payment through your connected Stripe account
  • Server-side API is best when signup completes on your backend

Response:

{
  "success": true,
  "message": "Lead tracked successfully"
}

Step 3 — Track Sales

When a customer pays, choose one revenue-tracking path.

Option A — Server-side tracking (works with any payment provider)

Send a server-to-server request once payment succeeds.

Endpoint: POST https://api.affitor.com/api/v1/track/sale

{
  "transaction_id": "txn_abc123",
  "customer_key": "user_123",
  "click_id": "cust_42_123456",
  "amount_cents": 9999,
  "currency": "USD",
  "sale_type": "payment",
  "is_recurring": false,
  "subscription_id": "sub_xyz",
  "subscription_interval": "monthly",
  "product_id": "prod_456",
  "line_items": [
    { "name": "Pro Plan", "amount_cents": 9999, "quantity": 1 }
  ]
}
ParameterRequiredTypeDescription
transaction_idYesstringUnique transaction identifier used for deduplication
amount_centsYesnumberPositive integer amount in cents
tax_centsNonumberThe tax included in amount_cents; accepted but not used yet
customer_keyConditionalstringYour internal customer ID
click_idConditionalstringTracked click identifier
currencyNostringUSD, EUR, or VND. Default: USD
sale_typeNostringpayment or subscription. Default: payment
is_recurringNobooleanUse true for recurring charges
subscription_idNostringExternal subscription ID
subscription_intervalNostringe.g. monthly, quarterly, annual
product_idNostringExternal product identifier
line_itemsNoarrayOptional item breakdown
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',
  }),
});

Response:

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

transaction_id must be unique. Reusing the same value returns 409 Conflict.

Option B — Stripe integration

If you already use Stripe Checkout, keep taking payment in your own Stripe account. Connect it under Program settings › Payments (Affitor receives its events through Stripe Connect, with no webhook to add), then attach Affitor metadata to the checkout session.

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

For subscriptions

Put the same fields in subscription_data.metadata: a subscription's payments are read from its invoices, which carry the subscription's metadata, not the session's. Setting them in metadata too does no harm.

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

Stripe integration rules

  • One-time payments are attributed from Stripe checkout metadata and webhook processing
  • Subscriptions and renewals rely on invoice.payment_succeeded
  • 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
    • affitor_customer_key
    • program_id

Which Sales Path Should You Use?

PathBest for
Server-side tracking — POST /api/v1/track/saleAny backend, any payment provider, custom checkout, Paddle/LemonSqueezy/manual server events
Stripe integrationExisting Stripe Checkout integration

Use server-side tracking when your backend controls the revenue event. Use Stripe integration when you want Affitor to attribute sales directly from the payment events of your connected Stripe account.


Verify it worked

Verify success
Browser
  • Visit your site with ?aff=TESTCODE — an affitor_click_id cookie is set
Network
  • Your signup handler's trackLead call returns 2xx; with the Server API, your server's trackSale call on a test payment returns 2xx; with Stripe connected, a one-time test-mode payment shows under Commissions as Test
Dashboard
  • Click, lead, and sale all appear under your program's tracking events, or run npx affitor onboard to fire the whole synthetic chain in one command (it also offers to add a trackSale call; with Stripe connected, read the CLI warning before accepting it)
If it doesn't work
  • Use additional_data: { test_mode: true } so test events never create real commissions

You're Done

All three steps working means your integration is live:

StepEventOutcome
Tracker installedClick/pageview trackingaffiliate visit captured
Signup trackedLead eventcustomer linked to partner
Sale trackedSale + commission flowrevenue attributed

Next:

Share feedback

Tell us what is missing or unclear on “3-Step Integration Guide”.

Last updated Oct 17, 2026