CLI
CLI Command Reference
Complete reference for all npx affitor commands, flags, and options.
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:
| Flag | Description |
|---|---|
--json | Output as JSON (for AI agents and scripts) |
--no-interactive | Skip all prompts, fail on missing values |
--auto-confirm | Auto-yes to confirmation prompts |
--quiet | Suppress non-essential output |
--api-key <key> | Override API key from config |
--api-url <url> | Override API URL |
--verbose | Show debug output |
-V, --version | Show version number |
-h, --help | Show help |
Which commands need a login session
Two different credentials are in play, and they are not interchangeable.
| Credential | Where it lives | Which commands use it |
|---|---|---|
| Account session | ~/.affitor/credentials.json, written by affitor login | affitor init, affitor programs, affitor whoami |
| Program API key | AFFITOR_API_KEY in the environment or .affitor/.env, or --api-key | affitor 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 loginThe 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 logoutaffitor whoami
Show which account the stored session belongs to, and when its token expires.
npx affitor whoamiWith 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 programsNeeds 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 initinit 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.
| Flag | Description | Default |
|---|---|---|
--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 days | 90, 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-wizard | Skip 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-interactiveFiles created:
All four land inside .affitor/, not in the project root:
| File | Purpose |
|---|---|
.affitor/config.json | Program ID, domain, commission and cookie settings. No secrets |
.affitor/.env.example | Environment variables template |
.affitor/AGENTS.md | Tracking code snippets and AI agent instructions |
.affitor/skills.md | Backward-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| Flag | Behavior |
|---|---|
--client <name> | claude_code, cursor, or codex; repeat to select more than one from your terminal |
--dry-run | Preview without writing files, even with --yes |
--yes | Apply the selected clients' plan; global --auto-confirm also applies it |
--json | One 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.
status | Exit | Meaning |
|---|---|---|
planned | 0 | Dry-run only; no files written |
needs_confirmation | 1 | Not applied; review the preview and explicitly apply |
needs_human | 0 | Applied local files; restart the client and check readiness; Codex also needs the printed manual MCP step |
error | 1 | Setup 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 --jsonRepeat --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 onboardThis 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.
| Flag | Description | Default |
|---|---|---|
--api-key <key> | Program API key — overrides the env var / .affitor/.env | (from config) |
--yes | Auto-confirm all diffs (apply every change without prompting) | false |
--json | Machine-readable output for agents; never auto-edits files | false |
--no-interactive | Skip prompts and apply changes without confirmation | false |
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:
- 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).
- Browser tracking — installs
@affitor/sdkand wires the<AffitorTracker />component via a diff-preview, scaffoldinglib/affitor.ts(the same install wizardaffitor inituses). - 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.completedhandler. Other providers reportmanualand print a recipe outside JSON mode. It also attempts to persistAFFITOR_API_KEYinto.env/.env.local; an existing value or uncertain env layers are left unchanged. - Verify — fires the synthetic click → lead → sale chain through the real attribution pipeline, then polls program readiness until
integration_verifiedis 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 decision | server_sale result | Webhook behavior |
|---|---|---|
stripe_account_bound: true and runtime key is certain | skipped | No trackSale added; use the Connect metadata recipe |
false, missing field, failed status request, or uncertain runtime key | manual in --yes, --auto-confirm, --no-interactive, or --json runs | Webhook unchanged; confirm the runtime program and sale path yourself |
| Unknown binding, interactive run without auto-confirm | Asks whether to keep the webhook unchanged | Keeping 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 --jsonThe 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:
| Case | What the summary shows | What to do |
|---|---|---|
| Readiness answered, a gate has not passed | blocker and next_action name the gate | Resolve it and re-run affitor onboard |
| No readiness verdict — every poll failed | No blocker, no next_action, no error | Nothing was determined. Check the API is reachable and re-run |
| The API key is past its rotation deadline | error.code is api_key_rotation_required | Replace 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 stripeFrom 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 dnsDNS 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 statusExample 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: 3affitor 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 eventThe 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:
| Command | Reports a negative outcome with exit 0 | Read instead |
|---|---|---|
affitor test | The test event was not received | received 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.
| Shape | Where | Example |
|---|---|---|
| A machine code | Every check a command makes before it calls the API | {"error": "not_logged_in"} |
| The API's message, with the HTTP status beside it | init, status, test when the API rejects the call | {"error": "Invalid API token", "status": 401} |
| A code with the message in its own field | setup 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:
loginandprogramsdo 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.onboarddoes not report a lost connection as an error. If every readiness poll fails, it still prints a summary withintegration_verified: falseand noerrorfield. Not verified is not the same as verified-as-broken — see what that verdict means.
Codes
| Condition | --json code | Commands | What to do |
|---|---|---|---|
| Not logged in | not_logged_in | init, programs | Run affitor login, then re-run the command |
| No config in this directory | no_config | status, test, setup dns | Run the command from the project root where you ran affitor init, or run affitor init to create a program here |
| Already configured | already_configured | init | init 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 options | missing_options | init with --no-interactive | Pass --name, --domain, --commission-type and --commission-rate |
| Invalid event type | invalid_event_type | test | affitor test takes click, lead, or sale |
| No API key | no_api_key | onboard | Pass --api-key, set AFFITOR_API_KEY, or run from a directory with .affitor/.env |
| API rejected the call, account session | error is the API message, status the HTTP status | init | init 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 key | error is the API message, status the HTTP status | status, test | On 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 elsewhere | stripe_setup_moved | setup stripe | Connect Stripe in the dashboard: program menu → Program settings → Payments, Connect Stripe |
| No usable response | network_error | init, status, test | See below |
| Anything else | unexpected_error | init, status, test | Re-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— runaffitor programsand look for the program by name before runninginitagain, 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 notransaction_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 sametransaction_id. The duplicate guard answers409for one already recorded, which is the one case here where a safe repeat is built in. A409confirms 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 carryingadditional_data.test_modeis handled before the duplicate check and is not promised a409.