SDKs
Typed client libraries for browser tracking (@affitor/sdk) and server-side conversion reporting (@affitor/sdk/server).
Beta — The @affitor/sdk package and CLI are in active development. The documented happy-path works; edge cases may change. Report issues on GitHub.
One package covers the full attribution chain via subpaths: the browser entry captures clicks and leads on the frontend; the /server entry reports conversions from your backend.
@affitor/sdk (Browser)
SSR-safe typed npm wrapper over the Affitor tracking endpoints. Captures ?aff= attribution, persists a affitor_click_id first-party cookie (60-day default), and reports click and lead events. All methods are no-ops on the server — safe to import in Next.js and other SSR frameworks without guards.
Install
npm install @affitor/sdkOr include the script tag directly:
<script
src="https://api.affitor.com/js/affitor-tracker.js"
data-affitor-program-id="YOUR_PROGRAM_ID"
defer
></script>Quick start
import { loadAffitor } from '@affitor/sdk';
// On app load (client-side; resolves to null on the server)
const affitor = await loadAffitor(123);
// After the user completes signup
await affitor?.signup('user_abc123', 'user@example.com');loadAffitor loads the tracker script once and resolves to the same instance on every later call. The tracker captures ?aff= from the URL and sets the click id cookie by itself.
The tracker sends events to https://api.affitor.com. To send them to a non-production API, set window.AFFITOR_API_BASE = '<API_BASE>' before the first loadAffitor call, or use the script tag with data-affitor-api-base.
loadAffitor(programId, options?)
| Option | Type | Default | Description |
|---|---|---|---|
env | 'production' | 'local' | 'production' | Selects the tracker script URL. |
scriptUrl | string | none | Custom tracker script URL; overrides env. It only chooses where the script loads from. |
debug | boolean | false | Verbose console.log output. Errors always log. |
Instance methods
| Method | Signature | Description |
|---|---|---|
signup | (customerKey: string, email: string) => Promise<void> | Report a lead (signup). Pass your internal user id as customerKey. |
getClickId | () => string | null | The current click id from memory or the affitor_click_id cookie. |
getVisitorId | () => string | null | The visitor id from Affitor's first-party cookie, or null. Put it in checkout metadata as affitor_visitor_id. With consent mode on, call it after consent(true). |
consent | (granted: boolean) => void | With data-affitor-consent="required" on the script, nothing is stored or sent until consent(true); consent(false) removes Affitor's cookies. |
getData | () => AffitorData | A snapshot of tracker state. |
trackTest | (data?) => void | Send a test event. |
signup()requirescustomerKey, your own user id. Affitor uses it to bind the customer to a click so later backend sale calls can resolve attribution by id alone.
AffitorData
Returned by getData().
| Field | Type | Description |
|---|---|---|
clickId | string | null | The active affitor_click_id (in memory or from the cookie). |
programId | number | The resolved program id. |
hasAttribution | boolean | true when a valid click has been tracked. |
affiliateUrl | string | null | The affiliate URL that started the current attribution. |
Full example (Next.js)
// app/affitor-tracker.tsx — client component
'use client';
import { useEffect } from 'react';
import { loadAffitor } from '@affitor/sdk';
export function AffitorTracker() {
useEffect(() => {
void loadAffitor(YOUR_PROGRAM_ID);
}, []);
return null;
}
// render <AffitorTracker /> once in app/layout.tsxAfter a successful signup, call signup(customerKey, email) on the instance (or window.affitor.signup(...) for the script-tag path):
import { loadAffitor } from '@affitor/sdk';
const affitor = await loadAffitor(YOUR_PROGRAM_ID);
await affitor?.signup(userId, email);React apps can also use AffitorProvider and useAffitor() from @affitor/sdk/react.
@affitor/sdk/server (Server)
Typed, Bearer-authenticated client over the Affitor conversion API. Use this from your backend to report leads, sales, and refunds for any payment provider (Stripe, Polar, Lemon Squeezy, Paddle, etc.).
Never ship the program API key to the browser.
Install
npm install @affitor/sdkQuick start
import Affitor from '@affitor/sdk/server';
const affitor = new Affitor({ apiKey: process.env.AFFITOR_API_KEY! });
// On user signup — bind the customer to their affiliate click
await affitor.trackLead({ customerExternalId: user.id, clickId });
// On payment success — report the sale
await affitor.trackSale({
customerExternalId: user.id,
amount: 4999, // cents
invoiceId: inv.id, // idempotency key
});AffitorOptions
Constructor options for new Affitor(opts).
| Field | Type | Required | Description |
|---|---|---|---|
apiKey | string | Yes | Program API key (Bearer). Find it in your Affitor dashboard under program settings. |
apiUrl | string | No | Override the API base URL. Defaults to https://api.affitor.com. |
fetch | typeof fetch | No | Custom fetch implementation. Required for Node versions below 18 that lack a global fetch. |
AffitorResponse<T>
All methods return Promise<AffitorResponse<T>>.
| Field | Type | Description |
|---|---|---|
ok | boolean | true when the HTTP status was 2xx. |
status | number | HTTP status code. 0 on network error. |
data | T | null | Parsed response body when ok is true; null on error. |
error | string | undefined | Error message or HTTP status text when ok is false. |
Methods
trackLead(input: TrackLeadInput)
Binds customerExternalId to clickId. Once bound, later trackSale calls need only customerExternalId — no click id required.
At least one of customerExternalId or clickId is required; throws synchronously if both are missing.
| Field | Type | Required | Description |
|---|---|---|---|
customerExternalId | string | Conditional | Your own user id. Binds the customer to the click for downstream sale attribution. |
clickId | string | Conditional | affitor_click_id cookie value from the browser. |
email | string | No | Customer email (hashed server-side). |
eventName | string | No | Custom event label for segmentation. |
additionalData | Record<string, unknown> | No | Arbitrary metadata passed through to the event record. |
trackSale(input: TrackSaleInput)
Records a completed sale and creates a commission. Resolves attribution by customerExternalId first, then clickId. Idempotent by invoiceId.
At least one of customerExternalId or clickId is required, and amount must be a positive integer. Both amount and invoiceId throw synchronously if invalid.
| Field | Type | Required | Description |
|---|---|---|---|
customerExternalId | string | Conditional | Your own user id — resolves attribution from the earlier trackLead call. |
clickId | string | Conditional | affitor_click_id cookie value (fallback if no prior lead). |
amount | number | Yes | Sale amount in integer cents (e.g. 4999 = $49.99). |
invoiceId | string | Yes | Unique invoice or transaction id — used as an idempotency key to deduplicate retries. |
currency | string | No | USD, EUR, or VND; other codes are not supported. Defaults to USD. Amounts are not converted between currencies: commissions and platform fees are calculated on the amount as sent, and invoices are issued in USD. |
saleType | 'payment' | 'subscription' | No | Categorize the sale type. |
isRecurring | boolean | No | true for recurring subscription charges. |
subscriptionId | string | No | External subscription id from your payment provider. |
subscriptionInterval | 'monthly' | 'quarterly' | 'annual' | No | Billing cadence for subscription sales. |
eventName | string | No | Custom event label for segmentation. |
trackClick(input?: TrackClickInput)
Track a click server-side. Click tracking is usually handled in the browser by @affitor/sdk; use this only for server-rendered flows or redirect-based affiliate links.
This endpoint does not require authentication.
| Field | Type | Required | Description |
|---|---|---|---|
affiliateUrl | string | No | The full affiliate URL containing the ?aff= parameter. |
pageUrl | string | No | The landing page URL. |
referrerUrl | string | No | The HTTP referrer. |
existingClickId | string | No | Pass an existing click id to enable last-partner attribution (overwrites old click). |
trackRefund(input: TrackRefundInput)
Reverses the commission for a previously tracked sale. Call it once per refund, from your payment provider's refund webhook.
- Omit
refundAmountCents(or pass0) to refund everything not yet refunded → commission status set toreversed. - Pass this refund's own amount → it is added to what was already refunded on the sale, and the commission is reduced proportionally (
refunded, orreversedonce the total reaches the sale amount). - Do not pass a running total (such as Stripe's
charge.amount_refunded): each call is counted as a new refund. - A retry with the same amount is ignored. To report two separate refunds of the same amount, call the HTTP endpoint with a distinct
refund_idfor each.
invoiceId is required and throws synchronously if missing.
| Field | Type | Required | Description |
|---|---|---|---|
invoiceId | string | Yes | The invoiceId you passed to trackSale — identifies which sale to refund. |
refundAmountCents | number | No | This refund's amount in integer cents (not the running total). Omit or pass 0 to refund the rest of the sale. |
refundReason | string | No | Optional reason string passed through to the refund record. |
Full example (Stripe webhook)
import Stripe from 'stripe';
import Affitor from '@affitor/sdk/server';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
const affitor = new Affitor({ apiKey: process.env.AFFITOR_API_KEY! });
// POST /webhooks/stripe
export async function handleStripeWebhook(rawBody: Buffer, sig: string) {
const event = stripe.webhooks.constructEvent(
rawBody,
sig,
process.env.STRIPE_WEBHOOK_SECRET!
);
if (event.type === 'checkout.session.completed') {
const session = event.data.object as Stripe.Checkout.Session;
const result = await affitor.trackSale({
customerExternalId: session.client_reference_id ?? undefined,
amount: session.amount_total ?? 0,
invoiceId: session.payment_intent as string,
currency: session.currency?.toUpperCase(),
saleType: 'payment',
});
if (!result.ok) {
console.error('Affitor trackSale failed:', result.error);
}
}
// One event per refund, carrying that refund's own amount.
if (event.type === 'refund.created') {
const refund = event.data.object as Stripe.Refund;
await affitor.trackRefund({
invoiceId: refund.payment_intent as string,
refundAmountCents: refund.amount,
});
}
}Related
- Track Click — raw HTTP endpoint reference
- Track Lead — raw HTTP endpoint reference
- Track Sale — raw HTTP endpoint reference
- Track Refund — raw HTTP endpoint reference
- Lead Tracking Guide — implementation walkthrough