CLI Command Reference

Complete reference for all npx affitor commands, flags, and options.

Every npx affitor command, flag, and option in one place.

Global Flags

Available on every command:

FlagDescription
--jsonOutput as JSON (for AI agents and scripts)
--no-interactiveSkip all prompts, fail on missing values
--auto-confirmAuto-yes to confirmation prompts
--quietSuppress non-essential output
--api-key <key>Override API key from config
--api-url <url>Override API URL
--verboseShow debug output
-V, --versionShow version number
-h, --helpShow help

Which commands need a login session

Two different credentials are in play, and they are not interchangeable.

CredentialWhere it livesWhich commands use it
Account session~/.affitor/credentials.json, written by affitor loginaffitor init, affitor programs, affitor whoami
Program API keyAFFITOR_API_KEY in the environment or .affitor/.env, or --api-keyaffitor onboard, affitor status, affitor test, affitor setup stripe

Run affitor login before the first group. Without a session, init and programs stop and exit non-zero (not_logged_in in --json mode) — they never fall back to a program API key.


affitor login

Log in to your Affitor account from the browser. Required before affitor init and affitor programs.

npx affitor login

The command prints a login URL and opens it, then waits while you sign in. Once the browser finishes, the session is written to ~/.affitor/credentials.json and the token is good for 90 days.

Takes no flags of its own. The session is per machine and per account, not per project, so you log in once and it covers every project directory.

If you are already logged in, login reports the account and stops without starting a new session — run affitor logout first to switch accounts.


affitor logout

Remove the stored session from this machine.

npx affitor logout

affitor whoami

Show which account the stored session belongs to, and when its token expires.

npx affitor whoami

With no session it exits non-zero. whoami --json instead prints {"logged_in": false} and exits 0, so a script should read that field rather than the exit code.


affitor programs

List the affiliate programs on the logged-in account, with each program's ID, status, commission, and partner count.

npx affitor programs

Needs a login session. This is how you find a program ID without opening the dashboard.


affitor init

Create a new affiliate program and generate config files. Requires a login session — run affitor login first.

npx affitor init

init always creates a new program. It does not adopt a program you already have and it does not issue a replacement key for one: if a program's key stopped working, a workspace owner regenerates it in Affitor under Settings → API Key. Running init in a directory that already has .affitor/config.json stops without changing anything.

FlagDescriptionDefault
--name <name>Program name(prompted)
--domain <domain>Root domain(prompted)
--commission-type <type>percent, fixed, recurring_percent, recurring_fixed(prompted)
--commission-rate <rate>Commission rate (% or $)Prompt pre-fills 40 for the percent types only; no default for the fixed types
--cookie-duration <days>Cookie window in days90, applied in both modes
--duration-months <months>Recurring commission duration in months, saved on the program (see the warning below)Prompt pre-fills 12, and is only asked for the recurring types
--no-wizardSkip the auto-install wizard and print manual setup steps—

The 40 and 12 above are values the prompt offers, not values the CLI falls back to.

init saves --commission-type, --commission-rate and --duration-months on the program, but it does not set what partners are paid. Payouts follow the program's Default partner group, which starts from Affitor's platform default commission. After init, open the program in the dashboard and set the commission on the Default group; that is the value used for every sale.

In affitor 0.4.1 the duration prompt and the summary also label 0 as "lifetime". In Affitor, 0 means one-time (the first purchase, per product when the sale carries a product id) and an empty duration means lifetime.

With --no-interactive there is no prompt, so --name, --domain, --commission-type and --commission-rate must all be passed or the run stops with missing_options.

Example (non-interactive):

npx affitor init \
  --name "My SaaS" \
  --domain example.com \
  --commission-type recurring_percent \
  --commission-rate 30 \
  --duration-months 12 \
  --no-interactive

Files created:

All four land inside .affitor/, not in the project root:

FilePurpose
.affitor/config.jsonProgram ID, domain, commission and cookie settings. No secrets
.affitor/.env.exampleEnvironment variables template
.affitor/AGENTS.mdTracking code snippets and AI agent instructions
.affitor/skills.mdBackward-compatible copy of AGENTS.md

