Errors & Troubleshooting

All API error codes, their causes, and how to fix common integration problems.

Beta — The Affitor SDKs and CLI are in active development. The documented happy-path works; edge cases may change. Report issues on GitHub.

This page covers every error response the tracking API can return and practical fixes for the most common integration issues.


Error response shape

All error responses return JSON with an "error" string. There is no wrapper envelope on error paths.

{
  "error": "transaction_id is required and must be a string"
}

Error reference

Authentication errors (401)

StatusConditionMeaningFix
401Authorization header missing or does not start with Bearer The request reached a server-authenticated endpoint without a valid headerAdd Authorization: Bearer YOUR_PROGRAM_API_KEY to the request
401Header present but token string is empty after trimmingBearer prefix found but no token value followed itCheck for trailing spaces or an empty env variable
401Token does not match any api_token in the program tableWrong key, revoked key, or key from a different programCopy the API key from your program settings in the Affitor dashboard

Endpoints that require a Bearer token: POST /api/v1/track/sale, POST /api/v1/track/refund. The lead endpoint accepts a Bearer token for server mode but does not require it for browser mode.


Validation errors (400)

Track click

StatusError messageMeaningFix
400affiliate_url is requiredBody did not include affiliate_urlPass the full URL that contains the ?aff= parameter
400Invalid affiliate_urlURL could not be parsed by new URL()Ensure the value is a fully-qualified URL (includes scheme)
400No aff parameter found in affiliate_urlURL was valid but had no aff query parameterOnly call this endpoint when ?aff= is present in the URL
400Referral link not foundNo referral link matched the aff valueVerify the partner link exists and short_link matches the aff value
400Invalid referral link configurationLink exists but has no linked partner or programContact Affitor support — the referral link record is incomplete

Track lead

StatusError messageMeaningFix
400click_id or customer_key is requiredNeither identifier was sent in the bodyPass the click_id from the affitor_click_id cookie, or the customer_key set at signup
400Customer not found. Provide a valid click_id or customer_key.No customer record matched either identifierThe click was not tracked, the cookie was cleared, or the customer_key was never recorded — ensure init() and signup() ran successfully
400Customer does not belong to this programCustomer was found but is attributed to a different program (server mode only)Use the API key for the program that originated the click
400Customer status cannot be updated to lead. Current status: <status>Lead service rejected the status transitionA lead was already recorded for this customer, or the customer is at a later funnel stage — this is usually safe to ignore

Track sale

StatusError messageMeaningFix
400transaction_id is required and must be a stringField missing or not a string typeAlways pass transaction_id as a string
400amount_cents is required and must be a positive integerField missing, zero, negative, or wrong typePass sale amount in smallest currency unit (e.g. 4900 for $49.00 USD) as a positive integer
400Customer not found. Provide customer_key or click_id.Neither identifier resolved a customerEnsure lead tracking ran first and the customer record exists
400No affiliate attribution found for this customer in this programCustomer exists but is attributed to a different programUse the API key matching the program where the click originated
400No partner-program relationship found for this customerCustomer has no linked partner-program junctionThe customer record is missing a partner relationship — check that the original click was tracked correctly
400partnerId required to create commissionCustomer resolved but has no affiliate_partner relationInternal data inconsistency — contact Affitor support

Track refund

StatusError messageMeaningFix
400transaction_id is requiredBody did not include transaction_idPass the same transaction_id used when recording the original sale

Conflict errors (409)

StatusError messageMeaningFix
409Duplicate transaction_id. This sale has already been recorded.A sale event with this processor_event_id already existsThis is the intended duplicate-guard behavior — do not retry with the same transaction_id. If the original was a test, check if you need to clear test data.

Duplicate detection is keyed on processor_event_id (mapped from transaction_id). The check is scoped globally, not per-program.


Not found errors (404)

StatusError messageMeaningFix
404Sale not found for transaction_idRefund was attempted but no sale with that transaction_id exists in the programVerify the transaction_id matches exactly what was sent to /track/sale and that the Bearer token is for the same program

Server errors (500)

StatusError messageMeaningFix
500Click tracking failedUnhandled exception in click handlerCheck server logs; usually a DB or network error
500Lead tracking failedUnhandled exception in lead handlerCheck server logs
500Sale tracking failedUnhandled exception in sale handlerCheck server logs
500Refund tracking failedUnhandled exception in refund handlerCheck server logs
500Failed to create commission (or commission service error message)Commission creation step threw internally after the sale event was writtenThe sale record exists but has no commission — contact Affitor support with the transaction_id

