{"_id":"@affitor/sdk","name":"@affitor/sdk","dist-tags":{"latest":"2.1.0"},"versions":{"2.1.0":{"name":"@affitor/sdk","version":"2.1.0","description":"Affitor affiliate tracking SDK for JavaScript — browser loader, React hooks, and a Node server client","repository":{"type":"git","url":"git+https://github.com/Affitor/sdk.git"},"publishConfig":{"registry":"https://registry.npmjs.org","access":"public"},"main":"dist/index.js","module":"dist/index.mjs","types":"dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.mjs","require":"./dist/index.js"},"./pure":{"types":"./dist/pure.d.ts","import":"./dist/pure.mjs","require":"./dist/pure.js"},"./react":{"types":"./dist/react.d.ts","import":"./dist/react.mjs","require":"./dist/react.js"},"./server":{"types":"./dist/server.d.ts","import":"./dist/server.mjs","require":"./dist/server.js"}},"scripts":{"build":"tsup","dev":"tsup --watch","prepublishOnly":"npm run build"},"keywords":["affitor","affiliate","tracking","analytics","saas","partner"],"license":"MIT","devDependencies":{"@types/react":"^19.2.14","tsup":"^8.0.0","typescript":"^5.0.0"},"peerDependencies":{"react":">=17.0.0"},"peerDependenciesMeta":{"react":{"optional":true}},"_id":"@affitor/sdk@2.1.0","gitHead":"410f648efbfe3f9b1856f6d175e4306af1730953","bugs":{"url":"https://github.com/Affitor/sdk/issues"},"homepage":"https://github.com/Affitor/sdk#readme","_nodeVersion":"22.22.3","_npmVersion":"10.9.8","dist":{"integrity":"sha512-AYP7Dg4TDYeyTHDMGK2B87RsU5pJSMg5RJX/658ufe+DC+r1DocNBwKEv2LJbCuowaJQ9UWt4qT+AVtpqsmh1Q==","shasum":"dbf98056b6c87049da845dc54f307c9dc23e062d","tarball":"https://registry.npmjs.org/@affitor/sdk/-/sdk-2.1.0.tgz","fileCount":19,"unpackedSize":68673,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIE8vYHlcliP6AIqCyzOhc+frbZBq8qJwgER7VUngE/EIAiBxfY4UclDSR/SHm7oksshLLD2zkOjv1zjQufPeCTgaeg=="}]},"_npmUser":{"name":"sonpiaz","email":"sonxpiaz@gmail.com"},"directories":{},"maintainers":[{"name":"tomto","email":"tomto@affitor.com"},{"name":"sonpiaz","email":"sonxpiaz@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/sdk_2.1.0_1781408123337_0.19799551172873198"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-14T03:35:23.177Z","2.1.0":"2026-06-14T03:35:23.488Z","modified":"2026-06-14T03:35:23.769Z"},"maintainers":[{"name":"tomto","email":"tomto@affitor.com"},{"name":"sonpiaz","email":"sonxpiaz@gmail.com"}],"description":"Affitor affiliate tracking SDK for JavaScript — browser loader, React hooks, and a Node server client","homepage":"https://github.com/Affitor/sdk#readme","keywords":["affitor","affiliate","tracking","analytics","saas","partner"],"repository":{"type":"git","url":"git+https://github.com/Affitor/sdk.git"},"bugs":{"url":"https://github.com/Affitor/sdk/issues"},"license":"MIT","readme":"# @affitor/sdk\n\nOfficial JavaScript/TypeScript SDK for [Affitor](https://affitor.com) affiliate tracking. A Promise-based wrapper around the Affitor tracker script, inspired by [@stripe/stripe-js](https://github.com/stripe/stripe-js), plus a Node server client for reporting leads and sales from your backend.\n\n## Install\n\n```bash\nnpm install @affitor/sdk\n```\n\n## Entry Points\n\nThe SDK provides entry points for different use cases:\n\n| Entry Point | Import | Best For |\n|---|---|---|\n| `@affitor/sdk` | `import { loadAffitor } from '@affitor/sdk'` | Any JS/TS application |\n| `@affitor/sdk/react` | `import { AffitorProvider, useAffitor } from '@affitor/sdk/react'` | React / Next.js apps |\n| `@affitor/sdk/server` | `import Affitor from '@affitor/sdk/server'` | Node backends (Bearer-auth tracking) — drop-in for `affitor-node` |\n\n---\n\n## `@affitor/sdk` — Core SDK\n\n### `loadAffitor(programId, options?)`\n\nLoads the Affitor tracker script and returns a Promise that resolves to the tracker instance.\n\n- **Singleton** — calling `loadAffitor` multiple times returns the same Promise\n- **SSR-safe** — resolves to `null` on the server\n- **Guaranteed** — the instance is always ready when the Promise resolves\n\n```ts\nimport { loadAffitor } from '@affitor/sdk';\n\nconst affitor = await loadAffitor('59');\n```\n\n### Options\n\n| Option | Type | Default | Description |\n|---|---|---|---|\n| `env` | `'production' \\| 'uat' \\| 'local'` | `'production'` | Environment preset (selects the tracker script URL) |\n| `debug` | `boolean` | `false` | Enable debug mode (verbose console logs, cookie verification) |\n| `scriptUrl` | `string` | — | Custom script URL (overrides `env` preset) |\n\n### Environment Presets\n\n| `env` | Script URL |\n|---|---|\n| `'production'` | `https://api.affitor.com/js/affitor-tracker.js` |\n| `'uat'` | `https://uat-affitor-cms.vanilla-ott.com/js/affitor-tracker-uat.js` |\n| `'local'` | `http://localhost:1337/js/affitor-tracker-local.js` |\n\n```ts\n// Production (default)\nconst affitor = await loadAffitor('59');\n\n// Local development with debug\nconst affitor = await loadAffitor('29', { env: 'local', debug: true });\n\n// UAT\nconst affitor = await loadAffitor('29', { env: 'uat' });\n\n// Custom URL (overrides env)\nconst affitor = await loadAffitor('29', { scriptUrl: 'https://my-cdn.com/tracker.js' });\n```\n\n### Methods\n\nOnce loaded, the `affitor` instance provides these methods:\n\n| Method | Description |\n|---|---|\n| `signup(customerKey, email)` | Track a signup event. Takes the user's ID and email as positional args. Returns a Promise. |\n| `trackLead(data)` | **Deprecated.** Alias for `signup()`. Accepts `{ email, user_id }`. Use `signup()` instead. |\n| `trackTest(data?)` | Send a test event to verify tracking is working. |\n| `redirectToCheckout(params)` | Redirect the user to the Affitor-powered checkout page. |\n\n### Properties\n\n| Property | Type | Description |\n|---|---|---|\n| `customerCode` | `string \\| null` | The affiliate customer code from cookie |\n| `affiliateUrl` | `string \\| null` | The affiliate referral URL from cookie |\n| `hasAffiliateAttribution` | `boolean` | Whether the current visitor was referred by an affiliate |\n| `debugMode` | `boolean` | Whether debug mode is enabled |\n| `programId` | `number` | The advertiser program ID |\n\n### Example: Track Signup\n\n```ts\nimport { loadAffitor } from '@affitor/sdk';\n\nasync function handleSignup(email: string, password: string) {\n  // 1. Create the user account\n  const { data } = await supabase.auth.signUp({ email, password });\n\n  // 2. Track the signup — awaits script load, guaranteed to fire\n  if (data.user) {\n    const affitor = await loadAffitor('59');\n    await affitor?.signup(data.user.id, data.user.email);\n  }\n\n  // 3. Navigate to dashboard\n  router.push('/dashboard');\n}\n```\n\n### Example: Redirect to Checkout\n\n```ts\nimport { loadAffitor } from '@affitor/sdk';\n\nasync function handleUpgrade() {\n  const affitor = await loadAffitor('59');\n  affitor?.redirectToCheckout({\n    price: 19.99,\n    programId: 59,\n  });\n}\n```\n\n---\n\n## `@affitor/sdk/react` — React Hooks\n\nThe React entry point provides two patterns: **Provider + hook** and **standalone hook**.\n\n### Pattern A: `AffitorProvider` + `useAffitor()`\n\nBest when your whole app needs access to the tracker state (e.g. showing referral badges).\n\n```tsx\nimport { AffitorProvider, useAffitor } from '@affitor/sdk/react';\n\n// 1. Wrap your app (layout.tsx or _app.tsx)\nexport default function RootLayout({ children }) {\n  return (\n    <AffitorProvider programId=\"59\">\n      {children}\n    </AffitorProvider>\n  );\n}\n\n// 2. Read tracker state in any component\nfunction ReferralBadge() {\n  const affitor = useAffitor();\n\n  if (!affitor?.hasAffiliateAttribution) return null;\n\n  return <span>Referred by a partner!</span>;\n}\n```\n\n#### `AffitorProvider` Props\n\n| Prop | Type | Required | Description |\n|---|---|---|---|\n| `programId` | `string \\| number` | Yes | Your Affitor program ID |\n| `debug` | `boolean` | No | Enable debug mode |\n| `scriptUrl` | `string` | No | Custom script URL |\n\n#### `useAffitor()`\n\nReturns `AffitorInstance | null`. Returns `null` while the SDK is loading.\n\n> **Important:** `useAffitor()` is best for reading tracker state in the UI (e.g. `hasAffiliateAttribution`, `customerCode`). For critical tracking events like `signup`, use `await loadAffitor()` directly instead — see [Which approach should I use?](#which-approach-should-i-use) below.\n\n### Pattern B: `useLoadAffitor()` (Standalone)\n\nUse this when you don't need a provider — the hook loads the SDK on mount.\n\n```tsx\nimport { useLoadAffitor } from '@affitor/sdk/react';\n\nfunction MyComponent() {\n  const affitor = useLoadAffitor('59', { debug: true });\n\n  return (\n    <div>\n      {affitor?.hasAffiliateAttribution && <p>Partner referral detected</p>}\n    </div>\n  );\n}\n```\n\n---\n\n## Which approach should I use?\n\n### For tracking events (`signup`, `redirectToCheckout`)\n\nAlways use `await loadAffitor()` directly. This guarantees the script is loaded before the event fires. Critical events must never be silently skipped.\n\n```ts\n// GOOD — guaranteed to fire\nconst affitor = await loadAffitor('59');\nawait affitor?.signup(userId, email);\n\n// BAD — affitor could be null if script still loading\nconst affitor = useAffitor();\nawait affitor?.signup(userId, email); // silently skipped if null\n```\n\n### For reading tracker state in UI\n\nUse `useAffitor()` or `useLoadAffitor()`. If the value is `null` momentarily while loading, the UI simply doesn't render that part yet. No data is lost.\n\n```tsx\n// GOOD — fine for UI, gracefully handles loading state\nconst affitor = useAffitor();\nreturn affitor?.hasAffiliateAttribution ? <Badge /> : null;\n```\n\n### Summary\n\n| Use Case | Approach | Why |\n|---|---|---|\n| `signup` on registration | `await loadAffitor()` | Must not be lost — awaits script load |\n| `redirectToCheckout` | `await loadAffitor()` | Must not be lost — awaits script load |\n| Show referral badge in UI | `useAffitor()` | OK to show nothing while loading |\n| Display customer code | `useAffitor()` | OK to show nothing while loading |\n| Check `hasAffiliateAttribution` for conditional UI | `useAffitor()` | OK to show nothing while loading |\n\n---\n\n## Server (Node)\n\nThe `@affitor/sdk/server` entry point **is** the [`affitor-node`](https://www.npmjs.com/package/affitor-node) server SDK — same ergonomic API, now shipped inside `@affitor/sdk`. It's a Node-only client (global `fetch`, no DOM) for reporting conversions from your backend. It authenticates with your **program API key** as a Bearer token, so it must never run in the browser.\n\nAttribution model (Dub-style): **bind the customer at lead time**, then a sale needs only `customerExternalId`.\n\n```ts\nimport Affitor from '@affitor/sdk/server';          // default import\n// import { Affitor } from '@affitor/sdk/server';   // named import — both work\n\nconst affitor = new Affitor({ apiKey: process.env.AFFITOR_API_KEY! });\n\n// Bind the customer at lead/signup time\nawait affitor.trackLead({ customerExternalId: user.id, clickId, email: user.email });\n\n// Report a sale from your payment webhook (Stripe, Polar, Lemon Squeezy, …)\nawait affitor.trackSale({\n  customerExternalId: user.id, // resolves attribution (no clickId needed after lead)\n  amount: 4900,                // integer cents\n  invoiceId: invoice.id,       // idempotency key — dedups retries\n  currency: 'USD',\n});\n\n// Reverse a commission from your refund webhook\nawait affitor.trackRefund({ invoiceId: invoice.id });\n```\n\n### Methods\n\nEvery `track*` method returns a `Promise<AffitorResponse<T>>` — `{ ok, status, data, error? }` — and never throws on an HTTP error (only on missing required input).\n\n| Method | Endpoint | Description |\n|---|---|---|\n| `trackLead(input)` | `POST /api/v1/track/lead` | Bind a customer to their click. Needs `customerExternalId` or `clickId`. |\n| `trackSale(input)` | `POST /api/v1/track/sale` | Record a sale + commission for an attributed customer. Needs `amount` (cents) + `invoiceId`. |\n| `trackRefund(input)` | `POST /api/v1/track/refund` | Reverse the commission for a prior sale. Needs `invoiceId`. |\n| `trackClick(input?)` | `POST /api/v1/track/click` | Mint (or reuse) a click id. Public — no auth. |\n| `readiness(opts?)` | `GET /api/v1/programs/me/readiness` | The 5-gate readiness verdict for this key's program. Added by `@affitor/sdk` (not in `affitor-node`). |\n\n### Key input fields (camelCase ergonomic)\n\n| Method | Field | Type | Notes |\n|---|---|---|---|\n| `trackLead` | `customerExternalId` | `string` | Your own user id — binds the customer. |\n| `trackLead` | `clickId` | `string` | Affitor click id. One of `clickId` / `customerExternalId` required. |\n| `trackSale` | `customerExternalId` | `string` | Resolves attribution. |\n| `trackSale` | `amount` | `number` | Sale amount in **integer cents**. |\n| `trackSale` | `invoiceId` | `string` | Idempotency key — dedups retries. |\n| `trackSale` | `currency` | `string` | ISO currency (default `USD`). |\n| `trackRefund` | `invoiceId` | `string` | The sale's `invoiceId`. |\n| `trackRefund` | `refundAmountCents` | `number` | Omit for a full refund. |\n\n### Options & errors\n\n```ts\n// Custom API base or fetch (testing, Node < 18)\nconst affitor = new Affitor({\n  apiKey,\n  apiUrl: 'https://uat.affitor.com',\n  fetch: myFetch,\n});\n```\n\n`track*` methods surface HTTP errors on the resolved `AffitorResponse` (`ok: false`, `status`, `error`) — they don't throw:\n\n```ts\nconst res = await affitor.trackSale({ customerExternalId: 'u_1', amount: 4900, invoiceId: 'inv_1' });\nif (!res.ok) {\n  console.error(`Affitor ${res.status}: ${res.error}`);\n}\n```\n\n`readiness()` returns the bare verdict and **throws** an `AffitorApiError` on a non-2xx response, exposing `status`, `code`, `retryAfterSeconds`, and the raw `body`. It parses both error envelope shapes the API returns (`{ error: \"...\" }` and `{ error: { code, message, retry_after_seconds } }`):\n\n```ts\nimport Affitor, { AffitorApiError } from '@affitor/sdk/server';\n\ntry {\n  const verdict = await affitor.readiness();\n  if (verdict.integration_verified) console.log('ready to go live');\n} catch (err) {\n  if (err instanceof AffitorApiError) {\n    console.error(`Affitor ${err.status} (${err.code}): ${err.message}`);\n  }\n}\n```\n\n---\n\n## Migrating from affitor-node\n\n**Change the import — that's it.** `@affitor/sdk/server` is `affitor-node`'s\nexact API: same options-object constructor, same method names, same camelCase\ninput fields, same `AffitorResponse`, both default and named export. It's a\ndrop-in replacement.\n\n```ts\n// Before (affitor-node)\nimport Affitor from 'affitor-node';\nconst affitor = new Affitor({ apiKey: process.env.AFFITOR_API_KEY! });\nawait affitor.trackSale({ customerExternalId: user.id, amount: 4900, invoiceId: inv.id });\n\n// After (@affitor/sdk) — only the import changed\nimport Affitor from '@affitor/sdk/server';\nconst affitor = new Affitor({ apiKey: process.env.AFFITOR_API_KEY! });\nawait affitor.trackSale({ customerExternalId: user.id, amount: 4900, invoiceId: inv.id });\n```\n\nBoth import styles work, matching `affitor-node`:\n\n```ts\nimport Affitor from '@affitor/sdk/server';       // default\nimport { Affitor } from '@affitor/sdk/server';   // named\n```\n\nThe only addition over `affitor-node` is the [`readiness()`](#methods) method\nfor agent-native onboarding.\n\n---\n\n## Migrating from Script Tag\n\nIf you're currently using the `<script>` tag approach:\n\n### Before (script tag)\n\n```html\n<script src=\"https://api.affitor.com/js/affitor-tracker.js\"\n        data-affitor-program-id=\"59\"></script>\n\n<script>\n  // Must check if loaded, use queue fallback, handle timing manually\n  if (window.affitor) {\n    window.affitor.trackLead({ email, user_id });\n  } else {\n    window.affitorQueue = window.affitorQueue || [];\n    window.affitorQueue.push(['trackLead', { email, user_id }]);\n  }\n</script>\n```\n\n### After (npm SDK)\n\n```ts\nimport { loadAffitor } from '@affitor/sdk';\n\n// No timing issues — awaits script load automatically\nconst affitor = await loadAffitor('59');\nawait affitor?.signup(userId, email);\n```\n\nNo more:\n- Manual `if (window.affitor)` checks\n- Queue fallback code (`affitorQueue.push`)\n- Race conditions between script load and event firing\n- `(window as any)` type casting in TypeScript\n\n---\n\n## API Reference\n\n### `loadAffitor(programId, options?)`\n\n| Parameter | Type | Description |\n|---|---|---|\n| `programId` | `string \\| number` | Your Affitor program ID |\n| `options.env` | `'production' \\| 'uat' \\| 'local'` | Environment preset |\n| `options.debug` | `boolean` | Enable debug mode |\n| `options.scriptUrl` | `string` | Custom script URL (overrides env) |\n\n**Returns:** `Promise<AffitorInstance | null>`\n\n### `AffitorInstance`\n\n| Method / Property | Type | Description |\n|---|---|---|\n| `signup(customerKey, email)` | `(customerKey: string, email: string) => Promise<void>` | Track a signup event |\n| `trackLead(data)` | `(data: TrackLeadData) => void` | **Deprecated.** Alias for `signup()`. |\n| `trackTest(data?)` | `(data?: TrackTestData) => void` | Send test event |\n| `redirectToCheckout(params)` | `(params: RedirectToCheckoutParams) => void` | Redirect to checkout |\n| `customerCode` | `string \\| null` | Affiliate customer code |\n| `affiliateUrl` | `string \\| null` | Affiliate referral URL |\n| `hasAffiliateAttribution` | `boolean` | Has affiliate attribution |\n| `debugMode` | `boolean` | Debug mode enabled |\n| `programId` | `number` | Program ID |\n\n### `TrackLeadData`\n\n| Field | Type | Required | Description |\n|---|---|---|---|\n| `email` | `string` | Yes | User's email |\n| `user_id` | `string` | Yes | Your app's user ID (critical for payment attribution) |\n| `additional_data` | `Record<string, unknown>` | No | Extra metadata |\n\n### `TrackTestData`\n\n| Field | Type | Required | Description |\n|---|---|---|---|\n| `step_id` | `string` | No | Step identifier (default: `'pageview'`) |\n| `message` | `string` | No | Test message |\n| `user_id` | `string` | No | User ID |\n\n### `RedirectToCheckoutParams`\n\n| Field | Type | Required | Description |\n|---|---|---|---|\n| `price` | `number` | No | Price amount |\n| `programId` | `string \\| number` | No | Override program ID |\n\n---\n\n## License\n\nMIT\n","readmeFilename":"README.md","_rev":"1-b94578df264346ce46475b403a514ebb"}