CLI
The tickwire command. Sign in, create a sandbox, list accounts, trade (with a reason) and see the result, forward webhooks to localhost, and tail your request log.
tickwire is a command-line tool built on the Python SDK.
It works the same for MT4 and MT5 accounts. Every command accepts --json, which prints
machine-readable output: one JSON object, or one per line for listen and logs tail.
pip install tickwire-cli # Python 3.11+; "tickwire-cli[keyring]" stores keys in the OS keychain
It isn’t on PyPI yet. We’re publishing it as tickwire-cli, together with the
Python SDK; until then, pip install tickwire-cli won’t find it. If you want
to use the CLI before then, contact us.
Two minutes in the sandbox
export TICKWIRE_SANDBOX_URL=https://sandbox.example # your sandbox API URL
tickwire sandbox create # a sandbox, one simulated MT5 account, a stored key
tickwire --sandbox accounts list
tickwire --sandbox trade open <account-id> --side buy --symbol EURUSD --volume 0.01 --reason "first test"
sandbox create calls POST /v1/sandbox/signup and stores the new twsk_ key in the sandbox
profile. The key is not printed unless you pass --show-key, because the server shows it only
once. A lost reply is reported as unknown and is not retried, because a retry could create a
second sandbox.
This key is restricted: it can manage accounts, trade and read. To use webhooks (listen,
trigger), sign in, create a sandbox key from your live key (POST /v1/sandbox/keys), and run
tickwire login --sandbox.
Global options
| Option | Meaning |
|---|---|
--json | JSON output on stdout. Notes and warnings still go to stderr |
--sandbox | Use the sandbox. It selects the sandbox profile and TICKWIRE_SANDBOX_URL |
--profile NAME | Use a named credential profile. The default is default (sandbox with --sandbox) |
--api-url URL | API base URL. Otherwise the CLI uses TICKWIRE_API_URL or TICKWIRE_SANDBOX_URL, then the profile’s URL. It must be https://; plain http:// is accepted only for localhost, 127.0.0.1 and [::1] (a local stack), because the key travels with every request |
You can put these options before or after the command.
Human output is terminal-safe: control characters in data from the server (escape sequences,
carriage returns, newlines inside a value, C1 controls, bidi overrides) are printed visibly as
\xNN or \uNNNN, never passed to your terminal. --json output is exact JSON.
login and logout
tickwire login --api-url https://api.example # hidden prompt
echo "$KEY" | tickwire login --sandbox --key-stdin # from a secret store or CI
tickwire logout
- The key is never a command-line argument, because arguments end up in shell history and
the process list. The CLI reads it from a hidden prompt, or from stdin when stdin is not a
terminal (or with
--key-stdin). - The key is never printed. Output shows a hint such as
twsk_…a1b2. - Before storing the key, the CLI checks it against the API with
GET /v1/accounts. A rejected key is not stored. Pass--no-verifyto skip the check. --sandboxaccepts onlytwsk_keys, and a live profile refuses them, so a sandbox key never ends up in a live profile or the other way round.- Storage: the OS keychain (service
tickwire-cli) when thekeyringpackage is installed and working. Otherwise the key goes incredentials.jsonwith mode0600. The file is written to a fresh temporary file (created exclusively, never through a symlink) and renamed into place. A config directory the CLI creates gets mode0700; an existing one is neverchmod-ed. The CLI refuses a credentials file or config directory that is a symlink or belongs to another user, and a credentials file other users can read (it tells you tochmod 600it, assshdoes).--no-keyringforces the file. - The config directory is
$TICKWIRE_CONFIG_DIR, else$XDG_CONFIG_HOME/tickwire, else~/.config/tickwire.config.jsonholds profiles (URL, sandbox flag, key hint) and never the key. TICKWIRE_API_KEYin the environment overrides any stored key, so CI jobs need no login.
accounts
tickwire accounts list
tickwire accounts get <account-id> --health # adds health: status, reasons, checked_at
trade
tickwire trade open <account-id> --side buy --symbol EURUSD --volume 0.01 [--sl 1.05 --tp 1.10] --reason "breakout"
tickwire trade close <account-id> <position-id> [--volume 0.5] --reason "take profit"
Trades use the guarded pipeline (POST /v1/execution/accounts/{id}/trade), so the account’s
pre-trade hooks apply.
--reasonis mandatory. It is sent asX-Reasonand kept in the audit log.- Every trade carries an
Idempotency-Key. The CLI generates one when you don’t pass--idempotency-key, and always prints it. - The result is one of:
| Outcome | Meaning | Exit code |
|---|---|---|
applied | The terminal confirmed it (evidence shows how) | 0 |
no_change | Nothing needed doing, e.g. the position was already closed | 0 |
rejected | Not executed. The error code says why | 3 |
held | A pre-trade hook holds it for approval. Nothing has executed yet | 4 |
unknown | It may have executed | 5 |
unknown is never retried, by the SDK or by the CLI. You get unknown when the reply is
lost (timeout, reset, 502/504) or when the server answers with an error and no outcome (5xx).
Check the account’s positions first. To try again, run the same command with
--idempotency-key <the printed key>: the server recognises the key and will not execute the
trade twice (idempotency). The SDK repeats a request with the same key only
when it knows the server didn’t process it: no connection, or a 408, 429 or 503.
Other exit codes: 1 means the API answered with an error, 2 means a usage or login problem, and 130 means you pressed Ctrl-C.
listen: webhooks on localhost
tickwire login --sandbox # an unrestricted sandbox key
tickwire --sandbox listen --forward-to http://localhost:3000/webhooks [--events "trade.*,account.*"]
On start, listen prints a listener signing secret (whsec_…). Configure your local
handler with it and verify deliveries exactly as in production
(webhooks): the same X-Tickwire-Signature, X-Tickwire-Timestamp and
Standard Webhooks webhook-* headers, and the same body shape. This secret is not your API key
and not an endpoint’s secret.
How it works:
POST /v1/webhook-listenerscreates a listener and returns its secret and a cursor (the current head of your event buffer). The cursor is opaque (<boot id>:<seq>).- The CLI polls
GET /v1/webhook-listeners/{id}/events?after=<cursor>every 2 seconds when idle (--interval). The server signs each event with the listener secret, at most 100 events per poll. - The CLI verifies both signatures and the timestamp. If an event fails verification, the CLI does not forward it and stops.
- It POSTs the exact signed bytes and headers to
--forward-toand prints the status and latency. A failed forward is reported and not retried.
What listen mode doesn’t do:
- It is not the delivery queue. Real endpoints get durable deliveries with retries for 72
hours. A listener only sees events published while it is running. Nothing is stored per
listener, and events published while nothing was listening are not replayed. To replay an
event, use
POST /v1/events/{id}/resendon a real endpoint. - Event ids differ. In listen mode an event’s id is
evl_<boot>_<seq>, not the id that a real endpoint receives for the same event. Each forwarded request carriesX-Tickwire-Listener: <id>. - Your own buffer. Listeners read only your last 1,000 events, so another account’s traffic can’t push yours out.
- Gaps get a warning. If the listener falls behind your event buffer, the
server sets
truncatedand the CLI prints a warning. After a server restart the server setsreset, sends the new buffer from the start, and the CLI prints a warning. Events from before the restart are not replayed. - Rate limits, per account and per minute: 10 listener starts, 90 polls and 10 triggers.
Over a limit you get
429 rate_limitedwithRetry-After. - Listener secrets are derived, not stored, so a listener survives server restarts. If the
secret ever changes, the CLI stops with a signature error instead of forwarding unverified
data. Run
listenagain.
Anonymous sandbox keys from sandbox create get 403 sandbox_key_restricted, as they do for
every webhook route.
trigger: send a test event
tickwire trigger --list
tickwire --sandbox trigger trade.outcome [--account <account-id>]
tickwire trigger webhook.test # the only type allowed on a live key
POST /v1/webhook-events/trigger publishes one event with the catalog’s example data plus
"test": true. The event goes to your real webhook endpoints and to running listen sessions.
A sandbox key can trigger any type in the catalog. A live key can trigger only webhook.test
(403 webhook_trigger_sandbox_only), so a fake trade never reaches a production handler. An
unknown type gets 400 unknown_event_type. The answer carries eventId, the id your endpoints
receive for the event (webhook-id), so a monitor can match a delivery to its trigger. It is null
when no active endpoint is subscribed to the type, and noDeliveryReason says why
(endpoint_disabled with disabledReasons, not_subscribed or no_endpoints). If webhook
delivery cannot queue the event you get 503 webhook_unavailable (running listen sessions still
got it). Triggers are idempotent (--idempotency-key) and audited.
logs tail: your request log
tickwire logs tail # the 20 most recent calls, oldest first
tickwire logs tail --status errors --since 1h -n 100 # failed calls of the last hour
tickwire logs tail -f --method POST --path /trade # keep following new calls; Ctrl-C stops
tickwire logs tail --status 4xx --bodies # with the redacted bodies of failed calls
logs tail reads request logs (GET /v1/request-logs): every
authenticated call your organisation’s keys made in the last 14 days. Each call is one line
(time, method, status, latency, path, error code, [aborted] when the answer was cut short, and
the request id); with --json, one JSON object per line, exactly as the API returned it.
| Option | Meaning |
|---|---|
-n, --limit N | How many recent calls to print first (default 20, at most 1,000). With --follow, 0 prints only calls made from now on |
-f, --follow | Keep polling (every 2 seconds, --interval) and print new calls as Tickwire records them |
--since, --until | ISO 8601 times (UTC when no zone is given), or relative: 30s, 15m, 2h, 7d. --until does not combine with --follow |
--status | 404, 4xx, 5xx, errors (400 and above) or aborted |
--method, --path, --error-code, --account, --key-id, --request-id | The API’s filters. --path (or --route) matches part of the path or of its template |
--bodies | Also fetch GET /v1/request-logs/{id} for failed calls and print their redacted requestBody and responseBody |
How it works:
- The list is newest first and paginated with an opaque cursor. The CLI passes
nextCursorback unchanged with the same filters, and prints the page oldest first, astaildoes. - With
--followit polls the first page and walks back until it reaches a call it has already printed, so each call is printed once, including one recorded late. If more than 2,000 calls arrive between two polls, it prints the newest and warns that older ones were skipped. - Bodies come only from the server. The list never includes excerpts. With
--bodies, the CLI prints only what the detail view returned, for failed calls (successful calls never keep a body). A credential that may not read bodies (anything but a full-access key with the writer role, not limited to some accounts or applications) getsexcerptsHidden: trueor403: the CLI says so once and prints metadata only. - Rate limits. On
429the CLI waits forRetry-Afterin full (or backs off 1, 2, 4… up to 60 seconds when there is none) and tries again. Timeouts and502/503/504are handled the same way. With--followit keeps trying; otherwise it gives up after 3 retries (exit 1). - Ctrl-C stops cleanly: no partial line, a short summary on stderr, exit code 130.