c11r
The Commerce Layer CLI for coding agents. One grammar from a sandbox to a running organization:
c11r <noun> <verb> [id] [--field value ...]
This is the 1.0.0 line, in development on the clean-slate branch. It replaces every earlier command; nothing of 0.x is kept.
The grammar
- Standard verbs:
list,get,create,update,delete. Nouns with a state machine add their own verbs:c11r markets disable eu,c11r orders approve 1001,c11r payments capture 1001. - A plain noun has no code in the CLI. Its fields, filters and verbs come from the API's schema, and
c11r schema <noun>returns that schema. - Flags are the schema's fields, in kebab or snake case:
--price-listand--price_listare the same. - On
list,--<field> <value>filters by equality and--<field>.<op> <value>by another operator:--placed-at.gte 2026-11-01,--status.in placed,approved.--waitinglists what needs a decision now. - A dotted flag sets part of an object:
--customer.email [email protected]. An array takes a JSON array, values separated by commas, or the flag repeated; an array of objects takeskey=valuepairs, one object per flag:--items sku=COFFEE,quantity=2.--datatakes the fields as one JSON object, or@and a file name. - Amounts are in major units:
--amount 12.50.
Built-in nouns
| Command | What it does |
|---|---|
c11r status | Where this machine and folder stand: the organization picked and from where, its state, plan, mode, limits, usage and what is waiting; the stored login, if any. |
c11r schema <noun> | The noun's schema document: fields, filters, verbs, which verbs are destructive and which a loop may automate. |
c11r sandbox create [--name] [--seed] | An anonymous sandbox. Its token is stored on this machine and never printed; the folder is linked to it. |
c11r sandbox get, reset, claim, destroy | The sandbox's details; a fresh organization behind the same token; a claim URL for the human; the end of the sandbox. |
c11r loops create <name> --skill --every --allow [--max-total] [--market] | A loop in loops.yml: a skill, a policy and a cadence. Also list, get, update, delete. |
c11r loops run <name> | One pass of a loop. The CLI is not a daemon: a scheduler repeats the pass. |
c11r report sales [--from] [--to] [--by] [--interval] [--market] [--top] | Sales in a window, from the Metrics API. A plain noun: its flags come from the API's schema. |
c11r auth login [--client-id] [--secret-file] [--keep-file] | The human's own Commerce Layer login, from their Provisioning API application. See below. |
c11r auth logout, auth switch <slug> [--global] | Forgetting the login; the organization commands act on. |
A loop policy allows verbs: a bare verb is the skill's own noun (approve), <noun>.<verb> another Manage noun's verb on the same item (payments.capture). Verbs that delete or return money are never open to a policy.
Login, organization tokens and secrets
Two kinds of bearer. An organization token (sbx_... from sandbox create, org_... from organizations create and organizations connect) acts on one organization's data. The login is the human's own Provisioning API application, for organizations, members, roles and the credentials of a claimed organization. Each verb's schema says which it takes (verbs[].credential), and the CLI sends that one.
c11r auth login reads the client id and the client secret:
- in a terminal, typed at a prompt; the secret is not echoed;
- with no terminal, from a file:
--secret-file <path>, or by default~/.config/c11r/provisioning.secret. The file holdsclient_id=...andclient_secret=...lines (or JSON with those keys, or the secret alone, with--client-id). c11r reads it only when no other user can (mode 600), and deletes it once read unless--keep-fileis given; - with no terminal and no file, it stops at once with
secret_required,next_step.actionhuman_stepand the dashboard page innext_step.url, and makes the file, mode 600, as two lines for the human to complete:client_id=andclient_secret=. A file with either value missing gets the same answer, and nothing is sent to Commerce Layer. Thenc11r auth loginreads it.
Never from a flag or the environment. c11r exchanges the pair for a two hour token at Commerce Layer and sends only that token to the c11r API; the client secret never leaves this machine. Both are kept in credentials.json, mode 600, and never printed. c11r status shows the stored login as cli.login, { client_id, email, stored_at }, or null: read on this machine, never the secret. auth logout forgets the login; its last token ends by itself within two hours. Organization tokens stay.
organizations create and organizations connect <slug> answer an organization token once: it is stored like a sandbox token. connect --revoke-others also ends every other token of that organization, and asks for confirmation. With a login and no organization token for the organization named, an organization noun stops with no_credentials and the next step c11r organizations connect <slug>. members and roles with an organization token and no login are sent with that token, and the API says what is needed: a claim for a sandbox, a login for a claimed organization. organizations with no login stops here with login_required.
credentials create and credentials token answer a secret once. c11r writes it to a file with mode 600 and prints the file, the format and a hint, never the value: by default ~/.config/c11r/secrets/<org>/<name>.env (<id>-token.env for a token), or --out <path>, as KEY=value lines or with --format json one JSON object. The file is checked before the request is sent; one there already is replaced only with --yes. A file inside a git repository that git does not ignore gets the warning secret_not_ignored. With --dry-run nothing is written and secret_file says where it would go. credentials delete removes the files c11r wrote for that credential in ~/.config/c11r/secrets/<org>/ (its secret and its tokens) and lists them in removed_files; sandbox destroy removes the whole folder of that organization and names it in forgotten.secrets. A file written with --out is not tracked and stays: the warning out_files_not_tracked says so, when a secret of that organization was written with --out on this machine, or when a delete found nothing to remove. A token file carries CL_BASE_ENDPOINT beside the token. sales-calls book answers a cancel token once: it is kept in credentials.json and sales-calls cancel <booking> sends it.
Global flags
| Flag | What it does |
|---|---|
--json | JSON output. The default and the only format. |
--dry-run | On a write, the CLI sends it with ?dry_run=true: the API checks it all (state, plan, limits, Commerce Layer's current status) and answers what it would do, would: [...], and nothing is written. No confirmation is needed. Reads ignore it. |
--yes | Confirms a destructive verb in advance. |
--org <slug> | The organization to act on, by its stored token. |
--live | Live data, on a claimed organization: ?mode=live on every request. Without it, test mode. |
--help | Describes the noun or verb it follows, from its schema. |
Destructive verbs (deletes, orders cancel, payments void and refund, sandbox reset and destroy) ask for confirmation in a terminal, then send confirm=true. With no terminal, as when an agent or a schedule runs the CLI, they never wait: without --yes they stop at once with confirmation_required and exit code 3.
Which organization
--org, then C11R_ORG, then .c11r.json in the project folder (found by walking up, like .git), then the default set by auth switch --global, then the stored organization when exactly one is stored (never for a login noun's ?org=). With several stored and nothing naming one, the command stops with org_required and lists them.
- Tokens are only in
~/.config/c11r/credentials.json(or underXDG_CONFIG_HOME), mode 600. The CLI never prints one, only its hint. .c11r.jsonholds only{ "org": "<slug>" }: no secret, safe to commit.sandbox create,organizations createandauth switchwrite it, andorganizations connectwhen the folder has no link;resetfollows the new slug;destroyremoves it when it names the destroyed organization.- The project folder is the one with
.c11r.json, else the nearest withstore.ymlorloops.yml, else the current one.loops.ymlgoes there.
Output, errors and exit codes
Every answer is one JSON object on stdout, the API's envelope:
{ "data": {}, "state": { "lifecycle": "sandbox", "plan": null, "mode": "test", "org": "zztest-blue-beans", "expires_at": "2026-11-17T00:00:00Z" }, "warnings": [], "next": [] }
The CLI's own answers (help, loops.yml) have the same shape, with state null. Every error, the API's or the CLI's, is:
{
"error": {
"code": "state_required",
"message": "Live mode needs a claimed organization; this is a sandbox.",
"required_state": "claimed",
"required_plan": null,
"missing": null,
"path": null,
"next_step": { "action": "claim", "command": "c11r sandbox claim", "url": null }
},
"state": { "lifecycle": "sandbox", "plan": null, "mode": "test", "org": "zztest-blue-beans", "expires_at": "2026-11-17T00:00:00Z" }
}
code is stable. missing is { noun, why }, path the flag or field at fault, next_step.action one of create_sandbox, claim, upgrade, contact_sales, create_dependency, fix_request, retry_later, human_step, none. human_step is something only the human can do, such as saving a secret to a file, with a page in url when there is one. The CLI may add did_you_mean and details.
| Exit | Meaning | Codes |
|---|---|---|
| 0 | done | |
| 1 | unexpected failure: a bug in c11r, or a secret the disk refused after the API answered it | secret_not_written |
| 2 | the command is not valid | unknown_noun, unknown_verb, unknown_field, invalid_value, missing_value, missing_argument, missing_id, unexpected_argument, no_fields, org_required, invalid_config, secret_required, insecure_file |
| 3 | a destructive verb needs confirmation | confirmation_required, confirmation_declined |
| 4 | the organization needs another state or plan, has expired, or reached a limit | state_required, plan_required, expired, limit_reached |
| 5 | no such resource | not_found, claim_not_found |
| 6 | the API or Commerce Layer refused the request as it is | invalid_argument, invalid_transition, conflict, dependency_missing, commercelayer_refused, policy_forbidden, not_supported, forbidden |
| 7 | the API did not answer, is busy, or failed | api_unreachable, rate_limited, commercelayer_busy, commercelayer_unavailable, api_error, schema_unavailable, invalid_response |
| 8 | a wrong or revoked token, no token stored for the organization named, or no login | unauthorized, no_credentials, login_required |
rate_limited is retried twice with a short backoff, for reads and writes: nothing was processed. commercelayer_busy and commercelayer_unavailable are retried for reads only; a write that may have reached Commerce Layer fails with its next step. The one exception is sandbox create: it sends a fresh Idempotency-Key, and after a 5xx or a lost connection it repeats the create with the same key, so the API answers the sandbox it made and its token is stored. Each retry waits as long as the API says (retry_after seconds in the error, or the Retry-After header), at most 30 seconds; without it, 1 then 3 seconds. When the API asks for longer than 30 seconds, the CLI does not wait: the error goes to the agent, with retry_after, to plan around. Each retry writes a line to stderr.
Environment
| Variable | What it does |
|---|---|
C11R_API_URL | The API's base URL, https://api.commercelayer.ai/v1 by default; http://localhost:8787/v1 for wrangler dev. A URL without /v1 gets it. |
C11R_ORG | The organization, when --org is not given. |
C11R_TOKEN | An organization token to use as is, whatever is stored: for CI. |
C11R_PROVISIONING_TOKEN | A ready login token, used as is: for CI. Never a client secret. |
C11R_AUTH_URL | Commerce Layer's token endpoint, https://auth.commercelayer.io/oauth/token by default: for tests. |
C11R_TEST | 1 marks a sandbox as a test organization, like a name starting zztest. |
C11R_SCHEMA_CACHE | 0 turns off the 10 minute schema cache in ~/.cache/c11r. |
C11R_RETRY_CAP_S | The longest the CLI waits before one retry, 30 by default. |
C11R_TIMEOUT_MS | How long a request may take, 30000 by default (sandbox create waits longer). |