The API key itself goes to .affitor/.env, which init adds to your .gitignore along with .affitor/.env.*.


affitor onboard

The recommended one-shot integration. Wires Affitor into this app end-to-end: detect the stack → install browser tracking → add the sale call → verify. Run it from your project root after affitor init.

npx affitor onboard

This is the flagship path for AI coding agents: a single command that finds your framework and payment provider, applies the integration, and proves attribution works — instead of pasting snippets by hand.

FlagDescriptionDefault
--api-key <key>Program API key — overrides the env var / .affitor/.env(from config)
--yesAuto-confirm all diffs (apply every change without prompting)false
--jsonMachine-readable output for agents; never auto-edits filesfalse
--no-interactiveSkip prompts and apply changes without confirmationfalse

An API key is required. onboard resolves it from --api-key, the AFFITOR_API_KEY env var, or .affitor/.env (written by affitor init). If none is found it exits non-zero with no_api_key.

What it does

onboard runs four phases in order:

  1. Detect — inspects the project to identify the framework (Next.js app/pages router, Fastify, Express, plain Node) and the payment provider (Stripe, Polar, Lemon Squeezy, Paddle).
  2. Browser tracking — installs @affitor/sdk and wires the <AffitorTracker /> component via a diff-preview, scaffolding lib/affitor.ts (the same install wizard affitor init uses).
  3. Server sale — for Stripe, locates your webhook handler and injects the affitor.trackSale call after stripe.webhooks.constructEvent in the checkout.session.completed case. Stripe is the only provider with an automatic edit; for every other provider it prints the snippet for you to paste and reports the step as manual. It also persists AFFITOR_API_KEY into .env / .env.local (never overwriting an existing value).
  4. Verify — fires the synthetic click → lead → sale chain through the real attribution pipeline, then polls program readiness until integration_verified is reached.

Safety and idempotency

onboard is idempotent — re-running it skips any step already applied (an existing AFFITOR_API_KEY, a webhook that already reports the sale). It never force-edits payment code it can't place confidently: when the webhook shape isn't cleanly recognized, or no webhook is found, or the provider is not Stripe, it degrades to printing the exact snippet for you to paste rather than guessing an edit site. Auto-edits to the payment handler always show a diff and ask for confirmation first (unless --yes / --no-interactive).

In --json mode onboard performs no file edits — the steps that would have written a file report manual so an agent drives the edits explicitly, then it still fires the verification chain and polls readiness.

Example (agent / non-interactive)

npx affitor onboard --api-key affitor_xxx --yes --json

The JSON summary reports the per-step status and the final verdict:

{
  "program_id": "430",
  "steps": [
    { "step": "detect", "status": "ok", "detail": "framework=next-app, provider=stripe" },
    { "step": "browser_tracking", "status": "skipped", "detail": "json mode" },
    { "step": "server_sale", "status": "manual", "detail": "json mode (no auto-edit)" },
    { "step": "env_key", "status": "manual", "detail": ".env: json mode (no auto-edit)" }
  ],
  "integration_verified": true
}

detect reports ok, browser_tracking reports skipped in --json mode, and the steps that would edit a file report manual.

Once onboard reaches the verification phase, --json always prints this summary. program_id, steps and integration_verified are always present, though program_id is null when no readiness verdict was obtained. blocker, next_action and error are omitted rather than sent as null, so read them with that in mind.

The only way to get no summary is for onboard to stop before it runs: with no API key it prints {"error": "no_api_key"} and exits non-zero.

What integration_verified: false means. The verification chain and the readiness poll are separate: if the chain fails to run, onboard carries on polling anyway, so a chain failure never decides the verdict. Readiness does. Three cases produce false, and the other fields tell them apart:

CaseWhat the summary showsWhat to do
Readiness answered, a gate has not passedblocker and next_action name the gateResolve it and re-run affitor onboard
No readiness verdict — every poll failedNo blocker, no next_action, no errorNothing was determined. Check the API is reachable and re-run
The API key is past its rotation deadlineerror.code is api_key_rotation_requiredReplace the key, then re-run

