# Authentication

> The header forms Tickwire accepts, token kinds, and how to narrow a token's reach.

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

Every request carries a key or token. We accept each of the forms the APIs we're compatible with
use, so existing clients work unchanged:

| Form | Example | Used by |
|---|---|---|
| `X-Tickwire-Agent-Key` | `X-Tickwire-Agent-Key: <key>` | AI agents (`/v1/agent/…` execution routes) |
| `Authorization: Bearer` | `Authorization: Bearer <key>` | Generic HTTP clients and MCP server tools |
| `X-API-Key` header | `X-API-Key: <key>` | The `/v1` API and terminal routes |
| `auth-token` header | `auth-token: <token>` | The `/users/current/…` REST API |
| Query parameter | `?auth-token=<token>` or `?api_key=<key>` | WebSocket and socket.io connections only, for clients that can't set headers (browsers) |

Prefer headers. Query parameters end up in logs more easily; we redact them from ours.

## WebSockets: header or query parameter

The WebSocket and socket.io upgrade accepts the same headers as REST: `auth-token`,
`X-API-Key` or `Authorization: Bearer`. A key in a header stays out of URLs, so any proxy or tool
between you and us never logs it. Use a header whenever your client can set one:

```python
import websockets

async with websockets.connect(
    "wss://api.tickwire.io/users/current/accounts/acc_7Qx/ws",
    additional_headers={"auth-token": TW_KEY},
) as ws:
    ...
```

```js
// Node.js (the ws package)
const ws = new WebSocket("wss://api.tickwire.io/users/current/accounts/acc_7Qx/ws", {
  headers: { "auth-token": process.env.TW_KEY },
});
```

Browsers can't set headers on a WebSocket, so in a browser the query parameter is the only way,
and it stays supported. There, use a [scoped token](#token-kinds) with a short time-to-live and
only the accounts the page needs, never a full API key. We replace the value of `auth-token`,
`api_key` and similar parameters with `REDACTED` in every log we write, our servers' access logs
included.

## Token kinds

Tickwire separates developer credentials based on whether orders are placed directly by code or guided by an AI assistant:

### 1. API keys (`tw_live_` and `tw_ro_`)

Created under **API keys** in the dashboard.
- **Full keys (`tw_live_`)**: For custom scripts, bot integrations, prop firm workflows, and direct trading. Can trade, manage accounts, and read.
- **Read-only keys (`tw_ro_`)**: Can query balances, quotes, positions, and history, but cannot trade or change settings.
- **Headers to send**: `Authorization: Bearer <key>` or `X-API-Key: <key>`.
- **Target routes**: `/users/current/…` and `/v1/…`.

### 2. Agent keys (`twag_`)

Created under **AI agents > Keys** in the dashboard.
- Built specifically for AI assistants (Google Antigravity, Claude, ChatGPT, Cursor, Windsurf).
- **Guarded execution**: Each key is linked to an instant kill switch and evaluated by your pre-trade decision hook before orders reach the broker.
- **Headers to send**: In MCP clients, send `Authorization: Bearer <key>`. For direct HTTP calls, send `X-Tickwire-Agent-Key: <key>`.
- **Target routes**: `/v1/agent/…` routes and the MCP server. Direct terminal routes reject agent keys with `401 invalid_token` to protect you from ungated agent orders.

### 3. Scoped tokens

Narrowed from an API key for browser pages or frontends: limited to certain accounts, with a time-to-live and optional IP allowlist.
A token with an allowlist is checked against your real public address on every request and
WebSocket, whatever `X-Forwarded-For` says. From anywhere else it gets `403`
[`ip_not_allowed`](https://tickwire.io/docs/errors/ip_not_allowed/); the answer never shows the allowlist.

Every key and token value is shown **once**, in the answer that creates it (or rotates a read-only
key). We keep only a SHA-256 digest of each key (full `tw_live_`, read-only `tw_ro_` and sandbox
`twsk_` keys, like API tokens and agent keys), so nobody can show it again, us included. Lists and
reads show only a short preview such as `tw_live_ab12…`, and asking the dashboard to reveal a key
answers `400` [`not_revealable`](https://tickwire.io/docs/errors/not_revealable/). Store the value when you create it;
if you lose it, rotate or revoke the key and create a new one.

Keys issued before 2026-10-03 start with `fxs_` and keep working.

## Testing your key: Mac, Linux & Windows

### Testing an API key (`tw_live_` or `tw_ro_`)

**macOS / Linux (Terminal):**
```bash
curl -s -H "Authorization: Bearer <YOUR_API_KEY>" \
  https://api.tickwire.io/users/current/accounts
```

**Windows (PowerShell):**
```powershell
curl.exe -s -H "Authorization: Bearer <YOUR_API_KEY>" `
  https://api.tickwire.io/users/current/accounts
```

### Testing an Agent key (`twag_`)

**macOS / Linux (Terminal):**
```bash
curl -s -H "X-Tickwire-Agent-Key: <YOUR_AGENT_KEY>" \
  https://api.tickwire.io/v1/agent/accounts
```

**Windows (PowerShell):**
```powershell
curl.exe -s -H "X-Tickwire-Agent-Key: <YOUR_AGENT_KEY>" `
  https://api.tickwire.io/v1/agent/accounts
```

## Rotation doesn't restart anything

Creating, rotating or revoking a key never restarts a terminal or drops a broker connection.

## Mistakes and what you'll see

| Situations | Status | `error` |
|---|---|---|
| No key | 401 | [`missing_token`](https://tickwire.io/docs/errors/missing_token/) |
| Unknown, expired or revoked key | 401 | [`invalid_token`](https://tickwire.io/docs/errors/invalid_token/) |
| Agent key sent to direct terminal route | 401 | [`invalid_token`](https://tickwire.io/docs/errors/invalid_token/) (send `X-Tickwire-Agent-Key` to `/v1/agent/…` instead) |
| Key not scoped to this account | 403 | [`account_forbidden`](https://tickwire.io/docs/errors/account_forbidden/) |
| Read-only API key on a mutation | 403 | [`read_only_token`](https://tickwire.io/docs/errors/read_only_token/) |
| Read-only Agent key on trade | 403 | `read_only_key` (uncheck Read-only in dashboard > AI agents > Keys to trade) |
| Request from outside the IP allowlist | 403 | [`ip_not_allowed`](https://tickwire.io/docs/errors/ip_not_allowed/) |

On the `/users/current/…` routes, `error` holds the class name (`UnauthorizedError` for 401,
`ForbiddenError` for 403) and the code above is in `code`.
