Command-line interface for the Two merchant APIs. Generated from the public OpenAPI specs, so every endpoint is exposed as a subcommand.
brew install two-inc/tap/twoctlDownload from releases and place on $PATH.
go install github.com/two-inc/twoctl-cli/cmd/twoctl@latesttwoctl ships completions for bash, zsh, fish, and PowerShell. The binary
generates them on demand via twoctl completion <shell>.
Linux:
twoctl completion bash | sudo tee /etc/bash_completion.d/twoctl > /dev/nullmacOS (with bash-completion@2 from Homebrew):
twoctl completion bash > "$(brew --prefix)/etc/bash_completion.d/twoctl"If you don't have write access to a system path, source the completion in your
~/.bashrc:
echo 'source <(twoctl completion bash)' >> ~/.bashrc# one-shot (re-run on shell start):
echo 'source <(twoctl completion zsh)' >> ~/.zshrc
# or persist to fpath:
twoctl completion zsh > "${fpath[1]}/_twoctl"If compinit hasn't been initialised in your ~/.zshrc, add:
autoload -Uz compinit && compinitbefore the completion source line.
twoctl completion fish | source # current shell
twoctl completion fish > ~/.config/fish/completions/twoctl.fish # persistenttwoctl completion powershell | Out-String | Invoke-Expression # current session
twoctl completion powershell >> $PROFILE # persistentAfter installing, open a new shell. Tab-completion works on subcommands, flags, and any flag value that has a known enum in the OpenAPI spec (country codes, currencies, statuses).
twoctl uses named contexts (kubectl-style). Each context bundles a base URL
and an OS-keychain entry holding the API key, so you can switch between
sandbox, prod, staging, cyber, perf, release, or any custom environment
without copy-pasting keys.
twoctl config set-context sandbox --url https://api.sandbox.two.inc --key secret_test_...
twoctl config set-context prod --url https://api.two.inc --key secret_prod_...
twoctl config get-contexts # show all contexts, * marks current
twoctl config use-context prod # switch the default
twoctl config current-context # print the current name
twoctl config delete-context temp # remove + clear keychain entryPer-invocation overrides (no switching):
twoctl --env prod company company-search --country NO --q two
twoctl --context cyber checkout get-order --order-id abc # kubectl-style alias
twoctl --url http://localhost:8080 --api-key secret_test_x ...Scripts can select a context without changing the saved selection:
TWOCTL_CONTEXT=staging twoctl order get --order-id abcContext selection follows --context, then --env, then non-empty
TWOCTL_CONTEXT, then the saved current context. --url overrides the
API URL and requires --api-key or TWO_API_KEY rather than using a stored
context key. API calls, auth whoami, auth login, auth logout, and
--dry-run honor the invocation context. config current-context continues
to show the saved selection; the environment override never writes it.
For built-in env names (prod, sandbox, staging, cyber, perf,
release) twoctl knows the URL. For any other name it falls back to
https://api.<name>.two.inc; override with --url.
API key resolution order (highest precedence first):
--api-keyflagTWO_API_KEYenv var- OS keychain entry for the active context
Keys are obtained from the Two merchant portal.
If a keychain lookup fails, twoctl reports the underlying error rather than
claiming no key is stored. On headless Linux without a running Secret
Service/DBus session, the error explains the required backend and suggests
TWO_API_KEY for headless use. Explicit --api-key and TWO_API_KEY values
bypass the keychain. A missing key still prompts you to run twoctl auth login;
keychain failures never fall back to storing keys in a file.
Set TWOCTL_CONFIG_HOME to use a dedicated twoctl configuration directory:
TWOCTL_CONFIG_HOME=/path/to/job-config twoctl config get-contextsDirectory precedence is TWOCTL_CONFIG_HOME, then
$XDG_CONFIG_HOME/twoctl, then ~/.config/twoctl. An empty override falls
back to the next location. Both config.yaml (and its lock) and updater
state.json use the selected directory; TWOCTL_CONFIG_HOME names that
directory directly, with no extra twoctl/ appended. Existing files are
not copied between locations. API keys stay in the OS keychain, keyed by
context name, so selecting a different directory does not isolate keys.
The command tree is resource-first: twoctl <resource> <action> [flags].
Every operation across every Two API is classified into a (resource, action) pair derived from the URL path + operationId, then registered
under the resource. Non-CRUD actions like cancel, refund, fulfill,
search, notify surface as first-class verbs alongside get/create/
edit/delete.
twoctl order get --order-id abc-123 # GET /v1/order/{order_id}
twoctl order create --file order.json # POST /v1/order
twoctl order cancel --order-id abc-123 # POST /v1/order/{id}/cancel
twoctl order fulfill --order-id abc-123 --file f.json
twoctl order refund --order-id abc-123 --file r.json
twoctl order edit --order-id abc-123 --file patch.json
twoctl company search --q "two" --country NO
twoctl billing-account get --billing-account-id ba_xxx
twoctl explain order create # JSON schema for the operation
twoctl api-resources --resource order # catalog filtered by resource
twoctl api-resources --action cancel # everything that's a "cancel"Run twoctl <resource> on its own to see every action available. Each leaf
command's --help lists flags derived from the OpenAPI operation;
request bodies go via --file path.json or --data '{...}'.
Top-level non-resource commands:
twoctl |
Purpose |
|---|---|
config set-context / use-context / get-contexts / current-context / delete-context |
Manage contexts (kubectl naming) |
auth login / logout / whoami |
Store the API key in the OS keychain |
explain <resource> <action> |
JSON schema for an operation, no network call |
api-resources |
JSON catalog of every operation, for agent discovery |
version |
Print version and build info |
upgrade |
Self-upgrade from the latest GitHub release |
--context / --env (alias) |
Per-invocation context override |
twoctl is designed to be driven by both humans and LLM agents.
twoctl api-resources [-r <resource>] [-a <action>]— emits a JSON catalogue of every operation (command, method, path, flags with kind/type/required, body status). Pipe this to an agent's planner so it can pick the right command without parsing--helptext.twoctl <resource> <action> --describe— JSON view of one operation's full spec (parameters, request body schema, response schemas) without a network call. Also reachable astwoctl explain <resource> <action>.twoctl <resource> <action> --dry-run— render the HTTP request that would be sent (URL, redacted headers, body presence) and exit. No credentials needed.-o table|json|yaml(defaultauto: table on TTY, JSON when piped).- Errors are emitted as JSON on stderr with shape
{error, message, status, response, request_id}. Stdout stays clean for piping.
Operations with a page_cursor parameter support --all to collect every
page and --no-paginate to explicitly fetch one page. Without either flag,
table output on an interactive terminal renders one complete page, then asks
next page? [Y/n] on stderr when a next cursor is present. Enter or y
loads the next page; n, other answers, or end-of-input stops.
Prompts require terminal stdin, stdout and stderr. JSON/YAML output, pipes,
redirected streams and --file - never prompt, preserving single-page
machine-readable responses and request-body stdin. --all still works in
those modes and emits one combined result. --all and --no-paginate
cannot be combined. Dry runs do not fetch or prompt.
JSON payloads supplied with --data, --file, or --file - are checked
against the embedded OpenAPI request schema before the API call (including
--all and --dry-run). Invalid JSON, missing required fields, wrong types,
and enum/constraint mismatches return usage exit code 2 without sending.
Unknown fields in structured objects produce warnings on stderr but are
sent unchanged to tolerate schema drift. Explicitly open and typed maps
retain their schema semantics. Payload values are not printed in validation
errors or warnings. Use --describe to inspect the expected schema.
| Code | Meaning |
|---|---|
| 0 | success |
| 1 | generic error |
| 2 | usage / missing required input |
| 3 | authentication or authorisation (401/403) |
| 4 | not found (404) |
| 5 | rate limited (429) |
| 6 | server error (5xx) |
| 7 | network / transport failure |
twoctl version and twoctl --version print the same version, Go runtime,
and platform information. twoctl version --json emits a JSON object with
version, go_version, os, and arch fields for scripts. Version queries
do not read credentials, check for upgrades, or make network requests.
Use -v (or --verbose) to write one JSON diagnostic per API request to
stderr, including method, URL, HTTP status, elapsed milliseconds, and the
upstream request ID when present. This includes successful calls and every
page fetched by --all; the duration includes retries and reading the body.
Transport and body-read failures include an error field.
Use -vv to include request/response headers and bodies. Sensitive header,
query, and JSON field values are redacted. Body diagnostics use the existing
4 MiB cap; an unavailable or oversized request body is reported without
interrupting the API request. Treat detailed output as API data when sharing
it. Stdout remains the operation response, and successful calls produce no
HTTP diagnostics by default.
API requests retry transient network errors, HTTP 429, and HTTP 5xx up to
three times by default (four attempts including the initial request).
Use --max-retries N to set the number of retries per request, or
--max-retries 0 to make a single attempt:
twoctl order get --order-id abc-123 --max-retries 0
twoctl billing-statement get --all --max-retries 1The setting applies to each page when using --all. Retries keep the same
idempotency key and request body. The existing 60-second request timeout
still bounds the attempts and backoff for each request.
twoctl checks for new releases at most once every 24 hours and prompts when one is available. The prompt offers:
yinstall nownnot now (default)sskip this version (won't ask again until the next release)
Direct controls:
twoctl upgrade # check + install
twoctl upgrade --check # only report, don't install
twoctl upgrade --reset-skips # forget every skipped version
twoctl upgrade --disable-autocheck # silence the prompt entirely
twoctl upgrade --enable-autocheck # turn it back on--no-upgrade-check is a per-run override for scripts. State lives in ~/.config/twoctl/state.json.
Every operation across all twelve published Two specs surfaces as a
twoctl <resource> <action> command. Actions are auto-derived from
operationId prefixes (get_/retrieve_/list_/search_, create_/make_/
fulfill_/refund_/cancel_/...) with HTTP-method fall-back when no prefix
matches. Run twoctl api-resources for the full machine-readable
catalog (120+ operations).
| Spec | Source |
|---|---|
| Checkout (Order) | checkout-api.yaml |
| Billing Account | billing-account-api.yaml |
| Repay | repay-api.yaml |
| Recourse | recourse-api.yaml |
| Company | company-api.yaml |
| Limits | limits-api.yaml |
| Autofill | autofill-api.yaml |
| Business Registration | business-registration-api.yaml |
| Marketplace | marketplace-api.yaml |
| Trade Account v2 | trade-account-v2-api.yaml |
| Trade Account v3 | trade-account-v3-api.yaml |
| Webhooks | webhooks-api.yaml |
The deprecated stubs (search-api.page-deprecated,
trade-account-api-deprecated-v1, trade-account-api.page-deprecated)
are not wired up.
The openapi/ specs are vendored from two-inc/docs.
When that source changes, a repository_dispatch event fires the regenerate.yml workflow,
which pulls fresh specs, runs codegen, and opens a PR.
To regenerate locally:
./scripts/codegen.sh
go build ./...CI runs make check-generated on every pull request and main-branch push.
It regenerates the embedded specs from the committed openapi/ inputs and
fails if any embedded file changes or is newly generated but untracked.
When changing source specs or the preprocessor, run make regen and commit
the updated cmd/twoctl/cli/specs/ files with the change. This check uses
vendored inputs and does not fetch upstream specs or require API credentials;
the nightly regenerate workflow continues to handle upstream refreshes.
This repository is generated from the OpenAPI specs in two-inc/docs.
Bug reports and PRs for the CLI itself (flags, output formatting, ergonomics) are welcome here.
For API behaviour or schema changes, please open an issue against two-inc/docs.
twoctl does not collect telemetry. There are no usage metrics, no
anonymous identifiers, no phone-home. The only outbound calls the CLI
makes are:
- The Two API endpoint configured by your active context (e.g.
https://api.sandbox.two.inc). https://api.github.com/repos/two-inc/twoctl-cli/releases/latestfor the daily self-upgrade check. Disable with--no-upgrade-check,TWOCTL_SKIP_UPGRADE_CHECK=1, ortwoctl upgrade --disable-autocheck.
Self-upgrade downloads come from github.com/two-inc/twoctl-cli/releases
when you (or the prompt) chooses to install.
Releases follow semver. Breaking changes to flags, commands, or behaviour bump the major version.
When a command or flag is renamed, the old name keeps working for at least one minor cycle and prints a deprecation note to stderr (never stdout). The note includes the version in which the alias will be removed.
MIT - see LICENSE.