A readiness poll that succeeds after a failed chain still reports integration_verified: true — the chain is one way to generate events, not the thing being measured.


affitor setup stripe

Connect your Stripe account so payments are tracked automatically.

npx affitor setup stripe

What happens:

  1. Opens Stripe Connect OAuth in your browser
  2. You authorize Affitor to read your payment data
  3. A webhook endpoint is created on your Stripe account
  4. Connection is saved to your program config
FlagDescription
--stripe-client-id <id>Override Stripe Connect client ID
--stripe-secret-key <key>Override Stripe secret key

Environment variables (alternative to flags):

  • STRIPE_CONNECT_CLIENT_ID or AFFITOR_STRIPE_CLIENT_ID
  • STRIPE_SECRET_KEY or AFFITOR_STRIPE_SECRET_KEY

Webhook events subscribed:

customer.created
checkout.session.completed
invoice.paid
invoice.payment_failed
charge.refunded
customer.subscription.deleted

The endpoint is created on your connected Stripe account with the delivery URL <api-url>/webhooks/stripe/<program-id> — for the default API URL, https://api.affitor.com/webhooks/stripe/430. That is the URL this command sends to Stripe; it is the value to check in the Stripe dashboard if deliveries are not arriving.

Confirm delivery before relying on it

After running this command, send a test event from the Stripe dashboard and confirm the sale shows up in Affitor before you treat Stripe tracking as live. If nothing arrives, report the sale from your own backend instead — see Track Sale or the Stripe integration guide, which does not depend on this endpoint.


affitor status

Check program health — tracking status, Stripe connection, and recent events.

npx affitor status

Example output:

  ╭──────────────────╮
  │   My SaaS        │
  │   example.com    │
  │                  │
  │   Program ID: 42 │
  ╰──────────────────╯

✓ Stripe: connected
⚠ DNS: not configured

  Events (last 24h):
    Clicks:  142
    Leads:   23
    Sales:   8

  Active partners:      5
  Pending commissions:  3

affitor test [event-type]

Write one flagged test event for your program, to confirm your key works and that events reach your dashboard.

npx affitor test click    # Test click event
npx affitor test lead     # Test lead event
npx affitor test sale     # Test sale event

The event type defaults to click. Anything other than click, lead or sale stops with invalid_event_type.

What this does and does not prove. The command creates a single record flagged is_test: true, which appears in your dashboard with a test badge. It confirms that your API key authenticates and that the program accepts events.

It does not exercise attribution. The record is inserted directly rather than run through the attribution pipeline, so no partner is resolved and no commission is created — the response always reports attributed: false, for every event type, and that is expected rather than a fault.

To check that attribution itself works, run affitor onboard, which fires a synthetic click → lead → sale chain through the real pipeline and then polls readiness.


Config File

Configuration is stored in .affitor/config.json:

{
  "version": 2,
  "program_id": "430",
  "domain": "example.com",
  "commission": {
    "type": "recurring_percent",
    "rate": 40,
    "duration_months": 12
  },
  "cookie": {
    "name": "affitor_click_id",
    "duration_days": 90
  },
  "stripe_connected": false,
  "api_url": "https://api.affitor.com"
}

Secrets (the program API key, and provider webhook secrets) are not stored in config.json — they live in .affitor/.env (AFFITOR_API_KEY, AFFITOR_PROGRAM_ID), which the CLI adds to .gitignore. Legacy v1 configs that carried an inline api_key are migrated automatically on the next CLI run.


When a command fails

A command that cannot do its job exits non-zero. Three outcomes are the exception, because they are results rather than failures, and each one is reported in the output instead:

CommandReports a negative outcome with exit 0Read instead
affitor onboardVerification did not passintegration_verified in the summary
affitor testThe test event was not receivedreceived in the JSON result
affitor whoami --jsonNo session is storedlogged_in

What --json puts on error

