View as Markdown

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

OptionMeaning
--jsonJSON output on stdout. Notes and warnings still go to stderr
--sandboxUse the sandbox. It selects the sandbox profile and TICKWIRE_SANDBOX_URL
--profile NAMEUse a named credential profile. The default is default (sandbox with --sandbox)
--api-url URLAPI 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-verify to skip the check.
  • --sandbox accepts only twsk_ 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 the keyring package is installed and working. Otherwise the key goes in credentials.json with mode 0600. 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 mode 0700; an existing one is never chmod-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 to chmod 600 it, as ssh does). --no-keyring forces the file.
  • The config directory is $TICKWIRE_CONFIG_DIR, else $XDG_CONFIG_HOME/tickwire, else ~/.config/tickwire. config.json holds profiles (URL, sandbox flag, key hint) and never the key.
  • TICKWIRE_API_KEY in 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.

  • --reason is mandatory. It is sent as X-Reason and 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:
OutcomeMeaningExit code
appliedThe terminal confirmed it (evidence shows how)0
no_changeNothing needed doing, e.g. the position was already closed0
rejectedNot executed. The error code says why3
heldA pre-trade hook holds it for approval. Nothing has executed yet4
unknownIt may have executed5

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:

  1. POST /v1/webhook-listeners creates a listener and returns its secret and a cursor (the current head of your event buffer). The cursor is opaque (<boot id>:<seq>).
  2. 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.
  3. The CLI verifies both signatures and the timestamp. If an event fails verification, the CLI does not forward it and stops.
  4. It POSTs the exact signed bytes and headers to --forward-to and 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}/resend on 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 carries X-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 truncated and the CLI prints a warning. After a server restart the server sets reset, 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_limited with Retry-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 listen again.

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.

OptionMeaning
-n, --limit NHow many recent calls to print first (default 20, at most 1,000). With --follow, 0 prints only calls made from now on
-f, --followKeep polling (every 2 seconds, --interval) and print new calls as Tickwire records them
--since, --untilISO 8601 times (UTC when no zone is given), or relative: 30s, 15m, 2h, 7d. --until does not combine with --follow
--status404, 4xx, 5xx, errors (400 and above) or aborted
--method, --path, --error-code, --account, --key-id, --request-idThe API’s filters. --path (or --route) matches part of the path or of its template
--bodiesAlso 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 nextCursor back unchanged with the same filters, and prints the page oldest first, as tail does.
  • With --follow it 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) gets excerptsHidden: true or 403: the CLI says so once and prints metadata only.
  • Rate limits. On 429 the CLI waits for Retry-After in full (or backs off 1, 2, 4… up to 60 seconds when there is none) and tries again. Timeouts and 502/503/504 are handled the same way. With --follow it 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.