---
name: Cirrus API
description: Use when writing or reviewing code that calls the Cirrus trading API (api.cirrus.trade) - orders, portfolio, streaming, webhooks, instruments, auth and rate limits.
alwaysApply: false
---

# Cirrus API integration rules

Follow these rules when writing or reviewing code that calls the Cirrus trading API: place and manage orders across broker accounts, read the portfolio, stream live updates and receive signed webhooks. Human docs: https://app.cirrus.trade/docs.

## Essentials

- REST base URL: `https://api.cirrus.trade/v1`. WebSocket: `wss://api.cirrus.trade/v1/stream`.
- Machine-readable spec (OpenAPI 3.1): https://app.cirrus.trade/docs/openapi.json (the API also serves it at https://api.cirrus.trade/v1/openapi.json). Full reference as Markdown: https://app.cirrus.trade/llms-full.txt. Check endpoints, fields and enum values there; never invent them.
- Auth: send `Authorization: token <key_id>:<secret>` (`ck_…:cs_…`) on every request. Partner apps acting for other users send `Authorization: Bearer pa_…` (OAuth 2 authorization code + PKCE).
- Read the key from the `CIRRUS_API_KEY` environment variable. Fail fast with a clear message when it is missing.
- Every response is `{"status", "data", "error"}`. Branch on `error.code`; show `error.message` to people. Keep the `x-request-id` response header in logs.
- All JSON keys are snake_case, in requests and responses, including webhook and stream payloads. Enum values are upper case (`BUY`, `LIMIT`, `MIS`). Unknown body fields are rejected, not ignored: send only documented fields.
- A key only reaches what its scopes allow: `read`, `orders`, `triggers`. A missing scope answers 403 `forbidden`.

## Placing orders

- `POST /v1/orders` and `POST /v1/orders/bracket` require an `Idempotency-Key` header: a new UUID for each new order request. Reuse a key only to retry that same request with the same body; a different body with the same key fails with 422 `idempotency_key_reused`.
- Results are per account and per slice: a 200 can mix `accepted`, `rejected` and `unknown`. Check every entry of `results[]` and the `accepted` / `rejected` / `unknown` counts; never treat the call as all-or-nothing.
- `unknown` (and a timeout) means the broker may or may not have the order. Never retry blindly with a new key: retry with the same `Idempotency-Key`, or wait for the order on the stream or a webhook (match on `order_tag`).
- 409 `request_in_progress`: the first request with that key is still running; wait, then retry with the same key.
- Before trading, call `GET /v1/accounts` and use only accounts whose `status` is `ready`.
- Use `POST /v1/margins/orders` (same body, places nothing) to check margin first.

## Rate limits and retries

- Default budget: 20 requests a second per API key (and per partner app and user). Over it: 429 `rate_limited`; the message says how many ms to wait. Back off; do not hammer.
- Order actions (place, modify, cancel, GTT, convert) share 10 a second per broker account; bursts queue for up to 2 s.
- Portfolio reads come from snapshots and never spend the broker budget: prefer them to repeated broker calls.
- Signal URLs answer with `X-RateLimit-Limit`, `X-RateLimit-Remaining` and, on 429, `Retry-After`.
- 503 `service_unavailable`: nothing was done; retry with exponential backoff and jitter. Other 4xx errors are final: fix the request.

## Getting updates: stream, webhooks or polling

- Stream (`wss://api.cirrus.trade/v1/stream`, `read` scope): for long-running processes. Authenticate with the `Authorization` header or a first `{"type": "auth", "token": …}` message within 5 s; never put tokens in the URL. You get a snapshot per account and kind, `snapshot_done`, then `order_update` and `portfolio` events with full state (never diffs). Reconnect with backoff after any close except 4001; send `{"type": "resync"}` for fresh snapshots.
- Webhooks: for servers that should be called back (up to 5 HTTPS endpoints, managed on API access). Events: `order_update`, `trade`, `account_alert`, `positions`. Answer 2xx within 5 s, handle the work asynchronously, deduplicate on the event `id`, and expect events out of order (keep the order with the highest `filled_qty` / latest state).
- Polling: for scripts and cron jobs. `GET /v1/portfolio/{kind}` (orders, positions, holdings, trades, margins); `POST /v1/portfolio/refresh` asks for a fresh broker read. Accounts without data are listed in `missing[]`, never silently left out.

## Verifying webhooks

- Deliveries follow the Standard Webhooks specification (headers `webhook-id`, `webhook-timestamp`, `webhook-signature`); prefer an official Standard Webhooks library.
- Signature: `base64(HMAC-SHA256(key, "{webhook-id}.{webhook-timestamp}.{raw body}"))`, where `key` is the base64-decoded part of the secret after `whsec_`. `webhook-signature` holds one or more space-separated `v1,<base64>` entries (two during a secret rotation): accept when any matches.
- Verify the raw body before parsing JSON, compare in constant time, reject timestamps more than 5 minutes off, and ignore ids already handled.
- Keep the webhook secret in an environment variable, like the API key.

## Instruments

- Every order needs an `instrument_token`. Look it up with `GET /v1/instruments?q=…` (plus exact filters `exchange`, `instrument`, `expiry`, `strike`, `option_type`) or `GET /v1/instruments/{instrument_token}`.
- For many lookups, download `https://api.cirrus.trade/v1/instruments.csv` once a day after 08:40 IST (send `If-None-Match` with the saved `ETag` to get 304 while unchanged) and search locally.
- Never hardcode tokens for derivatives: contracts expire and new ones are listed daily.

## Partner apps (OAuth), only when acting for other users

- Flow: authorization code + PKCE `S256` only (no `plain`, no client credentials or implicit grant). Guide: https://app.cirrus.trade/docs/oauth.md.
- Send the user's browser to `https://app.cirrus.trade/oauth/authorize` with `client_id` (`cp_…`), `redirect_uri` (exactly as registered), `scope` (space separated: `read`, `orders`, `triggers`), a random `state`, `code_challenge` and `code_challenge_method=S256`. Keep the verifier and state server-side and check `state` on the callback; a denial comes back as `error=access_denied`.
- Exchange the `oc_…` code (single use, 60 s) at `POST https://api.cirrus.trade/v1/oauth/token` with `grant_type=authorization_code`, `code`, `redirect_uri`, `code_verifier` and HTTP Basic client credentials, form-encoded. Replies are plain RFC 6749 JSON (`access_token`, `token_type`, `expires_in`, `refresh_token`, `scope`), not the envelope.
- Access tokens `pa_…` last 1 day: `Authorization: Bearer pa_…`. Refresh tokens `pr_…` last 90 days and are single use: save the new one from every refresh. A repeat within 30 s returns the same pair; reusing one later revokes the user's whole connection. `invalid_grant` on refresh means the user must connect again.
- The client secret and all tokens stay on the server, never in a browser or mobile app, never in logs.

## Safety rules for the agent

- Never hardcode, print, log or commit the API key or webhook secrets. Read them from the environment (`CIRRUS_API_KEY`); keep `.env` files out of version control.
- Never place, modify or cancel live orders, and never run code that would, without the user's explicit confirmation for that specific action. Default to read-only calls while exploring; ask before switching to a key with the `orders` scope.
- Prefer a key with only the scopes the task needs. Test order code with the smallest quantity or a paper account first.
- If a response is `unknown` or the network fails mid-order, stop and report it; do not loop retries.
