# 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.

Source: https://tickwire.io/docs/cli/

`tickwire` is a command-line tool built on the [Python SDK](https://tickwire.io/docs/sdk-python/).
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`.

```sh
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](https://tickwire.io/docs/sdk-python/); until then, `pip install tickwire-cli` won't find it. If you want
to use the CLI before then, [contact us](https://tickwire.io/contact/).

## Two minutes in the sandbox

```sh
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`

```sh
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`

```sh
tickwire accounts list
tickwire accounts get <account-id> --health     # adds health: status, reasons, checked_at
```

## `trade`

```sh
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](https://tickwire.io/docs/webhooks/) 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:

| 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](https://tickwire.io/docs/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

```sh
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](https://tickwire.io/docs/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

```sh
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

```sh
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](https://tickwire.io/docs/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 `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.
