---
id: "api-reference/sdks"
type: "doc"
url: "https://affitor.com/docs/api-reference/sdks"
kind: "reference"
updated: "2026-10-18"
---
# 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.

<FeatureCards>
  <FeatureCard icon="monitor" title="@affitor/sdk" href="#affitorsdk-browser">Browser SDK — captures affiliate clicks, persists attribution in a first-party cookie, and reports lead events.</FeatureCard>
  <FeatureCard icon="server" title="@affitor/sdk/server" href="#affitorsdkserver-server">Node SDK — reports leads, sales, clicks, and refunds from your backend with Bearer auth.</FeatureCard>
</FeatureCards>

---

## @affitor/sdk (Browser)

SSR-safe typed npm wrapper over the Affitor tracking endpoints. Captures `?aff=` (or `?via=`) 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

```bash
npm install @affitor/sdk
```

Or include the script tag directly:

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

### Quick start

```ts
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=` (or `?via=`) 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. |
| `init` | `(options: { programId?: string \| number; debug?: boolean }) => void` | Starts the tracker. It runs on its own when the script loads (through `loadAffitor` or the script tag), so you do not need to call it. |
| `trackLead` | `(data: TrackLeadData) => Promise<void>` | Deprecated: use `signup`. Older alias that calls `signup` with the object's `customer_key` (or `user_id`) and `email`. |

> `signup()` requires `customerKey`, 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)

```tsx
// 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.tsx
```

After a successful signup, call `signup(customerKey, email)` on the instance (or `window.affitor.signup(...)` for the script-tag path):

```ts
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/pure` exports `loadAffitor` and the types `AffitorInstance`, `AffitorInitOptions`, `TrackSignupData`, `TrackLeadData` and `TrackTestData`. `@affitor/sdk` exports the same, plus the option types `AffitorEnv` and `LoadAffitorOptions`. Both inject the tracker script only when `loadAffitor()` runs; importing from `/pure` makes that explicit.

---

## @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

```bash
npm install @affitor/sdk
```

### Quick start

```ts
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>`

The `track*` methods return `Promise<AffitorResponse<T>>`. [`readiness()`](#readinessopts) returns the verdict itself.

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

The SDK throws synchronously only when both `customerExternalId` and `clickId` are missing. Send `clickId` on every lead: Affitor finds the customer through the click. A first lead with only `customerExternalId` returns `ok: false` with status `400` (`Customer not found. Provide a valid click_id or customer_key.`).

| 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. Send it on every lead. |
| `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). |
| `taxCents` | `number` | No | The tax included in `amount`, same unit. Accepted but not used yet: commissions are calculated on `amount`. Requires `@affitor/sdk` 2.1.2 or later. |
| `invoiceId` | `string` | **Yes** | Unique invoice or transaction id — used as an idempotency key to deduplicate retries. |
| `currency` | `string` | No | `USD`, `EUR`, or `VND`; another code returns `422`. 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=` or `?via=` parameter. |
| `pageUrl` | `string` | No | The landing page URL. |
| `referrerUrl` | `string` | No | The HTTP referrer. |
| `existingClickId` | `string` | No | An existing click id. Affitor reuses the customer that has this click id, so that customer's partner does not change. `trackClick` sends no `program_id`, so this lookup is not limited to one program. When no customer has the click id, a new customer is created with it. |

---

#### `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 pass `0`) to refund everything not yet refunded → commission status set to `reversed`.
- Pass **this refund's own amount** → it is added to what was already refunded on the sale. Once the total reaches the sale amount, the commission is `reversed`. Before that, a partial refund reduces a commission already approved and cancels one still `pending` (in its hold period, not yet approved); both end `refunded`.
- 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](/docs/api-reference/track-refund) with a distinct `refund_id` for 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. |

---

#### `readiness(opts?)`

Returns the program's readiness verdict from `GET /api/v1/programs/me/readiness`; the program comes from the API key. Pass `{ forceRecheck: true }` (`?force_recheck=true`) to check your site for the tracker again instead of using the cached result; a forced check runs at most once per minute per program.

It resolves to the verdict object (`ReadinessResult`: `state`, `integration_verified`, `blocker`, `gates`, and more), not an `AffitorResponse`, and throws `AffitorApiError` (with `status`, `code` and `retryAfterSeconds`) on a non-2xx response.

---

### Full example (Stripe webhook)

```ts
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](/docs/api-reference/track-click) — raw HTTP endpoint reference
- [Track Lead](/docs/api-reference/track-lead) — raw HTTP endpoint reference
- [Track Sale](/docs/api-reference/track-sale) — raw HTTP endpoint reference
- [Track Refund](/docs/api-reference/track-refund) — raw HTTP endpoint reference
- [Lead Tracking Guide](/docs/brand/tracking/lead-tracking-signup) — implementation walkthrough
