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:
| 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, 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 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 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 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 Affitor under Settings → API Key. 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 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 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, locates your webhook handler and injects the
affitor.trackSalecall afterstripe.webhooks.constructEventin thecheckout.session.completedcase. 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 asmanual. It also persistsAFFITOR_API_KEYinto.env/.env.local(never overwriting an existing value). - Verify — fires the synthetic click → lead → sale chain through the real attribution pipeline, then polls program readiness until
integration_verifiedis 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 --jsonThe 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:
| 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
Connect your Stripe account so payments are tracked automatically.
npx affitor setup stripeWhat happens:
- Opens Stripe Connect OAuth in your browser
- You authorize Affitor to read your payment data
- A webhook endpoint is created on your Stripe account
- Connection is saved to your program config
| Flag | Description |
|---|---|
--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_IDorAFFITOR_STRIPE_CLIENT_IDSTRIPE_SECRET_KEYorAFFITOR_STRIPE_SECRET_KEY
Webhook events subscribed:
customer.created
checkout.session.completed
invoice.paid
invoice.payment_failed
charge.refunded
customer.subscription.deletedThe 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.
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 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. Three outcomes are the exception, because they are results rather than failures, and each one is reported in the output instead:
| Command | Reports a negative outcome with exit 0 | Read instead |
|---|---|---|
affitor onboard | Verification did not pass | integration_verified in the summary |
affitor test | The test event was not received | received in the JSON result |
affitor whoami --json | No session is stored | logged_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.
| 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": "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:
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 | 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 under Settings → API Key and deploy the new one |
| API rejected the call | api_error, with the API message on message and no status | setup stripe | Read message. A 401 here is a program-key problem, as in the row above |
| Stripe authorization failed | stripe_oauth_error | setup stripe | Read message, then run affitor setup stripe again |
| No usable response | network_error | init, status, test, setup stripe | See below |
| Anything else | unexpected_error | init, status, test, setup stripe | 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.