CLI

CLI Command Reference

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

View as Markdown

Every npx affitor command, flag, and option in one place. The agent setup, doctor, and Stripe-aware onboarding behavior below require CLI 0.4.5.

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

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 prints {"logged_in": false} and also exits 1.


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 the Affitor dashboard (program menu → Program settings, then Developers → API keys). 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.

Warning

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 agent setup

Preview and install project-scoped local MCP configuration and an Affitor skill. Run from your app's project root; no account login is required for this file setup.

npx affitor@0.4.5 agent setup --client claude_code --dry-run --json
npx affitor@0.4.5 agent setup --client claude_code --yes --json
FlagBehavior
--client <name>claude_code, cursor, or codex; repeat to select more than one from your terminal
--dry-runPreview without writing files, even with --yes
--yesApply the selected clients' plan; global --auto-confirm also applies it
--jsonOne JSON result; does not prevent writes when apply is selected

A detected coding-agent caller defaults to itself and cannot select other clients. With no caller or explicit selection, an interactive run asks which clients to select; unattended runs need --client.

statusExitMeaning
planned0Dry-run only; no files written
needs_confirmation1Not applied; review the preview and explicitly apply
needs_human0Applied local files; restart the client and check readiness; Codex also needs the printed manual MCP step
error1Setup failed; read message and run a fresh dry-run

A run with no selected clients and no --dry-run also exits 1. The preview contains paths, operations, and backup paths, not the contents of existing configs. For file locations, private credentials, and Codex steps, follow Set up a coding agent.


affitor doctor

Read-only static check of local agent configuration. It does not call Affitor, start an agent session, or verify integration readiness.

npx affitor@0.4.5 doctor --client claude_code --json

Repeat --client to check selected clients. Without it, a detected coding-agent caller selects itself; otherwise the command checks existing Claude Code/Cursor config files, or both clients when neither file exists. Codex must be selected explicitly when there is no detected caller.

Results contain mode: "static", per-client status, credential, warnings, and liveSession: "not_checked". Exit 0 and top-level aligned require every client to have aligned files, a present_unverified credential, and no warnings. Other static results produce needs_attention and exit 1; caught command errors report status: "error" and message.

Codex always reports client status manual, so its static check exits 1 even after manual configuration. A present_unverified credential has only passed the local parser, not authentication. Doctor reads AFFITOR_API_KEY or .affitor/.env, not global --api-key/--api-url flags. See the doctor results and next steps.


affitor onboard

The recommended one-shot integration. Wires Affitor into this app end-to-end: detect the stack → install browser tracking → review sale tracking → 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, reads the program's binding before deciding whether a sale call is appropriate (see below). Only an explicitly confirmed manual Stripe path can auto-edit a recognized checkout.session.completed handler. Other providers report manual and print a recipe outside JSON mode. It also attempts to persist AFFITOR_API_KEY into .env / .env.local; an existing value or uncertain env layers are left unchanged.
  4. Verify — fires the synthetic click → lead → sale chain through the real attribution pipeline, then polls program readiness until integration_verified is reached.

Stripe binding in 0.4.5

Before the install wizard, onboard reads stripe_account_bound from GET /api/v1/cli/status for the selected key, provided the app's env layers agree on that runtime key. It does not use stripe_connected as the sale-writer decision.

Binding decisionserver_sale resultWebhook behavior
stripe_account_bound: true and runtime key is certainskippedNo trackSale added; use the Connect metadata recipe
false, missing field, failed status request, or uncertain runtime keymanual in --yes, --auto-confirm, --no-interactive, or --json runsWebhook unchanged; confirm the runtime program and sale path yourself
Unknown binding, interactive run without auto-confirmAsks whether to keep the webhook unchangedKeeping it is the default; only choose No after ruling out current Connect sales, shared accounts, and delayed events after disconnect

A current false binding does not prove that Connect cannot still record a payment. After explicitly choosing manual tracking, the CLI previews a recognized handler change and asks before applying it. It does not add subscription renewal handlers.

Existing trackSale calls are not removed automatically. Inspect them before using Connect and manual tracking together; see the Stripe integration guide.

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 is not recognized, no webhook is found, or the provider is not Stripe, it reports manual and prints a recipe outside JSON mode 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. Steps may report manual, skipped, or already according to the binding and existing files. It still fires the synthetic verification chain and polls readiness, so JSON mode is not a read-only API check.

Example (agent / non-interactive)

# Configure AFFITOR_API_KEY privately before running.
npx affitor@0.4.5 onboard --yes --json

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

{
  "program_id": "42",
  "steps": [
    { "step": "detect", "status": "ok", "detail": "framework=fastify, provider=stripe" },
    { "step": "browser_tracking", "status": "skipped", "detail": "json mode" },
    { "step": "server_sale", "status": "skipped", "detail": "stripe connected: Affitor records the sale, no trackSale added" },
    { "step": "env_key", "status": "manual", "detail": ".env: json mode (no auto-edit)" }
  ],
  "integration_verified": true
}

This example has a confirmed Stripe binding and no runtime env file, so the sale step is skipped and the env step is manual. It shows the JSON shape, not a readiness guarantee for your program.

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. onboard exits 0 only when integration_verified is true; otherwise it prints the summary and exits 1.

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

Stripe is connected from the Affitor dashboard, not from the CLI: open the program menu, choose Program settings, open Payments, and use Connect Stripe. Affitor's own webhook then records sales, renewals and refunds for that account; nothing needs to be created on your Stripe account. To attribute each sale, add Affitor metadata to your Checkout Session, as described in the Stripe integration guide.

npx affitor setup stripe

From CLI 0.4.3 this command only prints those directions and exits 1 ({"error": "stripe_setup_moved", "message": "..."} with --json). Earlier versions start a browser flow that cannot complete; do not use it.


affitor setup dns

npx affitor setup dns

DNS tracking is not available yet. This command only prints a "coming soon" notice ("status": "not_implemented" with --json) and changes nothing. It reads .affitor/config.json first and exits 1 without it (no_config).


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. Since CLI 0.4.2 that includes affitor onboard when the integration is not verified (integration_verified: false) and affitor whoami --json when no session is stored; both still print their JSON first. One outcome is the exception, because it is a result rather than a failure, and it is reported in the output instead:

CommandReports a negative outcome with exit 0Read instead
affitor testThe test event was not receivedreceived in the JSON result

What --json puts on error

error does not have one shape across the CLI. Check the command before you branch on it. agent setup and doctor instead report status and message on caught errors, as described above.

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": "stripe_setup_moved", "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, setup dnsRun 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 (program menu → Program settings, then Developers → API keys) and deploy the new one
Stripe is connected elsewherestripe_setup_movedsetup stripeConnect Stripe in the dashboard: program menu → Program settings → Payments, Connect Stripe
No usable responsenetwork_errorinit, status, testSee below
Anything elseunexpected_errorinit, status, testRe-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.

Share feedback

Tell us what is missing or unclear on “CLI Command Reference”.

Last updated Oct 18, 2026