Metric update failures (step 10 in the sale flow) are logged as warnings and do not cause a 500 — the sale and commission are still created.


Troubleshooting

Cookies blocked by the browser

The Affitor browser SDK stores the click ID in a first-party cookie named affitor_click_id (SameSite=Lax; Secure on HTTPS). Attribution is lost when:

  • The user has a browser extension or privacy setting that blocks first-party cookies.
  • The page is served over HTTP in production (the Secure flag is only set on HTTPS).
  • Safari ITP aggressively expires cookies set via JavaScript within 7 days for domains classified as trackers.

Fix: The SDK auto-detects the root domain and sets the cookie there (e.g. .example.com) to share attribution across subdomains. For custom domains or cross-domain attribution, pass cookieDomain explicitly:

init({ programId: 123, cookieDomain: '.example.com' });

There is no server-side fallback for blocked cookies — if the cookie is missing when signup() fires, pass customer_key alone and accept that the lead will be unattributed.


CORS

The tracking API (api.affitor.com) must allow your site's origin for browser-mode requests. If you see CORS errors:

  1. Check that your site's domain is listed in the allowed origins on the Affitor program settings page.
  2. Browser-mode tracking calls (/track/click, /track/lead without a Bearer token) are the only routes intended for direct browser fetch. Server-mode routes (/track/sale, /track/refund) should only be called from your backend — they do not need CORS.
  3. In development, use localhost explicitly rather than 127.0.0.1 (or vice versa) — the two are treated as different origins.

Attribution window edge cases

The attribution window defaults to 60 days and is set per program (cookie_window_days). The actual value is returned in the click response:

{
  "success": true,
  "click_id": "cust_42_1712345678901",
  "cookie_window_days": 60
}

The SDK applies this value as the cookie expiry. Edge cases:

ScenarioBehavior
User clears cookies before signupAttribution is lost; lead and sale will be unattributed unless you pass customer_key server-side
User visits with a different partner's link after the first clickLast-touch attribution: the SDK detects a new aff value, fires a new click, and overwrites the cookie
Signup fires after cookie expiryclick_id from cookie is null; lead is still recorded (unattributed) if customer_key is provided
Sale fires with a customer_key for a customer whose window expiredSale is recorded and attribution is resolved from the stored customer record, not the cookie — the window only governs cookie persistence

Duplicate transaction_id handling

The sale endpoint uses transaction_id as a globally unique idempotency key stored as processor_event_id. The rules:

  • Sending the same transaction_id twice returns 409 Conflict — the second request is a no-op.
  • The 409 is not an error to retry. Your integration should treat it as "already recorded" and proceed.
  • Test-mode sales use test_txn_<timestamp> when transaction_id is omitted, so test records are never duplicates unless you pass the same value explicitly.
  • If a sale was recorded but commission creation failed (500), the transaction_id is still consumed. Contact Affitor support to investigate the commission state before re-submitting under a new ID.

Recommended pattern:

const response = await fetch('https://api.affitor.com/api/v1/track/sale', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${PROGRAM_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ transaction_id: chargeId, ... }),
});

if (response.status === 409) {
  // Already recorded — safe to continue
  return;
}

if (!response.ok) {
  // Unexpected error — log and alert
  const body = await response.json();
  throw new Error(`Affitor error ${response.status}: ${body.error}`);
}

SDK debug: true for verbose logging

Pass debug: true to init() to enable verbose console output. This sets AffitorInitOptions.debug on the tracker instance.

import { init } from '@affitor/sdk';

init({
  programId: 123,
  debug: true,
});

With debug: true, the SDK logs to the browser console using [Affitor] as the prefix for every info and warn level message — including initialization, program ID resolution, click tracking, and signup calls. Errors (console.error) are always logged regardless of the debug flag.

[Affitor] Affitor SDK initialized: { programId: 123, debug: true }
[Affitor] Using program ID: 123
[Affitor] Click tracked, click_id saved: cust_42_1712345678901
[Affitor] Signup tracked successfully

Disable in production. The debug flag is intended for development and integration testing only. Leave it off in production builds to avoid leaking internal state to end users.

To check the current tracker state at any point:

import { getData } from '@affitor/sdk';

console.log(getData());
// {
//   clickId: "cust_42_1712345678901",
//   programId: 123,
//   hasAttribution: true,
//   affiliateUrl: "https://example.com/?aff=abc123"
// }
Edit on GitHub
© 2026 Affitor