error does not have one shape across the CLI. Check which of the three you are reading before you branch on it.

ShapeWhereExample
A machine codeEvery check a command makes before it calls the API{"error": "not_logged_in"}
The API's message, with the HTTP status beside itinit, status, test when the API rejects the call{"error": "Invalid API token", "status": 401}
A code with the message in its own fieldsetup stripe{"error": "api_error", "message": "..."}

onboard is different again. Once it gets past its own preflight checks it prints a summary object, and in that summary error is itself an object rather than a string — the one place the CLI hands you the API's own error code:

{
  "program_id": "430",
  "steps": [],
  "integration_verified": false,
  "error": { "code": "api_key_rotation_required", "message": "..." }
}

Branch on error.code there; elsewhere, compare error as a string.

Two gaps are worth knowing before you build on this output:

  • login and programs do not convert failures into JSON. If the API call inside them fails, they surface the raw error rather than an {"error": ...} object, so a script should not assume parseable JSON from them.
  • onboard does not report a lost connection as an error. If every readiness poll fails, it still prints a summary with integration_verified: false and no error field. Not verified is not the same as verified-as-broken — see what that verdict means.

Codes

Condition--json codeCommandsWhat to do
Not logged innot_logged_ininit, programsRun affitor login, then re-run the command
No config in this directoryno_configstatus, test, setupRun the command from the project root where you ran affitor init, or run affitor init to create a program here
Already configuredalready_configuredinitinit found an existing .affitor/config.json and changed nothing. Use affitor status to check that program. Do not run init to recover a key — it only ever creates a new program
Missing required optionsmissing_optionsinit with --no-interactivePass --name, --domain, --commission-type and --commission-rate
Invalid event typeinvalid_event_typetestaffitor test takes click, lead, or sale
No API keyno_api_keyonboardPass --api-key, set AFFITOR_API_KEY, or run from a directory with .affitor/.env
API rejected the call, account sessionerror is the API message, status the HTTP statusinitinit authenticates with your account session, not a program key. A 401 here means the session is invalid or expired — run affitor login again. Do not regenerate a program key for this: it will not fix the session, and it breaks whatever is still using the old key
API rejected the call, program keyerror is the API message, status the HTTP statusstatus, testOn a 401, the key matches no program or was replaced. Compare it against the value you stored when it was issued; if you no longer hold that value, ask a workspace owner to regenerate the key under Settings → API Key and deploy the new one
API rejected the callapi_error, with the API message on message and no statussetup stripeRead message. A 401 here is a program-key problem, as in the row above
Stripe authorization failedstripe_oauth_errorsetup stripeRead message, then run affitor setup stripe again
No usable responsenetwork_errorinit, status, test, setup stripeSee below
Anything elseunexpected_errorinit, status, test, setup stripeRe-run with --verbose and keep the output

network_error does not mean the request was ignored

The CLI makes at most three attempts in total — the first call plus two retries — before reporting network_error, so by the time you see it the request may already have been sent three times. (API errors, including 429, are not retried; they surface on the first response.)

network_error covers two different situations, and the CLI cannot tell them apart: a connection that never delivered the request, and a response that arrived with a success status but a body the CLI could not parse. In the second case the server may have done the work and only the reply was lost — a success status is not proof that the write completed.

So treat the server state as unknown, not untouched, and verify the specific record before repeating anything that creates one:

  • init — run affitor programs and look for the program by name before running init again, or it will create a second one. There is no idempotency key on this call.
  • test — open your dashboard and look for the test event. The command sends no transaction_id, and the API generates a new event id on every call, so a repeat always writes another record.
  • Ordinary sales you report yourself through POST /api/v1/track/sale — resend the same transaction_id. The duplicate guard answers 409 for one already recorded, which is the one case here where a safe repeat is built in. A 409 confirms the sale exists; it does not confirm the commission was created, so check the sale in your dashboard if the first attempt may have failed partway. This does not extend to test-mode sales: a call carrying additional_data.test_mode is handled before the duplicate check and is not promised a 409.
© 2026 Affitor