Skip to content
Cirrus home
Sign in

Cirrus API · v1

API Reference

Place and manage orders across your broker accounts, read your portfolio and stream live updates from your own code, over plain HTTPS and JSON.

Overview#

The Cirrus order API is plain HTTPS + JSON under /v1, plus one WebSocket for live order and portfolio updates. It places orders directly with your broker, across one or many accounts.

REST base
https://api.cirrus.trade/v1
Stream
wss://api.cirrus.trade/v1/stream
  • Every response uses one envelope: status, data, error (see Errors). The OAuth token endpoint follows RFC 6749 instead.
  • Every response carries an x-request-id header. Send your own (1-64 letters, digits, - or _) to correlate logs; quote it when reporting a problem.
  • Order sides, types and products are upper case (BUY, LIMIT, MIS). Unknown body fields are rejected, not ignored.

Using your API key#

  1. Create a key on API access (sign in). Pick only the scopes you need, and optionally the IPs it may be used from.
  2. Copy the key and secret when shown: the secret is not shown again.
  3. Send them on every request as shown. Examples on this page read the same variable.
Quick start
export CIRRUS_API_KEY='ck_xxxxxxxx:cs_xxxxxxxx'

curl "https://api.cirrus.trade/v1/portfolio/positions" \
  -H "Authorization: token $CIRRUS_API_KEY"

Authentication#

Two credentials work on the public API: your own API keys, and partner OAuth tokens for apps acting for other users.

API keys#

  • Send Authorization: token ck_…:cs_… (key id, colon, secret). Keys are created and revoked on API access (sign in), only by you, signed in yourself.
  • The secret is shown once and stored only as a hash. Lost it? Create a new key and revoke the old one.
  • Optional IP allowlist (up to 20 addresses): from any other IP the key gets 403.
  • Revoking a key takes effect within about 20 seconds, open streams included.

Partner apps (OAuth)#

  • Partner apps use OAuth 2 authorization code + PKCE (S256): the user approves your app on a consent screen, you exchange the code for a pa_… access token (1 day) and a rotating pr_… refresh token (90 days).
  • Call the API with Authorization: Bearer pa_…. The full guide, with examples, is in OAuth for partner apps.

Read OAuth for partner apps

API key
curl "https://api.cirrus.trade/v1/user/profile" \
  -H "Authorization: token $CIRRUS_API_KEY"
Partner app (OAuth access token)
curl "https://api.cirrus.trade/v1/user/profile" \
  -H "Authorization: Bearer pa_xxxx"

OAuth for partner apps#

Build an app that other Cirrus users connect to their own accounts. Each user approves your app once; you get tokens that act for that user within the scopes they granted, and never see their password or API keys.

  • For a product serving many users (a trading tool, journal, analytics or copy-trading service): each user connects their own account through a consent screen.
  • For your own scripts and bots, use an API key instead (see Authentication): no redirect, no consent screen, no token refresh.
  • The only grant is authorization code with PKCE (RFC 6749 + RFC 7636, S256). There is no client credentials, implicit or password grant: every token acts for one user who approved it.

Register your app#

  • Register on API access in the app (signed in, Partner apps), or with POST /v1/partner-apps from your own signed-in app session. API keys and partner tokens cannot manage apps (403); an impersonated session cannot register, rotate, disable or disconnect.
  • The reply carries client_id (cp_ + 20 characters) and client_secret (cps_ + 48 characters). The secret is shown once and stored only as a keyed hash: store it in your server’s secret store right away. Lost or leaked? Rotate it (below): the new secret is shown once and the old one keeps working for 24 hours.
  • At most 10 active apps per user. Apps cannot be edited: to change the name, redirect URIs or scopes, register a new app (and disable the old one).
  • New apps are not verified. Verification is set by the platform operator only; the consent screen shows the app as Verified or Not verified, and warns users about unverified apps.
  • Rotate the secret with Rotate secret on API access or POST /v1/partner-apps/{client_id}/rotate-secret. For 24 hours both secrets authenticate, so update every server before the old one stops; connected users are not affected.
  • Disabling (DELETE /v1/partner-apps/{client_id}, or Disable on API access) is permanent: every token of every user of the app stops working within about 5 seconds, and the token endpoint answers invalid_client.

POST /v1/partner-apps body (JSON)

  • name string required

    1-80 characters. Shown to users on the consent screen.

  • redirect_uris string[] required

    1-10 URIs, matched exactly (no prefixes, no wildcards). https://, or http://localhost / http://127.0.0.1 (any port) for development. No #fragment, no whitespace, at most 512 characters each.

  • scopes string[] required

    At least one of read, orders, triggers: the most a user can grant your app. Ask for less at authorize time when you can.

  • logo_url string optional

    https:// image shown on the consent screen.

Registered · data
{
  "client_id": "cp_8fK2mQ7xL0aZ4tR9wY1b",
  "owner": "you@example.com",
  "name": "Trade Journal",
  "verified": false,
  "logo_url": null,
  "redirect_uris": ["https://partner.example/callback"],
  "scopes": ["read", "orders"],
  "created_at": "2026-09-28T09:00:00Z",
  "disabled": false,
  "client_secret": "cps_…",
  "note": "Store the client secret now: it is not shown again."
}
App-session routes (never with an API key or partner token)
RouteWhoWhat
GET /v1/partner-appsapp ownerYour apps, disabled ones included.
POST /v1/partner-appsapp ownerRegister an app. 201 with client_secret, shown once.
POST /v1/partner-apps/{client_id}/rotate-secretapp ownerNew client_secret, shown once. The old secret keeps working for 24 hours (previous_secret_expires_at).
DELETE /v1/partner-apps/{client_id}app ownerDisable the app for good; 404 unknown_app when it is not an active app of yours.
GET /v1/connectionsuserApps connected to your account: client_id, app_name, verified, scopes, connected_at, updated_at.
DELETE /v1/connections/{client_id}userDisconnect an app; 404 connection_not_found when there is no live connection.
GET /v1/oauth/consent · POST /v1/oauth/authorizeconsent screenUsed by the consent screen itself; partners never call them.

The flow#

Authorization code + PKCE
User's browser         Your server              Consent screen             API
      |                      |                        |                          |
      | 1. "Connect"         |                        |                          |
      |--------------------->|                        |                          |
      |                      | make verifier, state;  |                          |
      |                      | keep them server-side  |                          |
      | 2. 302 to /oauth/authorize?client_id&redirect_uri&scope&state&code_challenge
      |<---------------------|                        |                          |
      | 3. sign in, review the consent screen, Allow  |                          |
      |---------------------------------------------->| POST /v1/oauth/authorize |
      |                      |                        |------------------------->|
      | 4. 302 to redirect_uri?code=oc_…&state=…      |<-------------------------|
      |<----------------------------------------------|                          |
      |--------------------->| 5. POST /v1/oauth/token (code, code_verifier,     |
      |                      |    client_id + client_secret)                     |
      |                      |-------------------------------------------------->|
      |                      | 6. access_token pa_… (1 day), refresh_token pr_…  |
      |                      |<--------------------------------------------------|
      |                      | 7. API calls: Authorization: Bearer pa_…          |
      |                      |-------------------------------------------------->|
  1. Create a PKCE code_verifier (43-128 characters from A-Z a-z 0-9 - . _ ~) and a random state. Keep both in the user’s server-side session.
  2. Redirect the browser to the consent screen with the parameters below and code_challenge = BASE64URL(SHA256(code_verifier)), no padding.
  3. The user signs in if needed (they come back to the consent screen afterwards), sees your app’s name, logo, verified badge and the scopes you asked for, and chooses Allow or Deny.
  4. On Allow the browser returns to your redirect_uri with code and your state. Check state against the session before anything else.
  5. Within 60 seconds, exchange the code on your server for tokens at POST /v1/oauth/token, sending the verifier and your client credentials.
  6. Call the API with the access token. Refresh it before or when it expires (1 day); ask the user to connect again if the refresh is refused.

Send the user to the consent screen#

GET https://app.cirrus.trade/oauth/authorize in the user’s browser, with these query parameters:

Query parameters

  • client_id string required

    Your app’s cp_… id. Unknown or disabled apps are refused on the screen.

  • redirect_uri string required

    One of the app’s registered URIs, character for character. Anything else is refused on the screen and never redirected to.

  • scope string required

    Space separated (read orders), lower case. Each must be one the app registered; unknown scopes and scopes the app may not request are refused (invalid_scope). Repeats are ignored.

  • code_challenge string required

    BASE64URL(SHA256(code_verifier)) without = padding: exactly 43 characters of A-Z a-z 0-9 - _.

  • code_challenge_method S256 required

    Only S256. plain is refused.

  • state string optional

    Returned unchanged (percent-encoded) with the code or the denial. Optional for the server, but always send a fresh random value and check it: it is your CSRF protection.

  • response_type code optional

    Not needed: the code flow is the only one. OAuth libraries that send response_type=code work.

Consent URL
https://app.cirrus.trade/oauth/authorize?client_id=cp_xxxx&redirect_uri=https%3A%2F%2Fpartner.example%2Fcallback&scope=read%20orders&state=<random>&code_challenge=<BASE64URL(SHA256(verifier))>&code_challenge_method=S256
import base64, hashlib, secrets
from urllib.parse import urlencode

CLIENT_ID = "cp_xxxx"
REDIRECT_URI = "https://partner.example/callback"

def start_connect(session: dict) -> str:
    """Returns the consent URL; keeps verifier and state in the session."""
    verifier = secrets.token_urlsafe(64)  # 86 chars from A-Z a-z 0-9 - _
    digest = hashlib.sha256(verifier.encode("ascii")).digest()
    challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
    session["pkce_verifier"] = verifier
    session["oauth_state"] = secrets.token_urlsafe(32)
    return "https://app.cirrus.trade/oauth/authorize?" + urlencode({
        "client_id": CLIENT_ID,
        "redirect_uri": REDIRECT_URI,
        "scope": "read orders",
        "state": session["oauth_state"],
        "code_challenge": challenge,
        "code_challenge_method": "S256",
    })
Scopes (the same as for API keys)
ScopeConsent screenAllows
readPortfolio, orders, triggers, GTT and the live streamGET /v1/accounts, GET /v1/instruments (and .csv), GET /v1/portfolio/*, POST /v1/portfolio/refresh, /v1/stream, GET /v1/gtt, GET /v1/triggers, POST /v1/margins/orders
ordersPlace, modify, cancel and convert orders and GTTPOST / PATCH / DELETE /v1/orders and /v1/orders/bracket, POST / PATCH / DELETE /v1/gtt, POST /v1/positions/convert
triggersCreate, change and remove stop-loss / target triggersPOST / PATCH / DELETE /v1/triggers
  • Problems with the request (missing parameter, unknown or disabled app, unregistered redirect_uri, bad scope, bad PKCE) are shown to the user on the consent screen. They are never sent to your redirect_uri, because it is not trusted until it matched.
  • Scopes do not imply each other: ask for read as well as orders if you need to see what you placed.
  • Connecting again (same user, same app) replaces the connection’s scopes with the new ones once the new code is exchanged. Tokens issued before keep the scopes they were issued with.

Handle the callback#

Back at your redirect_uri
QueryWhenMeaning
codeAllowoc_…, single use, valid 60 s, bound to your client_id, this redirect_uri, the scopes and the challenge. Added with & when your URI already has a query.
stateAllow, DenyYour value, when you sent one.
error=access_deniedDenyThe user refused. It is the only error sent to your redirect_uri, without error_description.
Callbacks
https://partner.example/callback?code=oc_xxxx&state=<your state>
https://partner.example/callback?error=access_denied&state=<your state>

Exchange the code for tokens#

  • POST /v1/oauth/token on the API host, from your server only. Body form-encoded (application/x-www-form-urlencoded, as RFC 6749 says) or JSON (application/json).
  • Client authentication: HTTP Basic (Authorization: Basic base64(client_id:client_secret)), or client_id and client_secret in the body. Basic wins when both are sent.
  • Replies are plain RFC 6749 JSON, not the usual status / data / error envelope, with Cache-Control: no-store.
  • grant_type is authorization_code or refresh_token; anything else is unsupported_grant_type.
  • Too many failed attempts from one IP (wrong secrets, bad codes): 429 with a Retry-After header, in seconds. Wait that long before trying again.

grant_type=authorization_code

  • grant_type authorization_code required

    Exchange a code.

  • code string required

    The oc_… code from the callback. Used up by the first attempt, even a failed one.

  • redirect_uri string required

    Exactly the redirect_uri of the authorize request.

  • code_verifier string required

    The verifier behind code_challenge.

  • client_id string optional

    When not using HTTP Basic.

  • client_secret string optional

    When not using HTTP Basic.

curl -X POST https://api.cirrus.trade/v1/oauth/token \
  -u "$PARTNER_CLIENT_ID:$PARTNER_CLIENT_SECRET" \
  -d grant_type=authorization_code \
  -d code=oc_xxxx \
  --data-urlencode redirect_uri=https://partner.example/callback \
  -d code_verifier=<verifier from the session>

Token response

  • access_token string required

    pa_… bearer token for the API.

  • token_type "Bearer" required

    Always Bearer.

  • expires_in integer required

    Seconds the access token is valid: 86400 (1 day).

  • refresh_token string required

    pr_…, valid 90 days from issue, single use. Every reply carries a new one.

  • scope string required

    Granted scopes, space separated (read orders).

Token response
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store

{
  "access_token": "pa_…",
  "token_type": "Bearer",
  "expires_in": 86400,
  "refresh_token": "pr_…",
  "scope": "read orders"
}
Lifetimes
CredentialLifetimeNotes
oc_… code60 sSingle use; the first exchange attempt uses it up.
pa_… access token1 dayStays valid after a refresh until it expires, unless the connection is revoked.
pr_… refresh token90 daysSingle use (a repeat within 30 s gets the same reply); each refresh returns a new 90-day one, so an app in use stays connected.

Refresh tokens#

  • Refresh tokens rotate: store the new refresh_token from every reply before using the new access token.
  • Grace window: presenting the same refresh token again within 30 seconds of its first use (a retry after a lost reply, or two workers at once) returns the same new token pair, not an error.
  • Reusing a refresh token later than that revokes the whole connection (all its tokens): it is treated as stolen.
  • A refused refresh (invalid_grant: expired, used, or the user disconnected) means the user has to connect again through the consent screen.

grant_type=refresh_token

  • grant_type refresh_token required

    Get a new token pair.

  • refresh_token string required

    The latest pr_… you received.

  • client_id string optional

    When not using HTTP Basic.

  • client_secret string optional

    When not using HTTP Basic.

curl -X POST https://api.cirrus.trade/v1/oauth/token \
  -u "$PARTNER_CLIENT_ID:$PARTNER_CLIENT_SECRET" \
  -d grant_type=refresh_token \
  -d refresh_token=pr_xxxx

Token errors#

Token endpoint errors ({"error", "error_description"})
errorHTTPWhen
invalid_request400Unreadable body, or code, redirect_uri, code_verifier or refresh_token missing.
unsupported_grant_type400grant_type is neither authorization_code nor refresh_token.
invalid_client401No client credentials, unknown or disabled app, or wrong secret. With HTTP Basic the reply carries a WWW-Authenticate: Basic header.
invalid_grant400Code invalid, expired or already used; code issued to another app or redirect_uri; code_verifier does not match; refresh token invalid, reused after the 30 s grace (the connection is then revoked) or expired; the user disconnected the app.
temporarily_unavailable503A store is briefly down, or partner access is off on this server. Nothing was issued; retry.
Error response
HTTP/1.1 400 Bad Request
Content-Type: application/json
Cache-Control: no-store

{
  "error": "invalid_grant",
  "error_description": "code_verifier does not match"
}

Call the API#

  • Send Authorization: Bearer pa_… on every request. The stream accepts the same token (header on the upgrade, or {"type": "auth", "token": "pa_…"}).
  • A partner token reaches exactly what its scopes allow (see Scopes) for the one user who connected it. App-session routes (API keys, partner apps, connections, webhooks, signal URLs, shared data) answer 403 forbidden. GET /v1/user/profile works with any scope and shows auth.kind: "partner", your client_id and app name.
  • Rate limit: 20 requests a second per app and user by default, counted separately for each connected user; over it, 429 rate_limited with the wait in the message. Order actions also share the per-broker-account budget.
  • An expired, revoked or unknown token answers 401 unauthorized. Repeated failures from one IP get 429 for a minute.
GET Read positions as the user
curl "https://api.cirrus.trade/v1/portfolio/positions" \
  -H "Authorization: Bearer pa_xxxx"

Connections and revocation#

Users see every connected app on API access (or GET /v1/connections) and can disconnect any of them at any time.

What ends a connection
EventTokensThen
User disconnectsall deadOn API access or DELETE /v1/connections/{client_id}: every access token stops within about 5 s (open streams close within 10 s) and refresh tokens are deleted. Send the user through consent again.
Refresh token reused after 30 sall deadSame as a disconnect.
Password change or log out everywhereall deadEvery app the user connected, as if each were disconnected. The user connects again.
App disabledall deadFor every user of the app, within about 5 s; the token endpoint answers invalid_client.
Access token expiresthat oneAfter 1 day. Refresh.
Refresh token expiresno new ones90 days after it was issued if never used. Connect again.

Security checklist#

  • Keep client_secret, access and refresh tokens on your server (secret store, encrypted at rest). Never ship the secret in a browser or mobile app, never log tokens.
  • Always send a fresh random state and compare it (constant time) before exchanging the code; drop the callback when it does not match.
  • PKCE is mandatory: make a new verifier per authorization, keep it server-side, send it only to the token endpoint.
  • Register exact redirect URIs over HTTPS; keep http://localhost ones in a separate development app.
  • Ask for the fewest scopes; the user sees them. Place orders only on the user’s explicit instruction.
  • Save the new refresh token from every reply; only a repeat within 30 seconds is forgiven, later reuse disconnects the user.
  • Rotate the client secret if it may have leaked, and remove the old one from your servers within the 24-hour overlap.

POST /v1/oauth/token #

no auth

The partner’s server-to-server token endpoint: exchange an authorization code, or rotate a refresh token. The client authenticates with client_id + client_secret in the form body or with HTTP Basic.

Headers

  • Authorization string optional

    Optional HTTP Basic client authentication: Basic base64(client_id:client_secret).

Body (JSON)

  • grant_type string required

    authorization_code or refresh_token; missing or anything else is unsupported_grant_type.

    Values:
    • authorization_code
    • refresh_token
  • code string optional nullable

    authorization_code: the code from the redirect (oc_...).

  • redirect_uri string optional nullable

    authorization_code: the same redirect_uri the code was issued for.

  • code_verifier string optional nullable

    authorization_code: the PKCE verifier (43-128 characters of A-Z a-z 0-9 - . _ ~) whose S256 challenge was sent to authorize.

  • refresh_token string optional nullable

    refresh_token: the latest refresh token (pr_...).

  • client_id string optional nullable

    The app's client id (unless sent with HTTP Basic).

  • client_secret string optional nullable

    The app's client secret (unless sent with HTTP Basic).

Response data · 200

  • access_token string required

    Access token (pa_...); send it as Authorization: Bearer <access_token>.

  • token_type string required

    Always Bearer.

  • expires_in integer (int64) required

    Seconds until the access token expires (86400: one day).

  • refresh_token string required

    Refresh token (pr_...), valid for 90 days and single use: every refresh returns a new one. Presenting a used refresh token again revokes the whole connection.

  • scope string required

    Granted scopes, space separated (OAuth convention), e.g. read orders.

Errors

HTTPCodeWhen
400invalid_request

Unreadable body or a missing field (code, redirect_uri, code_verifier, refresh_token).

400unsupported_grant_type

grant_type missing or not authorization_code / refresh_token.

400invalid_grant

The code is invalid, expired or already used, was issued to another client or redirect_uri, or code_verifier does not match; the refresh token is invalid, already used, expired, or the user revoked the connection.

401invalid_client

No client credentials, an unknown or disabled app, or a wrong secret.

429slow_down

Too many failed attempts from this source; retry after Retry-After seconds.

503temporarily_unavailable

Partner access is off on this server or its store is temporarily unavailable; retry.

  • Send the body form-encoded (application/x-www-form-urlencoded, as RFC 6749 specifies) or as JSON. Replies are not the REST envelope: errors are {"error", "error_description"}.
POST
curl -X POST "https://api.cirrus.trade/v1/oauth/token" \
  -H "Content-Type: application/json" \
  -d '{
    "grant_type": "authorization_code",
    "code": "oc_3zb5bRr1InWe3ipR7EXinezj5kpxNWDpk9piRJNB",
    "redirect_uri": "https://partner.example/callback",
    "code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk",
    "client_id": "cp_3Oq3YXcuepv3Qx5Z2iC2",
    "client_secret": "cps_dG69y0yLbVBxwXqsWbWbrQk1aP0sEuT7mYw2HcN4vZ8LxJfR"
  }'
{
  "access_token": "pa_pyoxfMeCpLKlvksr5Io8XkG7z5ZaKnLz3QEF9m2nuyECcwr2",
  "token_type": "Bearer",
  "expires_in": 86400,
  "refresh_token": "pr_GWiBQeI7nOqJt9PoEovV8T2Nr82NA1jXAJJVHzUG9CFvpK6heN2TmZ76",
  "scope": "read orders"
}

Scopes#

A key or partner token can call only what its scopes allow; scopes do not imply each other (orders does not include read). Your own signed-in app session can call everything.

  • scope read

    GET /v1/accounts, GET /v1/instruments (and .csv), GET /v1/portfolio/*, POST /v1/portfolio/refresh, /v1/stream, GET /v1/gtt, GET /v1/triggers, POST /v1/margins/orders

  • scope orders

    POST / PATCH / DELETE /v1/orders and /v1/orders/bracket, POST / PATCH / DELETE /v1/gtt, POST /v1/positions/convert

  • scope triggers

    POST / PATCH / DELETE /v1/triggers

  • app session

    Everything else: API keys, partner apps, OAuth consent, connections, webhooks, signal URLs, shared data. Never reachable with a key or partner token. GET /v1/user/profile needs no scope.

A missing scope answers 403 forbidden with the scope named in the message.

Errors & idempotency#

Success is {"status": "success", "data": …, "error": null}. Errors keep the same shape: branch on error.code, show error.message to people.

Common error codes
CodeHTTPMeaning
malformed_json400Body is not valid JSON for this route.
invalid_request422Validation failed; field names the input, e.g. orders[0].price.
unauthorized401Missing, invalid or expired credential.
forbidden403Credential lacks the scope, key used from an IP outside its allowlist, or app-only.
oms_not_enabled403Your account is not served by this API yet.
account_not_found404Account id is not one of yours.
idempotency_key_required400Idempotency-Key header missing.
invalid_idempotency_key400Not 1-128 printable characters.
request_in_progress409The first request with this key is still running.
idempotency_key_reused422Key already used with a different body.
rate_limited429Rate limit reached; message says when to retry.
service_unavailable503A dependency is down; nothing was done. Retry.

Idempotency-Key#

  • Required on POST /v1/orders and POST /v1/orders/bracket: 1-128 printable characters, a new UUID per request. Keys are per user.
  • A retry with the same key and the same body returns the original response unchanged (for 24 hours), with the header idempotent-replayed: true. Nothing is placed twice.
  • While the first request is still running a retry gets 409 request_in_progress; the same key with a different body gets 422 idempotency_key_reused.
  • After a timeout or a slice with status unknown, retry with the same key, never a new one.
Error envelope
{
  "status": "error",
  "data": null,
  "error": {
    "code": "invalid_request",
    "message": "price must be > 0 for LIMIT orders",
    "field": "orders[0].price"
  }
}

Rate limits#

Defaults shown; a deployment can change them.

  • Per API key: 20 requests a second by default. Over it, 429 rate_limited at once; the message says how many ms to wait.
  • Per partner app and user: the same budget, counted separately for each user who connected the app.
  • Failed credentials: after about 30 failures in a minute from one IP, that IP gets 429 until the minute is up.
  • Per broker account: orders, modifies, cancels, GTT and convert share one budget (10 a second by default). A burst waits up to 2 s for a slot instead of failing.
  • Portfolio reads come from snapshots and never spend the broker budget.
  • Signal webhook URLs each have their own limit (429 over it).

Build with AI#

Coding agents write better integrations when they read the reference instead of guessing. Give yours the machine-readable docs and a rules file for your tool.

  1. Give the agent the reference: llms-full.txt for reading, openapi.json for exact schemas. Every section of these docs also has a Copy as Markdown button.
  2. Add the rules file for your tool (below) to your repository. Every file carries the same rules; only the file name and header differ.
  3. Keep your API key in an environment variable, and make the agent ask you before it places, modifies or cancels a live order.
  • llms.txt Index of the docs as Markdown pages, in the llmstxt.org format.
  • llms-full.txt The whole reference in one Markdown file.
  • openapi.json OpenAPI 3.1 spec: every endpoint, schema, enum value, stream message and webhook body. Live from the API: https://api.cirrus.trade/v1/openapi.json.

Rules files for your agent#

Same rules for every tool, filled in for Cirrus. Pick yours, then copy or download the file to the path shown. Download all as .zip

Save CLAUDE.md in the repository root (or append it to yours); it loads every session. Or install the skill instead: Claude loads it only when a task touches the API.

CLAUDE.md Download
CLAUDE.md
# 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.
.claude/skills/cirrus-api/SKILL.md Download
SKILL.md
---
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.
---

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

File conventions: Claude Code docs

Starter prompt
Read https://app.cirrus.trade/llms-full.txt and the OpenAPI spec at
https://app.cirrus.trade/docs/openapi.json, and follow the Cirrus API rules in this repository.
Write a script that lists my accounts and prints open positions.
Read the API key from $CIRRUS_API_KEY. Do not place any orders.

OpenAPI spec#

An OpenAPI 3.1 description of every route, enum, error code, stream message and webhook body. Use it to generate clients or to give a coding agent exact schemas. These docs also serve a copy at `/docs/openapi.json` with this API’s host filled in.

GET /v1/openapi.json #

no auth

The OpenAPI document. No credential needed. Cached for five minutes; send If-None-Match with the last ETag to revalidate.

Headers

  • If-None-Match string optional

    An ETag from an earlier response.

Response data · 200

No response body.

GET
curl "https://api.cirrus.trade/v1/openapi.json"

Accounts#

Your linked broker accounts: whether each can trade right now, what its setup lacks and how order updates arrive. Credentials are never returned.

status (first match wins)
ValueTradingWhen
unsupportedno tradingThis broker cannot trade here yet.
setup_incompleteno tradingA required field is missing; see missing.
login_requiredno tradingThe broker refused the login and none succeeded since, or there is no login from today (every broker except Paper).
readytradingCan trade.
updates: how order updates arrive now
ValueSpeedWhen
streamliveUpdates arrive on the broker socket.
postbackliveThe broker posts each update to us.
pollingevery few sThe order book is read periodically: the socket is down or was refused, or a postback broker lacks the API secret needed to verify postbacks.

Poll GET /v1/accounts before trading, or watch account_alert events on a webhook: a login that expires or live updates that stop are reported there as they happen.

GET /v1/accounts #

scope read

Every account of yours, sorted by account id.

Response data · 200

  • items object[] required

Errors

HTTPCodeWhen
401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

  • missing lists labels only, never values. required: true blocks trading; required: false turns one feature off (for example postbacks).
GET
curl "https://api.cirrus.trade/v1/accounts" \
  -H "Authorization: token $CIRRUS_API_KEY"
{
  "status": "success",
  "data": {
    "items": [
      {
        "account": "5P1",
        "broker": "5paisa",
        "broker_name": "5paisa",
        "tag": null,
        "multiplier": 1,
        "supported": true,
        "status": "login_required",
        "missing": [
          {
            "field": "static_ip",
            "label": "Static IP",
            "required": false
          }
        ],
        "updates": "stream",
        "static_ip": null,
        "last_login_at": null,
        "session_expires_at": null,
        "seat_assigned": null,
        "last_update_at": null
      },
      {
        "account": "MO1",
        "broker": "motilaloswal",
        "broker_name": "Motilal Oswal",
        "tag": null,
        "multiplier": 1,
        "supported": true,
        "status": "login_required",
        "missing": [
          {
            "field": "static_ip",
            "label": "Static IP",
            "required": false
          }
        ],
        "updates": "stream",
        "static_ip": null,
        "last_login_at": null,
        "session_expires_at": null,
        "seat_assigned": null,
        "last_update_at": null
      },
      {
        "account": "P1",
        "broker": "paper",
        "broker_name": "Paper",
        "tag": null,
        "multiplier": 1,
        "supported": true,
        "status": "ready",
        "missing": [],
        "updates": "postback",
        "static_ip": null,
        "last_login_at": null,
        "session_expires_at": null,
        "seat_assigned": null,
        "last_update_at": null
      },
      {
        "account": "P2",
        "broker": "paper",
        "broker_name": "Paper",
        "tag": null,
        "multiplier": 2,
        "supported": true,
        "status": "ready",
        "missing": [],
        "updates": "postback",
        "static_ip": null,
        "last_login_at": null,
        "session_expires_at": null,
        "seat_assigned": null,
        "last_update_at": null
      },
      {
        "account": "PF1",
        "broker": "pocketful",
        "broker_name": "Pocketful",
        "tag": null,
        "multiplier": 1,
        "supported": true,
        "status": "login_required",
        "missing": [],
        "updates": "stream",
        "static_ip": null,
        "last_login_at": null,
        "session_expires_at": null,
        "seat_assigned": null,
        "last_update_at": null
      },
      {
        "account": "TJ1",
        "broker": "tradejini",
        "broker_name": "Tradejini",
        "tag": null,
        "multiplier": 1,
        "supported": true,
        "status": "ready",
        "missing": [
          {
            "field": "static_ip",
            "label": "Static IP",
            "required": false
          }
        ],
        "updates": "stream",
        "static_ip": null,
        "last_login_at": null,
        "session_expires_at": "2026-09-28T20:31:11Z",
        "seat_assigned": null,
        "last_update_at": "2026-09-28T19:31:11.879659Z"
      },
      {
        "account": "U1",
        "broker": "upstox",
        "broker_name": "Upstox",
        "tag": null,
        "multiplier": 1,
        "supported": true,
        "status": "login_required",
        "missing": [],
        "updates": "stream",
        "static_ip": null,
        "last_login_at": null,
        "session_expires_at": null,
        "seat_assigned": null,
        "last_update_at": null
      },
      {
        "account": "Z1",
        "broker": "zerodha",
        "broker_name": "Zerodha",
        "tag": null,
        "multiplier": 1,
        "supported": true,
        "status": "setup_incomplete",
        "missing": [
          {
            "field": "api_key",
            "label": "API Key",
            "required": true
          },
          {
            "field": "static_ip",
            "label": "Static IP",
            "required": false
          }
        ],
        "updates": "stream",
        "static_ip": null,
        "last_login_at": null,
        "session_expires_at": null,
        "seat_assigned": null,
        "last_update_at": null
      }
    ]
  },
  "error": null
}

GET /v1/accounts/{account} #

scope read

One account, plus health: what the order engine last reported for it.

Path parameters

  • account string required

    Account id.

Response data · 200

  • account string required

    Account id (the broker's client code), as used in an order's accounts and the account filters.

  • broker string required

    Broker id (zerodha, upstox, paper, ...).

  • broker_name string required nullable

    Broker display name; null for an unknown broker.

  • tag string required nullable

    The user's own label for the account.

  • multiplier number (double) required

    Quantity multiplier applied when an order is placed in this account.

  • supported boolean required

    Whether the broker is supported for trading.

  • status string required

    Whether the account can trade now: ready, login_required (log in to the broker again), setup_incomplete (a required field is missing, see missing) or unsupported (broker not supported for trading).

    Values:
    • ready
    • login_required
    • setup_incomplete
    • unsupported
  • missing object[] required

    Fields the account lacks (empty when complete).

  • updates string required nullable

    Null for an unsupported broker.

    Values:
    • stream
    • postback
    • polling
  • static_ip string required nullable

    The account's static IP address, if one is assigned.

  • last_login_at string (date-time) required nullable

    Last broker login.

  • session_expires_at string (date-time) required nullable

    When the broker session expires; null when there is no active session (always null for paper accounts, which need none).

  • seat_assigned boolean required nullable

    Whether the account has a trading seat assigned (null when unknown).

  • last_update_at string (date-time) required nullable

    When the order book was last read from the broker.

  • health object required nullable

    Null when no live worker has reported for the account recently.

Errors

HTTPCodeWhen
401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

404account_not_found

No linked broker account with this id for the caller.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

  • 404 account_not_found for an id that is not one of yours.
GET
curl "https://api.cirrus.trade/v1/accounts/<account>" \
  -H "Authorization: token $CIRRUS_API_KEY"
{
  "status": "success",
  "data": {
    "account": "U1",
    "broker": "upstox",
    "broker_name": "Upstox",
    "tag": null,
    "multiplier": 1,
    "supported": true,
    "status": "login_required",
    "missing": [],
    "updates": "stream",
    "static_ip": null,
    "last_login_at": null,
    "session_expires_at": null,
    "seat_assigned": null,
    "last_update_at": null,
    "health": null
  },
  "error": null
}

Instruments#

Everything you can trade, from the daily instrument master, with the `instrument_token` every order needs. The list is refreshed every day after 08:40 IST; until then the previous day is served.

Daily file, with your API key: https://api.cirrus.trade/v1/instruments.csv

  • Contracts listed that morning (new weekly expiries, new strikes) appear after 08:40 IST.
  • Older integrations may send cirrus_token and cirrus_tag: both are accepted as other names for instrument_token and order_tag, and responses carry both.

GET /v1/instruments/{instrument_token} #

scope read

One instrument. One listed during the day (not yet in the daily file) is also found, with isin, name and sector null.

Path parameters

  • instrument_token string required

    Instrument id.

Response data · 200

  • instrument_token string required

    Instrument id: what orders, triggers and every row call instrument_token.

  • exchange string required

    Exchange (NSE, BSE, NFO, BFO, MCX, ...), upper case.

  • instrument string required nullable

    Instrument type (EQUITY, INDEX, FUTIDX, OPTIDX, OPTSTK, ...).

  • exchange_token string required nullable

    The exchange's own token for the instrument.

  • lot_size integer (int64) required

    Contract lot size; 0 for indices (not tradable).

  • expiry string required nullable

    Expiry date (YYYY-MM-DD); null for non-derivatives.

  • strike number (double) required nullable

    Strike price; null for non-options.

  • option_type string required nullable

    CE / PE; null for non-options.

  • tick_size number (double) required nullable

    Smallest price step.

  • underlying_symbol string required nullable

    Underlying symbol (derivatives), or the stock's own symbol.

  • isin string required nullable
  • freeze_qty integer (int64) required nullable

    Exchange freeze quantity: larger orders are split into slices.

  • name string required nullable

    Company or instrument name.

  • tradingsymbol string required

    Trading symbol, as the exchange lists it.

  • sector string required nullable
  • indices string[] required

    Index memberships (e.g. NIFTY 50); empty when none.

Errors

HTTPCodeWhen
401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

404instrument_not_found

No instrument with this token (unknown or expired).

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

  • 404 instrument_not_found for an unknown or expired instrument.
GET
curl "https://api.cirrus.trade/v1/instruments/<instrument_token>" \
  -H "Authorization: token $CIRRUS_API_KEY"
{
  "status": "success",
  "data": {
    "exchange": "NSE",
    "instrument": "OPTIDX",
    "exchange_token": "50319",
    "lot_size": 60,
    "expiry": "2026-10-27",
    "strike": 25000,
    "option_type": "CE",
    "tick_size": 0.05,
    "underlying_symbol": "FINNIFTY",
    "isin": null,
    "freeze_qty": 1801,
    "name": "FINNIFTY",
    "tradingsymbol": "FINNIFTY27OCT202625000CE",
    "sector": null,
    "indices": [],
    "instrument_token": "CT:1:6:FINNIFTY27OCT202625000CE"
  },
  "error": null
}

GET /v1/instruments.csv #

scope read

The whole list as CSV (about 10 MB, 1.3 MB gzipped), same columns as above, indices pipe-separated. Download once a day after 08:40 IST and search locally for many lookups.

Headers

  • If-None-Match string optional

    An ETag received earlier.

  • If-Modified-Since string optional

    A Last-Modified received earlier.

Response data · 200

No response body.

Errors

HTTPCodeWhen
401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503instruments_unavailable

The instrument master is not loaded yet; retry shortly.

  • Send Accept-Encoding: gzip for a compressed body. Cache-Control: public, max-age=300.
  • ETag and Last-Modified are the same on every server: send them back as If-None-Match / If-Modified-Since and get 304 Not Modified (no body) while the file is unchanged.
GET
curl "https://api.cirrus.trade/v1/instruments.csv" \
  -H "Authorization: token $CIRRUS_API_KEY"
{
  "status": "error",
  "data": null,
  "error": {
    "code": "instruments_unavailable",
    "message": "The instrument list is loading. Please retry in a minute.",
    "field": null
  }
}

Orders#

Place in one or many accounts, modify, cancel, and broker bracket / cover orders. Order state changes arrive on the stream.

POST /v1/orders #

scope orders

Place orders, each in one or more accounts. Large quantities are split into freeze-quantity slices.

Headers

  • Idempotency-Key string required

    A new UUID per order request (1-128 printable characters).

Body (JSON)

  • orders object[] required
  • use_multiplier boolean optional

    Scale quantity by each account's Multiplier.

Response data · 200

  • results object[] required
  • accepted integer required
  • rejected integer required
  • unknown integer required

Errors

HTTPCodeWhen
400malformed_json

The body is not JSON, or does not fit the request shape (unknown or missing fields, wrong types).

400idempotency_key_required

The Idempotency-Key header is missing (use a new UUID per order request).

400invalid_idempotency_key

The Idempotency-Key header is not 1-128 printable characters.

401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

409request_in_progress

A request with this Idempotency-Key is still being processed.

422invalid_request

The request is well-formed but not valid; field names the input.

422idempotency_key_reused

This Idempotency-Key was already used with a different request.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

  • Returns 200 with per-slice results once placement starts: some accounts may succeed while others are refused. Request problems (bad JSON, validation, unknown instrument, missing Idempotency-Key) are 4xx and place nothing.
  • unknown means the broker may or may not have the order (for example a timeout). Never retry blindly; watch the stream or retry with the same Idempotency-Key.
  • Unknown fields are rejected, not ignored.
  • protection is broker-native first: intraday goes out as a broker bracket / cover order, positional gets a broker GTT on fill; a Cirrus trigger only when the broker cannot.
POST
curl -X POST "https://api.cirrus.trade/v1/orders" \
  -H "Authorization: token $CIRRUS_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "orders": [
      {
        "instrument_token": "CT:TEST:RELIANCE",
        "side": "BUY",
        "order_type": "LIMIT",
        "product": "DELIVERY",
        "quantity": 1,
        "price": 2500,
        "accounts": [
          "P1",
          "Z1"
        ]
      }
    ]
  }'
{
  "status": "success",
  "data": {
    "results": [
      {
        "order_index": 0,
        "account": "PF1",
        "quantity": 5,
        "status": "accepted",
        "order_id": null,
        "message": "placed as a broker bracket order (basket 777)",
        "order_type": "LIMIT",
        "price": 2500,
        "protection": "bracket",
        "protection_id": "777",
        "instrument_token": "CT:TEST:RELIANCE",
        "order_tag": "01M3MQZWZ8PW47QZB5A0Y2DWXR"
      },
      {
        "order_index": 0,
        "account": "P1",
        "quantity": 5,
        "status": "accepted",
        "order_id": "PAPER-1",
        "message": null,
        "order_type": "LIMIT",
        "price": 2500,
        "protection": "trigger",
        "instrument_token": "CT:TEST:RELIANCE",
        "order_tag": "01M3MQZWZ84XTJYCDY33KN40EP"
      }
    ],
    "accepted": 2,
    "rejected": 0,
    "unknown": 0
  },
  "error": null
}

PATCH /v1/orders/{order_id} #

scope orders

Modify one open order in one account.

Path parameters

  • order_id string required

    Broker order id (1-64 letters, digits, -, _ or .).

Body (JSON)

  • account string required

    Account id the order is in (see /v1/accounts).

  • instrument_token string required

    Instrument id of the order.

  • side string required

    Order side.

    Values:
    • BUY
    • SELL
  • order_type string required

    MARKET, LIMIT, SL (stop-loss limit) or SL_M (stop-loss market; SL-M and SLM are accepted in requests).

    Values:
    • MARKET
    • LIMIT
    • SL
    • SL_M
  • product string required

    MIS (intraday), CARRYFORWARD (overnight derivatives, NRML), DELIVERY (equity delivery, CNC) or MTF (margin trading facility).

    Values:
    • MIS
    • CARRYFORWARD
    • DELIVERY
    • MTF
  • quantity integer (int64) required

    Units, or lots when qty_is_in_lot; a multiple of the lot size.

  • qty_is_in_lot boolean optional

    quantity counts lots, not units.

  • price number (double) optional

    Limit price (LIMIT, SL); 0 or absent for market orders.

  • trigger_price number (double) optional nullable

    Trigger price for SL / SL_M.

Response data · 200

  • account string required
  • order_id string required
  • status string required

    accepted (the broker took it; fills arrive on the stream), rejected (refused before or by the broker; nothing was placed) or unknown (the broker may or may not have it, e.g. a timeout; reconciliation settles it: never retry blindly).

    Values:
    • accepted
    • rejected
    • unknown
  • message string required nullable

    Why it was rejected, or what is unknown; null when accepted.

Errors

HTTPCodeWhen
400malformed_json

The body is not JSON, or does not fit the request shape (unknown or missing fields, wrong types).

401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

404account_not_found

No linked broker account with this id for the caller.

422invalid_request

The request is well-formed but not valid; field names the input.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

PATCH
curl -X PATCH "https://api.cirrus.trade/v1/orders/<order_id>" \
  -H "Authorization: token $CIRRUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "account": "P1",
    "instrument_token": "CT:TEST:RELIANCE",
    "side": "BUY",
    "order_type": "SL",
    "product": "MIS",
    "quantity": 5,
    "price": 2500,
    "trigger_price": 2499
  }'
{
  "status": "success",
  "data": {
    "account": "P1",
    "order_id": "PAPER-9",
    "status": "accepted",
    "message": null
  },
  "error": null
}

DELETE /v1/orders/{order_id} #

scope orders

Cancel one open order in one account.

Path parameters

  • order_id string required

    Broker order id (1-64 letters, digits, -, _ or .).

Query parameters

  • account string required

    Account id the order is in.

  • instrument_token string required

    Instrument id of the order.

  • quantity integer (int64) optional

    Units still open (some brokers need it to cancel); 0 or absent otherwise.

Response data · 200

  • account string required
  • order_id string required
  • status string required

    accepted (the broker took it; fills arrive on the stream), rejected (refused before or by the broker; nothing was placed) or unknown (the broker may or may not have it, e.g. a timeout; reconciliation settles it: never retry blindly).

    Values:
    • accepted
    • rejected
    • unknown
  • message string required nullable

    Why it was rejected, or what is unknown; null when accepted.

Errors

HTTPCodeWhen
400invalid_query

The query string does not fit the route's parameters.

401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

404account_not_found

No linked broker account with this id for the caller.

422invalid_request

The request is well-formed but not valid; field names the input.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

DELETE
curl -X DELETE "https://api.cirrus.trade/v1/orders/<order_id>?account=<account>&instrument_token=<instrument_token>" \
  -H "Authorization: token $CIRRUS_API_KEY"
{
  "status": "success",
  "data": {
    "account": "P1",
    "order_id": "PAPER-7",
    "status": "rejected",
    "message": "Order already completed"
  },
  "error": null
}

POST /v1/orders/bracket #

scope orders

Broker bracket (BO: entry + stop-loss + target) or cover (CO: entry + stop-loss) order in one account. Pocketful and Tradejini.

Headers

  • Idempotency-Key string required

    A new UUID per order request (1-128 printable characters).

Body (JSON)

  • account string required

    Account id (see /v1/accounts).

  • instrument_token string required

    Instrument id (see /v1/instruments).

  • kind string required

    BO (bracket: entry + stop-loss + target) or CO (cover: entry + stop-loss).

    Values:
    • BO
    • CO
  • side string required

    Order side.

    Values:
    • BUY
    • SELL
  • order_type string required

    Entry order type: LIMIT or MARKET.

    Values:
    • MARKET
    • LIMIT
    • SL
    • SL_M
  • product string required

    MIS (intraday), CARRYFORWARD (overnight derivatives, NRML), DELIVERY (equity delivery, CNC) or MTF (margin trading facility).

    Values:
    • MIS
    • CARRYFORWARD
    • DELIVERY
    • MTF
  • quantity integer (int64) required

    Units, or lots when qty_is_in_lot; a multiple of the lot size.

  • qty_is_in_lot boolean optional

    quantity counts lots, not units.

  • price number (double) optional

    Entry limit price (LIMIT); 0 or absent for MARKET.

  • stop_loss number (double) required

    Stop-loss price.

  • target number (double) optional nullable

    Target price: required for BO, not allowed for CO.

  • trailing_stop_loss number (double) optional nullable

    Trailing stop-loss step, in points (on the tick grid).

Response data · 200

  • account string required
  • basket_id string required nullable

    Broker basket / parent id (placement only; null otherwise).

  • order_id string required nullable

    The order changed or cancelled (null on placement).

  • status string required

    accepted (the broker took it; fills arrive on the stream), rejected (refused before or by the broker; nothing was placed) or unknown (the broker may or may not have it, e.g. a timeout; reconciliation settles it: never retry blindly).

    Values:
    • accepted
    • rejected
    • unknown
  • message string required nullable

    Why it was rejected, or what is unknown; null when accepted.

Errors

HTTPCodeWhen
400idempotency_key_required

The Idempotency-Key header is missing (use a new UUID per order request).

400invalid_idempotency_key

The Idempotency-Key header is not 1-128 printable characters.

400malformed_json

The body is not JSON, or does not fit the request shape (unknown or missing fields, wrong types).

401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

404account_not_found

No linked broker account with this id for the caller.

409request_in_progress

A request with this Idempotency-Key is still being processed.

422invalid_request

The request is well-formed but not valid; field names the input.

422idempotency_key_reused

This Idempotency-Key was already used with a different request.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

  • Legs are absolute prices checked against the entry (the limit price, or LTP for a market entry): a BUY needs stop_loss < entry < target, on the tick grid.
  • The legs show up in the order book as ordinary orders.
POST
curl -X POST "https://api.cirrus.trade/v1/orders/bracket" \
  -H "Authorization: token $CIRRUS_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "account": "PF1",
    "instrument_token": "CT:TEST:RELIANCE",
    "kind": "BO",
    "side": "BUY",
    "order_type": "LIMIT",
    "product": "MIS",
    "quantity": 5,
    "price": 2500,
    "stop_loss": 2450,
    "target": 2600
  }'
{
  "status": "success",
  "data": {
    "account": "PF1",
    "basket_id": "777",
    "order_id": null,
    "status": "accepted",
    "message": null
  },
  "error": null
}

PATCH /v1/orders/bracket/{order_id} #

scope orders

Change the price, quantity or legs of a bracket / cover order.

Path parameters

  • order_id string required

    Broker order id of the entry (1-64 letters, digits, -, _ or .).

Body (JSON)

  • account string required

    Account id the order is in.

  • instrument_token string required

    Instrument id of the order.

  • kind string required

    BO (bracket: entry + stop-loss + target) or CO (cover: entry + stop-loss).

    Values:
    • BO
    • CO
  • side string required

    Side of the entry.

    Values:
    • BUY
    • SELL
  • entry_price number (double) required

    Entry price the legs are measured from.

  • price number (double) optional nullable

    New entry limit price.

  • quantity integer (int64) optional nullable

    New quantity, in units.

  • stop_loss number (double) optional nullable

    New stop-loss price.

  • target number (double) optional nullable

    New target price (BO only).

  • trailing_stop_loss number (double) optional nullable

    New trailing stop-loss step, in points.

Response data · 200

  • account string required
  • basket_id string required nullable

    Broker basket / parent id (placement only; null otherwise).

  • order_id string required nullable

    The order changed or cancelled (null on placement).

  • status string required

    accepted (the broker took it; fills arrive on the stream), rejected (refused before or by the broker; nothing was placed) or unknown (the broker may or may not have it, e.g. a timeout; reconciliation settles it: never retry blindly).

    Values:
    • accepted
    • rejected
    • unknown
  • message string required nullable

    Why it was rejected, or what is unknown; null when accepted.

Errors

HTTPCodeWhen
400malformed_json

The body is not JSON, or does not fit the request shape (unknown or missing fields, wrong types).

401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

404account_not_found

No linked broker account with this id for the caller.

422invalid_request

The request is well-formed but not valid; field names the input.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

PATCH
curl -X PATCH "https://api.cirrus.trade/v1/orders/bracket/<order_id>" \
  -H "Authorization: token $CIRRUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "account": "PF1",
    "instrument_token": "CT:TEST:RELIANCE",
    "kind": "BO",
    "side": "BUY",
    "entry_price": 2500,
    "stop_loss": 2460,
    "target": 2620
  }'
{
  "status": "success",
  "data": {
    "account": "PF1",
    "basket_id": null,
    "order_id": "250929000001",
    "status": "accepted",
    "message": null
  },
  "error": null
}

DELETE /v1/orders/bracket/{order_id} #

scope orders

Cancel a bracket / cover order.

Path parameters

  • order_id string required

    Broker order id of the entry (1-64 letters, digits, -, _ or .).

Query parameters

  • account string required

    Account id the order is in.

  • kind string required

    BO or CO.

    Values:
    • BO
    • CO

Response data · 200

  • account string required
  • basket_id string required nullable

    Broker basket / parent id (placement only; null otherwise).

  • order_id string required nullable

    The order changed or cancelled (null on placement).

  • status string required

    accepted (the broker took it; fills arrive on the stream), rejected (refused before or by the broker; nothing was placed) or unknown (the broker may or may not have it, e.g. a timeout; reconciliation settles it: never retry blindly).

    Values:
    • accepted
    • rejected
    • unknown
  • message string required nullable

    Why it was rejected, or what is unknown; null when accepted.

Errors

HTTPCodeWhen
400—

Plain text (not the JSON envelope): a query parameter is missing or not valid, e.g. Failed to deserialize query string: missing field kind.

401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

404account_not_found

No linked broker account with this id for the caller.

422invalid_request

The request is well-formed but not valid; field names the input.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

DELETE
curl -X DELETE "https://api.cirrus.trade/v1/orders/bracket/<order_id>?account=<account>&kind=<kind>" \
  -H "Authorization: token $CIRRUS_API_KEY"
{
  "status": "success",
  "data": {
    "account": "PF1",
    "basket_id": null,
    "order_id": "250929000001",
    "status": "accepted",
    "message": null
  },
  "error": null
}

Protection rules#

The rules object used by an order's protection and by Cirrus triggers. Levels are measured from the entry price.

Rules

  • stop_loss rule optional

    Exit below (long) / above (short) the entry.

  • target rule optional

    Profit exit. Enable a stop-loss, a target, or both.

  • trail rule optional

    Trailing stop-loss; needs stop_loss and cannot be of type price.

  • <rule>.enabled boolean optional

    Default false.

  • <rule>.type percentage | points | price optional

    Default percentage; any casing.

  • <rule>.value number optional

    Must be above 0 when enabled.

Rules object
{
  "stop_loss": {"enabled": true, "type": "percentage", "value": 1.5},
  "target":    {"enabled": true, "type": "price", "value": 250},
  "trail":     {"enabled": true, "type": "points", "value": 2}
}

Portfolio#

Reads come from the latest snapshots, never a broker call, so they are fast and do not spend the broker rate budget.

GET /v1/portfolio/{kind} #

scope read

kind is orders, positions, holdings, trades or margins.

Path parameters

  • kind string required

    Which data: orders, positions, holdings, trades or margins.

    Values:
    • orders
    • positions
    • holdings
    • trades
    • margins

Query parameters

  • account string optional

    Only this linked account (its id).

Response data · 200

  • accounts object[] required
  • missing object[] required

Errors

HTTPCodeWhen
400—

Unreadable query string (an unknown parameter): a plain-text answer, not the JSON envelope.

401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

404account_not_found

No linked broker account with this id for the caller.

422invalid_request

The request is well-formed but not valid; field names the input.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

GET
curl "https://api.cirrus.trade/v1/portfolio/<kind>" \
  -H "Authorization: token $CIRRUS_API_KEY"
{
  "status": "success",
  "data": {
    "accounts": [
      {
        "account": "P1",
        "updated_at": "2026-09-28T19:31:12.035116Z",
        "data": [
          {
            "account": "P1",
            "tradingsymbol": "RELIANCE-EQ",
            "product": "DELIVERY",
            "quantity": 10,
            "average_price": 2400,
            "invested_amount": 24000,
            "pnl": 1000,
            "pnl_percent": 4.17,
            "lot_size": 1,
            "exchange": "NSE",
            "instrument_type": "EQUITY",
            "symbol": "RELIANCE",
            "expiry": "",
            "strike": 0,
            "option_type": "",
            "ltp": 2500,
            "prev_close": 0,
            "instrument_token": "CT:TEST:RELIANCE"
          }
        ]
      }
    ],
    "missing": []
  },
  "error": null
}

POST /v1/portfolio/refresh #

scope read

Ask for a fresh read from the broker. Answers 202; new data arrives on the stream.

Query parameters

  • account string optional

    Only this linked account (its id).

Response data · 202

  • requested integer required

    How many accounts were asked to re-read.

Errors

HTTPCodeWhen
400—

Unreadable query string (an unknown parameter): a plain-text answer, not the JSON envelope.

401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

404account_not_found

No linked broker account with this id for the caller.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

POST
curl -X POST "https://api.cirrus.trade/v1/portfolio/refresh" \
  -H "Authorization: token $CIRRUS_API_KEY"
{
  "status": "success",
  "data": {
    "requested": 1
  },
  "error": null
}

Triggers#

Cirrus stop-loss / target / trail triggers, evaluated on every tick. Protection held by the broker (bracket, cover, GTT) is listed here but changed at the broker.

GET /v1/triggers #

scope read

Every trigger of yours, with live levels and status.

Response data · 200

  • trigger_id string required
  • account string required

    Account id (see /v1/accounts).

  • instrument_token string required nullable

    Instrument id.

  • name string required

    Enabled rules: SL, TGT, TSL, joined with + .

  • side string required

    Side of the protected entry: BUY (a long position, exited by a SELL) or SELL.

  • product string required

    Product of the position (MIS, CARRYFORWARD, DELIVERY, MTF).

  • quantity integer (int64) required

    Quantity the exit covers, in units.

  • price number (double) required

    Entry / average price the levels are measured from.

  • order_id string required nullable

    The protected entry order, when the trigger came with one.

  • broker_order_id string required nullable

    Broker id of the order / GTT holding the legs (broker-held only).

  • kind string required nullable

    Where the protection lives: platform (a server-side trigger; the server places the exit), bracket or cover (broker bracket / cover order legs), gtt_oco or gtt_single (broker GTT with two legs or one). Null for triggers created without a kind; other values may appear for triggers created elsewhere.

  • exits boolean required

    The server (not the broker) places the exit when a level is hit. false: the broker holds the legs, so change or cancel them at the broker (only delete works here).

  • rules object required

    The trigger's stop-loss, target and trailing stop-loss rules.

  • stop_loss_price number (double) required nullable

    Current stop-loss level (moves when trailing); null when disabled.

  • initial_stop_loss_price number (double) required nullable

    Stop-loss level before any trailing; null when disabled.

  • target_price number (double) required nullable

    Target level; null when disabled.

  • live boolean required

    Protection is armed (the entry filled).

  • status string required nullable

    Set once the trigger fired or ended: sl_hit, target_hit, target_reached, invalidated, exit, cancelled, ...; null while it watches.

  • created_at string required

    Creation time (India time, YYYY-MM-DD HH:MM:SS).

  • updated_at string required

    Last change (India time, YYYY-MM-DD HH:MM:SS).

Errors

HTTPCodeWhen
401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

GET
curl "https://api.cirrus.trade/v1/triggers" \
  -H "Authorization: token $CIRRUS_API_KEY"
{
  "status": "success",
  "data": [
    {
      "trigger_id": "efb67bb6-5c8a-44dc-ab8e-dbdec912a0b6",
      "account": "P1",
      "name": "SL + TGT + TSL",
      "side": "BUY",
      "product": "MIS",
      "quantity": 5,
      "price": 2500,
      "order_id": null,
      "broker_order_id": null,
      "kind": "platform",
      "rules": {
        "target": {
          "enabled": true,
          "type": "percentage",
          "value": 2
        },
        "stop_loss": {
          "enabled": true,
          "type": "points",
          "value": 25
        },
        "trail": {
          "enabled": true,
          "type": "points",
          "value": 5
        }
      },
      "stop_loss_price": 2475,
      "initial_stop_loss_price": 2475,
      "target_price": 2550,
      "live": true,
      "status": null,
      "created_at": "2026-09-29 01:01:12",
      "updated_at": "2026-09-29 01:01:12",
      "instrument_token": "CT:TEST:RELIANCE",
      "exits": true
    },
    {
      "trigger_id": "489301aa-4fbb-4cb6-adef-6b7813c6b78f",
      "account": "PF1",
      "name": "SL + TGT",
      "side": "BUY",
      "product": "DELIVERY",
      "quantity": 5,
      "price": 2500,
      "order_id": "250929000002",
      "broker_order_id": "gtt-held",
      "kind": "gtt_oco",
      "rules": {
        "target": {
          "enabled": true,
          "type": "points",
          "value": 100
        },
        "stop_loss": {
          "enabled": true,
          "type": "points",
          "value": 25
        },
        "trail": {
          "enabled": false,
          "type": "percentage",
          "value": 0
        }
      },
      "stop_loss_price": 2475,
      "initial_stop_loss_price": 2475,
      "target_price": 2600,
      "live": true,
      "status": null,
      "created_at": "2026-09-29 01:01:12",
      "updated_at": "2026-09-29 01:01:12",
      "instrument_token": "CT:TEST:RELIANCE",
      "exits": false
    }
  ],
  "error": null
}

POST /v1/triggers #

scope triggers

Protect an existing position.

Body (JSON)

  • account string required

    Account id (see /v1/accounts).

  • instrument_token string required

    Instrument id (see /v1/instruments).

  • side string required

    Side of the position's entry (BUY = long, protected by a SELL exit).

    Values:
    • BUY
    • SELL
  • product string required

    MIS (intraday), CARRYFORWARD (overnight derivatives, NRML), DELIVERY (equity delivery, CNC) or MTF (margin trading facility).

    Values:
    • MIS
    • CARRYFORWARD
    • DELIVERY
    • MTF
  • quantity integer (int64) required

    Units to exit when a level is hit.

  • price number (double) optional nullable

    Entry / average price the levels are measured from; LTP if absent.

  • rules object required

    Stop-loss, target and trailing stop-loss. Enable a stop-loss, a target or both; a trail needs a stop-loss and is percentage or points.

Response data · 201

  • trigger_id string required
  • account string required

    Account id (see /v1/accounts).

  • instrument_token string required nullable

    Instrument id.

  • name string required

    Enabled rules: SL, TGT, TSL, joined with + .

  • side string required

    Side of the protected entry: BUY (a long position, exited by a SELL) or SELL.

  • product string required

    Product of the position (MIS, CARRYFORWARD, DELIVERY, MTF).

  • quantity integer (int64) required

    Quantity the exit covers, in units.

  • price number (double) required

    Entry / average price the levels are measured from.

  • order_id string required nullable

    The protected entry order, when the trigger came with one.

  • broker_order_id string required nullable

    Broker id of the order / GTT holding the legs (broker-held only).

  • kind string required nullable

    Where the protection lives: platform (a server-side trigger; the server places the exit), bracket or cover (broker bracket / cover order legs), gtt_oco or gtt_single (broker GTT with two legs or one). Null for triggers created without a kind; other values may appear for triggers created elsewhere.

  • exits boolean required

    The server (not the broker) places the exit when a level is hit. false: the broker holds the legs, so change or cancel them at the broker (only delete works here).

  • rules object required

    The trigger's stop-loss, target and trailing stop-loss rules.

  • stop_loss_price number (double) required nullable

    Current stop-loss level (moves when trailing); null when disabled.

  • initial_stop_loss_price number (double) required nullable

    Stop-loss level before any trailing; null when disabled.

  • target_price number (double) required nullable

    Target level; null when disabled.

  • live boolean required

    Protection is armed (the entry filled).

  • status string required nullable

    Set once the trigger fired or ended: sl_hit, target_hit, target_reached, invalidated, exit, cancelled, ...; null while it watches.

  • created_at string required

    Creation time (India time, YYYY-MM-DD HH:MM:SS).

  • updated_at string required

    Last change (India time, YYYY-MM-DD HH:MM:SS).

Errors

HTTPCodeWhen
400malformed_json

The body is not JSON, or does not fit the request shape (unknown or missing fields, wrong types).

401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

404account_not_found

No linked broker account with this id for the caller.

422invalid_request

The request is well-formed but not valid; field names the input.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503trigger_worker_down

No trigger exit worker is running; nothing was created.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

  • 503 trigger_worker_down when no exit worker is running; nothing is created.
POST
curl -X POST "https://api.cirrus.trade/v1/triggers" \
  -H "Authorization: token $CIRRUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "account": "P1",
    "instrument_token": "CT:TEST:RELIANCE",
    "side": "BUY",
    "product": "MIS",
    "quantity": 5,
    "price": 2500,
    "rules": {
      "stop_loss": {
        "enabled": true,
        "type": "points",
        "value": 25
      },
      "target": {
        "enabled": true,
        "type": "percentage",
        "value": 2
      },
      "trail": {
        "enabled": true,
        "type": "points",
        "value": 5
      }
    }
  }'
{
  "status": "success",
  "data": {
    "trigger_id": "aa554bda-c2ad-4981-9f83-c7291c567c40",
    "account": "P1",
    "name": "SL + TGT + TSL",
    "side": "BUY",
    "product": "MIS",
    "quantity": 5,
    "price": 2500,
    "order_id": null,
    "broker_order_id": null,
    "kind": "platform",
    "rules": {
      "target": {
        "enabled": true,
        "type": "percentage",
        "value": 2
      },
      "stop_loss": {
        "enabled": true,
        "type": "points",
        "value": 25
      },
      "trail": {
        "enabled": true,
        "type": "points",
        "value": 5
      }
    },
    "stop_loss_price": 2475,
    "initial_stop_loss_price": 2475,
    "target_price": 2550,
    "live": true,
    "status": null,
    "created_at": "2026-09-29 01:01:12",
    "updated_at": "2026-09-29 01:01:12",
    "instrument_token": "CT:TEST:RELIANCE",
    "exits": true
  },
  "error": null
}

PATCH /v1/triggers/{id} #

scope triggers

Change rules, quantity or price. Send at least one.

Path parameters

  • trigger_id string required

    The trigger's id.

Body (JSON)

  • rules object optional nullable

    New rules (all three legs).

  • quantity integer (int64) optional nullable

    New quantity, in units.

  • price number (double) optional nullable

    New entry price the levels are measured from.

Response data · 200

  • trigger_id string required
  • account string required

    Account id (see /v1/accounts).

  • instrument_token string required nullable

    Instrument id.

  • name string required

    Enabled rules: SL, TGT, TSL, joined with + .

  • side string required

    Side of the protected entry: BUY (a long position, exited by a SELL) or SELL.

  • product string required

    Product of the position (MIS, CARRYFORWARD, DELIVERY, MTF).

  • quantity integer (int64) required

    Quantity the exit covers, in units.

  • price number (double) required

    Entry / average price the levels are measured from.

  • order_id string required nullable

    The protected entry order, when the trigger came with one.

  • broker_order_id string required nullable

    Broker id of the order / GTT holding the legs (broker-held only).

  • kind string required nullable

    Where the protection lives: platform (a server-side trigger; the server places the exit), bracket or cover (broker bracket / cover order legs), gtt_oco or gtt_single (broker GTT with two legs or one). Null for triggers created without a kind; other values may appear for triggers created elsewhere.

  • exits boolean required

    The server (not the broker) places the exit when a level is hit. false: the broker holds the legs, so change or cancel them at the broker (only delete works here).

  • rules object required

    The trigger's stop-loss, target and trailing stop-loss rules.

  • stop_loss_price number (double) required nullable

    Current stop-loss level (moves when trailing); null when disabled.

  • initial_stop_loss_price number (double) required nullable

    Stop-loss level before any trailing; null when disabled.

  • target_price number (double) required nullable

    Target level; null when disabled.

  • live boolean required

    Protection is armed (the entry filled).

  • status string required nullable

    Set once the trigger fired or ended: sl_hit, target_hit, target_reached, invalidated, exit, cancelled, ...; null while it watches.

  • created_at string required

    Creation time (India time, YYYY-MM-DD HH:MM:SS).

  • updated_at string required

    Last change (India time, YYYY-MM-DD HH:MM:SS).

Errors

HTTPCodeWhen
400malformed_json

The body is not JSON, or does not fit the request shape (unknown or missing fields, wrong types).

401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

404trigger_not_found

No trigger with this id for the caller.

409trigger_fired

The trigger already fired; its exit is being handled.

409broker_held

The protection is held by the broker; change the broker order instead.

409trigger_busy

The trigger changed while it was being updated; retry.

422invalid_request

The request is well-formed but not valid; field names the input.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

  • 409 trigger_busy (changed while updating; retry), trigger_fired (already fired), or broker_held (change the broker order instead).
PATCH
curl -X PATCH "https://api.cirrus.trade/v1/triggers/<trigger_id>" \
  -H "Authorization: token $CIRRUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "price": 2510,
    "quantity": 3
  }'
{
  "status": "success",
  "data": {
    "trigger_id": "710f58d1-9253-4194-b225-12f7a23e5862",
    "account": "P1",
    "name": "SL + TGT + TSL",
    "side": "BUY",
    "product": "MIS",
    "quantity": 3,
    "price": 2510,
    "order_id": null,
    "broker_order_id": null,
    "kind": "platform",
    "rules": {
      "target": {
        "enabled": true,
        "type": "percentage",
        "value": 2
      },
      "stop_loss": {
        "enabled": true,
        "type": "points",
        "value": 25
      },
      "trail": {
        "enabled": true,
        "type": "points",
        "value": 5
      }
    },
    "stop_loss_price": 2485,
    "initial_stop_loss_price": 2485,
    "target_price": 2560.2,
    "live": true,
    "status": null,
    "created_at": "2026-09-29 01:01:12",
    "updated_at": "2026-09-29 01:01:12",
    "instrument_token": "CT:TEST:RELIANCE",
    "exits": true
  },
  "error": null
}

DELETE /v1/triggers/{id} #

scope triggers

Remove a trigger.

Path parameters

  • trigger_id string required

    The trigger's id.

Response data · 200

  • trigger_id string required
  • deleted boolean required

    Always true.

Errors

HTTPCodeWhen
401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

404trigger_not_found

No trigger with this id for the caller.

404account_not_found

No linked broker account with this id for the caller.

409trigger_fired

The trigger already fired; its exit is being handled.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

502broker_refused

The broker refused the change.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

DELETE
curl -X DELETE "https://api.cirrus.trade/v1/triggers/<trigger_id>" \
  -H "Authorization: token $CIRRUS_API_KEY"
{
  "status": "success",
  "data": {
    "trigger_id": "33a9061d-deb5-4c85-8969-01d63f07e0bc",
    "deleted": true
  },
  "error": null
}

GTT#

Good-till-triggered orders held at the broker: one leg, or stop-loss + target. Zerodha, Pocketful, Tradejini. Shares the per-account order budget.

GET /v1/gtt #

scope read

Every GTT at the broker, per account, including ones made outside Cirrus (live read). Listing is available for Zerodha; other brokers report "not available".

Query parameters

  • account string optional

    Only this account; every account when absent.

Response data · 200

  • account string required
  • broker string required

    The account's broker.

  • gtts object[] required

    Empty when the list could not be read (error says why).

  • error string required nullable

    Why this account's list is missing (others still listed); null when it was read.

Errors

HTTPCodeWhen
400—

Plain text (not the JSON envelope): the query string is not valid, e.g. an unknown parameter.

401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

404account_not_found

No linked broker account with this id for the caller.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

GET
curl "https://api.cirrus.trade/v1/gtt" \
  -H "Authorization: token $CIRRUS_API_KEY"
{
  "status": "success",
  "data": [
    {
      "account": "P1",
      "broker": "paper",
      "gtts": [],
      "error": "Listing GTTs is not available for paper accounts."
    }
  ],
  "error": null
}

POST /v1/gtt #

scope orders

Create a GTT in one account.

Body (JSON)

  • account string required

    Account id (see /v1/accounts).

  • instrument_token string required

    Instrument id (see /v1/instruments).

  • side string required

    Side of the order a trigger places (SELL to protect a long).

    Values:
    • BUY
    • SELL
  • product string required

    MIS (intraday), CARRYFORWARD (overnight derivatives, NRML), DELIVERY (equity delivery, CNC) or MTF (margin trading facility).

    Values:
    • MIS
    • CARRYFORWARD
    • DELIVERY
    • MTF
  • quantity integer (int64) required

    Units, or lots when qty_is_in_lot; a multiple of the lot size.

  • qty_is_in_lot boolean optional

    quantity counts lots, not units.

  • stop_loss object optional nullable

    One GTT leg: the price that fires it and the order's limit price.

  • target object optional nullable

    One GTT leg: the price that fires it and the order's limit price.

Response data · 200

  • account string required
  • trigger_id string required nullable

    The broker's GTT id; null when creating failed.

  • status string required

    accepted (the broker took it; fills arrive on the stream), rejected (refused before or by the broker; nothing was placed) or unknown (the broker may or may not have it, e.g. a timeout; reconciliation settles it: never retry blindly).

    Values:
    • accepted
    • rejected
    • unknown
  • message string required nullable

    Why it was rejected, or what is unknown; null when accepted.

Errors

HTTPCodeWhen
400malformed_json

The body is not JSON, or does not fit the request shape (unknown or missing fields, wrong types).

401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

404account_not_found

No linked broker account with this id for the caller.

422invalid_request

The request is well-formed but not valid; field names the input.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

  • A SELL stop-loss sits below LTP and its target above (reversed for BUY). A single leg may be either, but not at LTP.
POST
curl -X POST "https://api.cirrus.trade/v1/gtt" \
  -H "Authorization: token $CIRRUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "account": "PF1",
    "instrument_token": "CT:TEST:RELIANCE",
    "side": "SELL",
    "product": "DELIVERY",
    "quantity": 5,
    "stop_loss": {
      "trigger": 2400
    },
    "target": {
      "trigger": 2600,
      "price": 2600
    }
  }'
{
  "status": "success",
  "data": {
    "account": "PF1",
    "trigger_id": "0f6c1a52-4d7e-4b8e-9d51-2c7a9e3b1f20",
    "status": "accepted",
    "message": null
  },
  "error": null
}

PATCH /v1/gtt/{trigger_id} #

scope orders

Replace the legs / quantity. Same body as create.

Path parameters

  • trigger_id string required

    The broker's GTT id (1-64 letters, digits, -, _ or .).

Body (JSON)

  • account string required

    Account id (see /v1/accounts).

  • instrument_token string required

    Instrument id (see /v1/instruments).

  • side string required

    Side of the order a trigger places (SELL to protect a long).

    Values:
    • BUY
    • SELL
  • product string required

    MIS (intraday), CARRYFORWARD (overnight derivatives, NRML), DELIVERY (equity delivery, CNC) or MTF (margin trading facility).

    Values:
    • MIS
    • CARRYFORWARD
    • DELIVERY
    • MTF
  • quantity integer (int64) required

    Units, or lots when qty_is_in_lot; a multiple of the lot size.

  • qty_is_in_lot boolean optional

    quantity counts lots, not units.

  • stop_loss object optional nullable

    One GTT leg: the price that fires it and the order's limit price.

  • target object optional nullable

    One GTT leg: the price that fires it and the order's limit price.

Response data · 200

  • account string required
  • trigger_id string required nullable

    The broker's GTT id; null when creating failed.

  • status string required

    accepted (the broker took it; fills arrive on the stream), rejected (refused before or by the broker; nothing was placed) or unknown (the broker may or may not have it, e.g. a timeout; reconciliation settles it: never retry blindly).

    Values:
    • accepted
    • rejected
    • unknown
  • message string required nullable

    Why it was rejected, or what is unknown; null when accepted.

Errors

HTTPCodeWhen
400malformed_json

The body is not JSON, or does not fit the request shape (unknown or missing fields, wrong types).

401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

404account_not_found

No linked broker account with this id for the caller.

422invalid_request

The request is well-formed but not valid; field names the input.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

PATCH
curl -X PATCH "https://api.cirrus.trade/v1/gtt/<trigger_id>" \
  -H "Authorization: token $CIRRUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "account": "PF1",
    "instrument_token": "CT:TEST:RELIANCE",
    "side": "SELL",
    "product": "DELIVERY",
    "quantity": 5,
    "stop_loss": {
      "trigger": 2420
    },
    "target": {
      "trigger": 2650,
      "price": 2650
    }
  }'
{
  "status": "success",
  "data": {
    "account": "PF1",
    "trigger_id": "0f6c1a52-4d7e-4b8e-9d51-2c7a9e3b1f20",
    "status": "accepted",
    "message": null
  },
  "error": null
}

DELETE /v1/gtt/{trigger_id} #

scope orders

Delete a GTT.

Path parameters

  • trigger_id string required

    The broker's GTT id (1-64 letters, digits, -, _ or .).

Query parameters

  • account string required

    Account id the GTT is in.

Response data · 200

  • account string required
  • trigger_id string required nullable

    The broker's GTT id; null when creating failed.

  • status string required

    accepted (the broker took it; fills arrive on the stream), rejected (refused before or by the broker; nothing was placed) or unknown (the broker may or may not have it, e.g. a timeout; reconciliation settles it: never retry blindly).

    Values:
    • accepted
    • rejected
    • unknown
  • message string required nullable

    Why it was rejected, or what is unknown; null when accepted.

Errors

HTTPCodeWhen
400—

Plain text (not the JSON envelope): account is missing or the query string is not valid.

401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

404account_not_found

No linked broker account with this id for the caller.

422invalid_request

The request is well-formed but not valid; field names the input.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

DELETE
curl -X DELETE "https://api.cirrus.trade/v1/gtt/<trigger_id>?account=<account>" \
  -H "Authorization: token $CIRRUS_API_KEY"
{
  "status": "success",
  "data": {
    "account": "PF1",
    "trigger_id": "0f6c1a52-4d7e-4b8e-9d51-2c7a9e3b1f20",
    "status": "accepted",
    "message": null
  },
  "error": null
}

Positions convert#

Move an open position between products (for example MIS to CARRYFORWARD). Zerodha, Pocketful, Upstox, Motilal Oswal, Tradejini.

POST /v1/positions/convert #

scope orders

Convert in one account; positions and margins refresh afterwards.

Body (JSON)

  • account string required

    Account id holding the position.

  • instrument_token string required

    Instrument id of the position.

  • side string required

    Side of the open position (BUY for long, SELL for short).

    Values:
    • BUY
    • SELL
  • quantity integer (int64) required

    Units to convert (greater than 0).

  • from string required

    The position's current product.

    Values:
    • MIS
    • CARRYFORWARD
    • DELIVERY
    • MTF
  • to string required

    The product to move it to (different from from).

    Values:
    • MIS
    • CARRYFORWARD
    • DELIVERY
    • MTF
  • overnight boolean optional

    The position was carried from a previous session (not opened today).

Response data · 200

  • account string required
  • status string required

    accepted (the broker took it; fills arrive on the stream), rejected (refused before or by the broker; nothing was placed) or unknown (the broker may or may not have it, e.g. a timeout; reconciliation settles it: never retry blindly).

    Values:
    • accepted
    • rejected
    • unknown
  • message string required nullable

    Why it was rejected, or what is unknown; null when accepted.

Errors

HTTPCodeWhen
400malformed_json

The body is not JSON, or does not fit the request shape (unknown or missing fields, wrong types).

401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

404account_not_found

No linked broker account with this id for the caller.

422invalid_request

The request is well-formed but not valid; field names the input.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

POST
curl -X POST "https://api.cirrus.trade/v1/positions/convert" \
  -H "Authorization: token $CIRRUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "account": "P1",
    "instrument_token": "CT:TEST:RELIANCE",
    "side": "BUY",
    "quantity": 5,
    "from": "MIS",
    "to": "DELIVERY"
  }'
{
  "status": "success",
  "data": {
    "account": "P1",
    "status": "rejected",
    "message": "Position conversion is not available for paper accounts."
  },
  "error": null
}

Margins#

Margin required for an order body, per order and account, after multiplier / lot rounding and market protection.

POST /v1/margins/orders #

scope read

Same body as POST /v1/orders. Places nothing; no Idempotency-Key needed.

Body (JSON)

  • orders object[] required
  • use_multiplier boolean optional

    Scale quantity by each account's Multiplier.

Response data · 200

  • results object[] required
  • total_required number (double) required

    Sum over results that have a figure.

Errors

HTTPCodeWhen
400malformed_json

The body is not JSON, or does not fit the request shape (unknown or missing fields, wrong types).

401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

422invalid_request

The request is well-formed but not valid; field names the input.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

POST
curl -X POST "https://api.cirrus.trade/v1/margins/orders" \
  -H "Authorization: token $CIRRUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "orders": [
      {
        "instrument_token": "CT:TEST:RELIANCE",
        "side": "BUY",
        "order_type": "LIMIT",
        "product": "MIS",
        "quantity": 10,
        "price": 2500,
        "accounts": [
          "P2",
          "PF1"
        ]
      }
    ],
    "use_multiplier": true
  }'
{
  "status": "success",
  "data": {
    "results": [
      {
        "order_index": 0,
        "account": "PF1",
        "quantity": 10,
        "total_required": null,
        "message": "Margin calculation is not available for pocketful",
        "instrument_token": "CT:TEST:RELIANCE"
      },
      {
        "order_index": 0,
        "account": "P2",
        "quantity": 20,
        "total_required": 50000,
        "message": null,
        "instrument_token": "CT:TEST:RELIANCE"
      }
    ],
    "total_required": 50000
  },
  "error": null
}

Profile#

Who you are calling as, and with which credential. Handy to check a key or partner token.

GET /v1/user/profile #

any credential

Any valid credential may call it, whatever its scopes. Secrets are never returned.

Response data · 200

  • username string required

    The user's id.

  • auth object | object | object required

    The credential of this request, told apart by kind: session (the signed-in app), api_key or partner (a connected partner app).

Errors

HTTPCodeWhen
401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

GET
curl "https://api.cirrus.trade/v1/user/profile" \
  -H "Authorization: token $CIRRUS_API_KEY"
{
  "status": "success",
  "data": {
    "username": "user-01M3MQZX31VE1ERX4EGEWXDXDC",
    "auth": {
      "kind": "api_key",
      "key_id": "ck_tpVIaOozVzi8aEUO9yH6",
      "name": "Reporting",
      "scopes": [
        "read"
      ],
      "ip_allowlist": [
        "203.0.113.7"
      ]
    }
  },
  "error": null
}

Activity log#

Everything that happened, in plain English: order actions, fills, rejections, triggers, signals and access changes. One trading day (IST) per query; the same records arrive live as stream `activity` messages.

GET /v1/activity #

scope read

One day of the log, newest first, in pages. Filters combine; comma-separated lists match any value.

Query parameters

  • date string optional

    Trading day YYYY-MM-DD (IST); today when absent.

  • account string optional

    Only records touching this account.

  • kind string optional

    Comma-separated ActivityKind values (e.g. place,cancel).

  • category string optional

    Comma-separated ActivityCategory values (e.g. orders,protection).

  • outcome string optional

    Comma-separated ActivityOutcome values (e.g. failed,partial).

  • q string optional

    Case-insensitive text in the summary or trading symbol (at most 64 characters).

  • cursor string optional

    next_cursor of the previous page.

  • limit integer (int32) optional

    Page size, at least 1 (default 50; above 200 means 200).

Response data · 200

  • items object[] required

    Records without timeline (see GET /v1/activity/{id}).

  • next_cursor string required nullable

    Pass as cursor for the next (older) page; null on the last page.

  • source string required

    Where a page was read from: hot (the live store) or archive (the day's archive, for older days).

    Values:
    • hot
    • archive

Errors

HTTPCodeWhen
400—

Unreadable query string (an unknown parameter, or limit not a whole number): a plain-text answer, not the JSON envelope.

401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

422invalid_request

The request is well-formed but not valid; field names the input.

422archive_day_too_large

That day's archived activity is too large to serve at once.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

GET
curl "https://api.cirrus.trade/v1/activity" \
  -H "Authorization: token $CIRRUS_API_KEY"
{
  "status": "success",
  "data": {
    "items": [
      {
        "id": "01M3MQZWW6BZSTR16YB6359R87",
        "username": "user-01M3MQZWS63GTYZTS4FNMT47RV",
        "at": "2026-09-28T19:31:11.875323958Z",
        "kind": "place",
        "category": "orders",
        "source": {
          "type": "app",
          "name": null
        },
        "request_id": "01M3MQZWW3EWEM5BWG2XZRAMS3",
        "instrument": {
          "tradingsymbol": "RELIANCE-EQ",
          "exchange": "NSE",
          "instrument_token": "CT:TEST:RELIANCE"
        },
        "side": "BUY",
        "order_type": "LIMIT",
        "product": "DELIVERY",
        "quantity": 1,
        "price": 2500,
        "trigger_price": null,
        "summary": "Buy 1 RELIANCE-EQ at ₹2,500 (Limit) in 1 account",
        "message": null,
        "outcome": "ok",
        "duration_ms": 2,
        "accounts": [
          {
            "account": "P1",
            "broker": "paper",
            "status": "placed",
            "order_ids": [
              "PAPER-1"
            ],
            "message": null,
            "broker_message": null,
            "duration_ms": 1
          }
        ]
      }
    ],
    "next_cursor": null,
    "source": "hot"
  },
  "error": null
}

GET /v1/activity/{id} #

scope read

One record with its timeline: every step of the orders it touched, oldest first.

Path parameters

  • id string required

    Record id (a ULID).

Response data · 200

  • id string required

    ULID (sorts by creation time).

  • username string required
  • at string (date-time) required

    When the action started.

  • kind string required

    What happened. Serialized snake_case (order_filled, gtt_create...).

    Values:
    • place
    • bracket
    • modify
    • cancel
    • convert
    • order_filled
    • order_partially_filled
    • order_rejected
    • order_cancelled_by_broker
    • external_order
    • order_unknown
    • gtt_create
  • category string required

    Filter group of a record, derived from its kind: orders, protection, signals, account or access.

    Values:
    • orders
    • protection
    • signals
    • account
    • access
  • source object required

    Who or what started an activity.

  • request_id string required nullable

    The x-request-id of the API call that started it, if any.

  • instrument object required nullable
  • side string required nullable

    Order side.

    Values:
    • BUY
    • SELL
  • order_type string required nullable

    MARKET, LIMIT, SL or SL_M.

  • product string required nullable

    MIS, CARRYFORWARD, DELIVERY or MTF.

  • quantity integer (int64) required nullable
  • price number (double) required nullable
  • trigger_price number (double) required nullable
  • summary string required
  • message string required nullable

    Plain reason for records without a per-account result (an ignored signal, a lost session, protection that failed...).

  • outcome string required

    ok (every account succeeded), partial or failed (none did).

    Values:
    • ok
    • partial
    • failed
  • duration_ms integer (int64) required
  • accounts object[] required
  • timeline object[] optional nullable

    Only on detail responses and in the archive.

Errors

HTTPCodeWhen
401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

404activity_not_found

No activity record with this id for the caller.

422archive_day_too_large

That day's archived activity is too large to serve at once.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

GET
curl "https://api.cirrus.trade/v1/activity/<id>" \
  -H "Authorization: token $CIRRUS_API_KEY"
{
  "status": "success",
  "data": {
    "id": "01M3MQZWWD5WSM33JA5AJ680C5",
    "username": "user-01M3MQZWS7J3SMP5VGDZ3X17SS",
    "at": "2026-09-28T19:31:11.882796667Z",
    "kind": "place",
    "category": "orders",
    "source": {
      "type": "app",
      "name": null
    },
    "request_id": "01M3MQZWWAJC9KDSNW93HHA938",
    "instrument": {
      "tradingsymbol": "RELIANCE-EQ",
      "exchange": "NSE",
      "instrument_token": "CT:TEST:RELIANCE"
    },
    "side": "BUY",
    "order_type": "LIMIT",
    "product": "DELIVERY",
    "quantity": 1,
    "price": 2500,
    "trigger_price": null,
    "summary": "Buy 1 RELIANCE-EQ at ₹2,500 (Limit) in 1 account",
    "message": null,
    "outcome": "ok",
    "duration_ms": 2,
    "accounts": [
      {
        "account": "P1",
        "broker": "paper",
        "status": "placed",
        "order_ids": [
          "PAPER-1"
        ],
        "message": null,
        "broker_message": null,
        "duration_ms": 1
      }
    ],
    "timeline": [
      {
        "at": "2026-09-28T19:31:12.992525Z",
        "account": "P1",
        "order_id": "PAPER-1",
        "state": "OPEN",
        "message": null,
        "filled_qty": 0,
        "average_price": null
      },
      {
        "at": "2026-09-28T19:31:13.992525Z",
        "account": "P1",
        "order_id": "PAPER-1",
        "state": "FILLED",
        "message": null,
        "filled_qty": 1,
        "average_price": 2499.5
      }
    ]
  },
  "error": null
}

Streaming#

One WebSocket per client with your live orders and portfolio: a snapshot first, then every change.

  • Needs the read scope. Authenticate with the Authorization header on the upgrade, or with a first message within 5 s. The token may be ck_…:cs_…, token ck_…:cs_… or pa_…. Tokens are never read from the URL.
  • Server messages, each with a per-connection seq: hello (user, accounts[] with account, broker, tag); one snapshot per account and kind (account, kind, data); snapshot_done; then live order_update (account, full order data) and portfolio (account, kind, full data); error (code, message).
  • Kinds: orders, positions, holdings, trades, margins.
  • Events carry full state, never diffs: a missed message is repaired by the next one for the same order or kind. Send {"type":"resync"} for a fresh set of snapshots ending in snapshot_done.
  • The server pings every 15 s. Credentials are re-checked every 10 s, so a revoked key or connection closes the stream.
Close codes
CodeReasonMeaning
4001unauthorizedBad first message or credential, or revoked.
4002auth timeoutNo auth message within 5 s.
1013slow consumerYou fell too far behind; reconnect.
1001server shutdownRolling deploy; reconnect.
1000idleNothing heard from the client for 45 s.

Reconnect with backoff after any close except 4001; each new connection starts with a fresh snapshot.

Node.js
const socket = new WebSocket('wss://api.cirrus.trade/v1/stream');
socket.onopen = () =>
  socket.send(JSON.stringify({ type: 'auth', token: process.env.CIRRUS_API_KEY }));
socket.onmessage = (event) => {
  const message = JSON.parse(event.data);
  // message.type: hello | snapshot | snapshot_done | order_update | portfolio | error
  console.log(message.seq, message.type, message.account);
};
socket.onclose = (event) => console.log('closed', event.code, event.reason);

GET /v1/stream #

scope read

Upgrade to a WebSocket with live orders, portfolio and new activity records. Authenticate on the upgrade (Authorization header) or with an auth message within 5 s.

Response data

No response body.

Errors

HTTPCodeWhen
400—

Not a valid WebSocket upgrade (missing or wrong Connection, Upgrade, Sec-WebSocket-Version or Sec-WebSocket-Key header): plain text.

426—

Upgrade Required: the connection cannot be upgraded (e.g. an HTTP/1.0 or proxied connection that drops the upgrade); plain text.

GET Endpoint
GET https://api.cirrus.trade/v1/stream

Messages#

Server messages

Stream message

hello

First server message after authentication: who is connected and which accounts the stream covers. snapshot messages follow.

Fields

  • seq integer (int64) required

    Per-connection sequence number, from 1, +1 per message. A gap means messages were lost: send resync.

  • type string required

    The message type.

    Values:
    • hello
  • user string required
  • accounts object[] required
Example
{
  "seq": 1,
  "type": "hello",
  "user": "alice",
  "accounts": [
    {
      "account": "ZX1234",
      "broker": "zerodha",
      "tag": "Main"
    }
  ]
}

Stream message

snapshot

The current state of one account's data kind, sent after hello (one per account and kind that has data) and again after resync. Kinds with no data yet are left out. snapshot_done ends the set.

Fields

  • seq integer (int64) required

    Per-connection sequence number (see hello).

  • type string required

    The message type.

    Values:
    • snapshot
  • account string required
  • kind string required

    Snapshot kind: orders (today's order book) or a portfolio kind.

    Values:
    • orders
    • positions
    • holdings
    • trades
    • margins
  • data object[] | object[] | object[] | object[] | MarginRow required

    orders, positions, holdings, trades: a list of rows (may be empty); margins: one object.

Example
{
  "seq": 2,
  "type": "snapshot",
  "account": "ZX1234",
  "kind": "positions",
  "data": [
    {
      "account": "ZX1234",
      "tradingsymbol": "RELIANCE-EQ",
      "product": "MIS",
      "net_qty": 4,
      "buy_qty": 4,
      "sell_qty": 0,
      "buy_value": 9999.2,
      "sell_value": 0,
      "average_price": 2499.8,
      "buy_average_price": 2499.8,
      "sell_average_price": 0,
      "pnl": 6.8,
      "lot_size": 1,
      "exchange": "NSE",
      "instrument_type": "EQ",
      "symbol": "RELIANCE-EQ",
      "expiry": "",
      "strike": 0,
      "option_type": "",
      "ltp": 2501.5,
      "prev_close": 2490,
      "instrument_token": "CT:1:2:RELIANCE-EQ"
    }
  ]
}

Stream message

snapshot_done

Every snapshot has been sent; live messages follow.

Fields

  • seq integer (int64) required

    Per-connection sequence number (see hello).

  • type string required

    The message type.

    Values:
    • snapshot_done
Example
{
  "seq": 3,
  "type": "snapshot_done"
}

Stream message

order_update

Live: an order changed (state, fills, broker id...). Carries the full order, never a diff, so a missed message is repaired by the next one for the same order.

Fields

  • seq integer (int64) required

    Per-connection sequence number (see hello).

  • type string required

    The message type.

    Values:
    • order_update
  • account string required
  • data object required

    An order as it is delivered live: the tracked order plus instrument details and live prices (added when the instrument is known).

    instrument_token and order_tag are also sent under their legacy names, always with the same value; read the primary names.

Example
{
  "seq": 4,
  "type": "order_update",
  "account": "ZX1234",
  "data": {
    "broker_tag": "01K6D8ZQ4X9V2M3N",
    "order_id": "250928000123456",
    "parent_tag": null,
    "username": "alice",
    "account": "ZX1234",
    "broker": "zerodha",
    "tradingsymbol": "RELIANCE-EQ",
    "side": "BUY",
    "order_type": "LIMIT",
    "product": "MIS",
    "quantity": 10,
    "price": 2500,
    "trigger_price": null,
    "state": "PARTIALLY_FILLED",
    "filled_qty": 4,
    "average_price": 2499.8,
    "status_message": null,
    "broker_updated_at": "2026-09-28T04:05:07.120Z",
    "created_at": "2026-09-28T04:05:06.789Z",
    "status_history": [
      {
        "from": "SUBMITTED",
        "to": "OPEN",
        "at": "2026-09-28T04:05:06.789Z",
        "message": null
      },
      {
        "from": "OPEN",
        "to": "PARTIALLY_FILLED",
        "at": "2026-09-28T04:05:07.120Z",
        "message": null
      }
    ],
    "lot_size": 1,
    "exchange": "NSE",
    "instrument_type": "EQ",
    "symbol": "RELIANCE-EQ",
    "expiry": "",
    "strike": 0,
    "option_type": "",
    "ltp": 2501.5,
    "prev_close": 2490,
    "instrument_token": "CT:1:2:RELIANCE-EQ",
    "order_tag": "01K6D8ZQ4X9V2M3N5P7R8S9T0W"
  }
}

Stream message

portfolio

Live: an account's positions, holdings, trades or margins changed. Carries the full current data of that kind, never a diff.

Fields

  • seq integer (int64) required

    Per-connection sequence number (see hello).

  • type string required

    The message type.

    Values:
    • portfolio
  • account string required
  • kind string required

    Portfolio data kinds kept as per-account snapshots.

    Values:
    • positions
    • holdings
    • trades
    • margins
  • data object[] | object[] | object[] | MarginRow required

    positions, holdings, trades: a list of rows (may be empty); margins: one object.

Example
{
  "seq": 5,
  "type": "portfolio",
  "account": "ZX1234",
  "kind": "margins",
  "data": {
    "account": "ZX1234",
    "opening_balance": 100000,
    "available": 90000.8,
    "utilised": 9999.2
  }
}

Stream message

activity

Live: a new activity-log record.

Fields

  • seq integer (int64) required

    Per-connection sequence number (see hello).

  • type string required

    The message type.

    Values:
    • activity
  • data object required

    One activity-log record: something that happened to the user's accounts, in plain English, with the per-account results.

Example
{
  "seq": 6,
  "type": "activity",
  "data": {
    "id": "01K6D8ZQ4XA1B2C3D4E5F6G7H8",
    "username": "alice",
    "at": "2026-09-28T04:05:06.789Z",
    "kind": "place",
    "category": "orders",
    "source": {
      "type": "app",
      "name": null
    },
    "request_id": "req_01K6D8ZQ4X",
    "instrument": {
      "tradingsymbol": "RELIANCE-EQ",
      "exchange": "NSE",
      "instrument_token": "CT:1:2:RELIANCE-EQ"
    },
    "side": "BUY",
    "order_type": "LIMIT",
    "product": "MIS",
    "quantity": 10,
    "price": 2500,
    "trigger_price": null,
    "summary": "Buy 10 RELIANCE-EQ at 2500 in 1 account",
    "message": null,
    "outcome": "ok",
    "duration_ms": 84,
    "accounts": [
      {
        "account": "ZX1234",
        "broker": "zerodha",
        "status": "placed",
        "order_ids": [
          "250928000123456"
        ],
        "message": null,
        "broker_message": null,
        "duration_ms": 84
      }
    ]
  }
}

Stream message

error

The stream hit a problem it cannot recover from on this connection; the connection ends after it. Reconnect.

Fields

  • seq integer (int64) required

    Per-connection sequence number (see hello).

  • type string required

    The message type.

    Values:
    • error
  • code string required

    Why the stream cannot go on: unavailable (live updates could not be set up; reconnect).

    Values:
    • unavailable
  • message string required

    Plain-English explanation.

Example
{
  "seq": 7,
  "type": "error",
  "code": "unavailable",
  "message": "live updates unavailable, please reconnect"
}

Client messages

Stream message

auth

Client -> server, the first frame: authenticates the connection. Not needed when the upgrade request already carried a credential (an Authorization header, or the app's session cookie). Sent later it is ignored. Without it within 5 s the server closes with 4002; a bad credential closes with 4001.

Fields

  • type string required

    The message type.

    Values:
    • auth
  • token string required

    An API key, access token or app session token (the same credentials /v1 routes accept); needs the read scope.

Example
{
  "type": "auth",
  "token": "ck_live_4f1d0c9a7b2e"
}

Stream message

resync

Client -> server: asks for a fresh snapshot (every account's snapshot messages again, then snapshot_done). Use it after a gap in seq. Any other frame the client sends only counts as activity for the idle timeout.

Fields

  • type string required

    The message type.

    Values:
    • resync
Example
{
  "type": "resync"
}

Webhooks#

Register up to 5 HTTPS endpoints of your own and receive signed events as they happen: order updates, trades, account alerts and position changes. Pick the events, and optionally the accounts, per endpoint. Manage them on API access, or with these app-session routes (keys and partner tokens get 403).

Events#

Event types
typeAboutMeaning
order_updateorderAny change to one of your orders (state, fill, broker id). data is the full order: keep the one with the highest filled_qty / latest state.
tradefillThe quantity an order filled since the previous update. The quantity values of an order’s trades add up to its filled_qty.
account_alertaccountsession_expired, relogin_detected, live_updates_unavailable, live_updates_restored, protection_failed, order_rejected, account_setup_incomplete, webhook_disabled.
positionsaccountAn account’s positions changed. At most one per second per account (the latest).
pingtestSent only by the test route.

Delivery#

  • Each event is a POST with a JSON body: id, type, created_at (RFC 3339, UTC) and data. All keys are snake_case.
  • id is the same on every retry and never reused (it is also the webhook-id header): use it to ignore duplicates.
  • Events of one order can arrive out of order. Every order_update carries the full order, so keep the one with the highest filled_qty / latest state.
  • Instruments and orders are identified by instrument_token and order_tag, the same values you use when placing orders.
  • Alerts without an account (webhook_disabled) reach every webhook subscribed to account_alert, whatever its accounts.

Retries and turning off#

Your answer
StatusResultWhat happens
2xxdeliveredAnswer within 5 s. The body is ignored.
408 429 5xx 3xxretriedAlso timeouts and connection errors: retried after 30 s, 2 min, 10 min, 30 min, 1 h and 2 h (7 attempts in about 4 hours), same id and body.
other 4xxfinalNot retried.
  • A webhook whose deliveries keep failing (50 failed attempts in a row, or failing for 24 hours without a success) is turned off: enabled: false, disabled_reason too_many_failures or failing_for_24_hours.
  • You get a webhook_disabled entry in your activity log and an account_alert on your other webhooks. Fix the receiver, then turn it back on (PATCH {"enabled": true} or Resume on API access). Events from while it was off are not replayed.

Payloads#

Webhook body

An order changed

order_update: any change to an order (state, fills, broker id). data is the full order, the same as the stream's order_update.

Every delivery is an HTTPS POST with Content-Type: application/json and a body {id, type, created_at, data} (all keys snake_case), signed per Standard Webhooks (https://www.standardwebhooks.com):

  • webhook-id: the event id (same on every retry; de-duplicate on it);
  • webhook-timestamp: Unix seconds when this attempt was sent;
  • webhook-signature: v1,<base64 HMAC-SHA256> over {webhook-id}.{webhook-timestamp}.{raw body}, keyed with the base64-decoded part of the webhook's whsec_... secret. While a rotated secret is still honoured, one signature per secret is sent, space separated; accept the message when any one matches.

Answer 2xx within 5 s. 408, 429, 5xx, timeouts and network errors are retried after 30 s, 2 min, 10 min, 30 min, 1 h and 2 h (7 attempts); other 4xx answers are not retried. A webhook that keeps failing (50 failures in a row, or failing for 24 h) is turned off and an account_alert (webhook_disabled) goes to the user's remaining webhooks that subscribe to account_alert.

Fields

  • id string required

    Event id, evt_ + 32 hex characters. Deterministic: the same event always has the same id (it is also the webhook-id header), so receivers can de-duplicate on it.

  • type string required

    The message type.

    Values:
    • order_update
  • created_at string (date-time) required

    When it happened (RFC 3339, UTC, milliseconds).

  • data object required

    An order as it is delivered live: the tracked order plus instrument details and live prices (added when the instrument is known).

    instrument_token and order_tag are also sent under their legacy names, always with the same value; read the primary names.

Example
{
  "id": "evt_b9af59b3434f9997a6fc3ece875ca056",
  "type": "order_update",
  "created_at": "2026-09-28T04:05:07.120Z",
  "data": {
    "broker_tag": "01K6D8ZQ4X9V2M3N",
    "order_id": "250928000123456",
    "parent_tag": null,
    "username": "alice",
    "account": "ZX1234",
    "broker": "zerodha",
    "tradingsymbol": "RELIANCE-EQ",
    "side": "BUY",
    "order_type": "LIMIT",
    "product": "MIS",
    "quantity": 10,
    "price": 2500,
    "trigger_price": null,
    "state": "PARTIALLY_FILLED",
    "filled_qty": 4,
    "average_price": 2499.8,
    "status_message": null,
    "broker_updated_at": "2026-09-28T04:05:07.120Z",
    "created_at": "2026-09-28T04:05:06.789Z",
    "status_history": [
      {
        "from": "SUBMITTED",
        "to": "OPEN",
        "at": "2026-09-28T04:05:06.789Z",
        "message": null
      },
      {
        "from": "OPEN",
        "to": "PARTIALLY_FILLED",
        "at": "2026-09-28T04:05:07.120Z",
        "message": null
      }
    ],
    "lot_size": 1,
    "exchange": "NSE",
    "instrument_type": "EQ",
    "symbol": "RELIANCE-EQ",
    "expiry": "",
    "strike": 0,
    "option_type": "",
    "ltp": 2501.5,
    "prev_close": 2490,
    "instrument_token": "CT:1:2:RELIANCE-EQ",
    "order_tag": "01K6D8ZQ4X9V2M3N5P7R8S9T0W"
  }
}

Webhook body

An order filled (fully or partly)

trade: one fill of an order.

Every delivery is an HTTPS POST with Content-Type: application/json and a body {id, type, created_at, data} (all keys snake_case), signed per Standard Webhooks (https://www.standardwebhooks.com):

  • webhook-id: the event id (same on every retry; de-duplicate on it);
  • webhook-timestamp: Unix seconds when this attempt was sent;
  • webhook-signature: v1,<base64 HMAC-SHA256> over {webhook-id}.{webhook-timestamp}.{raw body}, keyed with the base64-decoded part of the webhook's whsec_... secret. While a rotated secret is still honoured, one signature per secret is sent, space separated; accept the message when any one matches.

Answer 2xx within 5 s. 408, 429, 5xx, timeouts and network errors are retried after 30 s, 2 min, 10 min, 30 min, 1 h and 2 h (7 attempts); other 4xx answers are not retried. A webhook that keeps failing (50 failures in a row, or failing for 24 h) is turned off and an account_alert (webhook_disabled) goes to the user's remaining webhooks that subscribe to account_alert.

Fields

  • id string required

    Event id, evt_ + 32 hex characters. Deterministic: the same event always has the same id (it is also the webhook-id header), so receivers can de-duplicate on it.

  • type string required

    The message type.

    Values:
    • trade
  • created_at string (date-time) required

    When it happened (RFC 3339, UTC, milliseconds).

  • data object required

    A fill: the quantity an order filled since its previous update, with the order's cumulative state after it.

Example
{
  "id": "evt_ae7b238d8eb79444044e61522187f090",
  "type": "trade",
  "created_at": "2026-09-28T04:05:07.120Z",
  "data": {
    "account": "ZX1234",
    "broker": "zerodha",
    "order_id": "250928000123456",
    "order_tag": "01K6D8ZQ4X9V2M3N5P7R8S9T0W",
    "instrument_token": "CT:1:2:RELIANCE-EQ",
    "tradingsymbol": "RELIANCE-EQ",
    "side": "BUY",
    "product": "MIS",
    "order_type": "LIMIT",
    "quantity": 4,
    "price": 2499.8,
    "filled_qty": 4,
    "order_quantity": 10,
    "average_price": 2499.8,
    "order_state": "PARTIALLY_FILLED",
    "filled_at": "2026-09-28T04:05:07.120Z"
  }
}

Webhook body

Something about an account needs the user

account_alert: something about an account that needs the user (see WebhookAlertKind).

Every delivery is an HTTPS POST with Content-Type: application/json and a body {id, type, created_at, data} (all keys snake_case), signed per Standard Webhooks (https://www.standardwebhooks.com):

  • webhook-id: the event id (same on every retry; de-duplicate on it);
  • webhook-timestamp: Unix seconds when this attempt was sent;
  • webhook-signature: v1,<base64 HMAC-SHA256> over {webhook-id}.{webhook-timestamp}.{raw body}, keyed with the base64-decoded part of the webhook's whsec_... secret. While a rotated secret is still honoured, one signature per secret is sent, space separated; accept the message when any one matches.

Answer 2xx within 5 s. 408, 429, 5xx, timeouts and network errors are retried after 30 s, 2 min, 10 min, 30 min, 1 h and 2 h (7 attempts); other 4xx answers are not retried. A webhook that keeps failing (50 failures in a row, or failing for 24 h) is turned off and an account_alert (webhook_disabled) goes to the user's remaining webhooks that subscribe to account_alert.

Fields

  • id string required

    Event id, evt_ + 32 hex characters. Deterministic: the same event always has the same id (it is also the webhook-id header), so receivers can de-duplicate on it.

  • type string required

    The message type.

    Values:
    • account_alert
  • created_at string (date-time) required

    When it happened (RFC 3339, UTC, milliseconds).

  • data object required

    Something about an account that needs the user.

Example
{
  "id": "evt_db006f409e35b14728f9b0bd51a7845d",
  "type": "account_alert",
  "created_at": "2026-09-28T04:05:07.120Z",
  "data": {
    "activity_id": "01K6D8ZR0MA1B2C3D4E5F6G7H8",
    "kind": "session_expired",
    "account": "ZX1234",
    "broker": "zerodha",
    "summary": "Zerodha login expired",
    "message": "Log in to Zerodha again to keep trading from this account",
    "outcome": "failed",
    "at": "2026-09-28T04:05:07.120Z"
  }
}

Webhook body

An account's positions changed

positions: an account's positions changed (full list; at most one per second per account).

Every delivery is an HTTPS POST with Content-Type: application/json and a body {id, type, created_at, data} (all keys snake_case), signed per Standard Webhooks (https://www.standardwebhooks.com):

  • webhook-id: the event id (same on every retry; de-duplicate on it);
  • webhook-timestamp: Unix seconds when this attempt was sent;
  • webhook-signature: v1,<base64 HMAC-SHA256> over {webhook-id}.{webhook-timestamp}.{raw body}, keyed with the base64-decoded part of the webhook's whsec_... secret. While a rotated secret is still honoured, one signature per secret is sent, space separated; accept the message when any one matches.

Answer 2xx within 5 s. 408, 429, 5xx, timeouts and network errors are retried after 30 s, 2 min, 10 min, 30 min, 1 h and 2 h (7 attempts); other 4xx answers are not retried. A webhook that keeps failing (50 failures in a row, or failing for 24 h) is turned off and an account_alert (webhook_disabled) goes to the user's remaining webhooks that subscribe to account_alert.

Fields

  • id string required

    Event id, evt_ + 32 hex characters. Deterministic: the same event always has the same id (it is also the webhook-id header), so receivers can de-duplicate on it.

  • type string required

    The message type.

    Values:
    • positions
  • created_at string (date-time) required

    When it happened (RFC 3339, UTC, milliseconds).

  • data object required

    An account's full positions after a change.

Example
{
  "id": "evt_014d471054c08189e7d7d67e7673474a",
  "type": "positions",
  "created_at": "2026-09-28T04:05:07.120Z",
  "data": {
    "account": "ZX1234",
    "positions": [
      {
        "account": "ZX1234",
        "tradingsymbol": "RELIANCE-EQ",
        "product": "MIS",
        "net_qty": 4,
        "buy_qty": 4,
        "sell_qty": 0,
        "buy_value": 9999.2,
        "sell_value": 0,
        "average_price": 2499.8,
        "buy_average_price": 2499.8,
        "sell_average_price": 0,
        "pnl": 6.8,
        "lot_size": 1,
        "exchange": "NSE",
        "instrument_type": "EQ",
        "symbol": "RELIANCE-EQ",
        "expiry": "",
        "strike": 0,
        "option_type": "",
        "ltp": 2501.5,
        "prev_close": 2490,
        "instrument_token": "CT:1:2:RELIANCE-EQ"
      }
    ]
  }
}

Webhook body

Test delivery

ping: a test delivery, sent only when the user asks for one (a new id every time).

Every delivery is an HTTPS POST with Content-Type: application/json and a body {id, type, created_at, data} (all keys snake_case), signed per Standard Webhooks (https://www.standardwebhooks.com):

  • webhook-id: the event id (same on every retry; de-duplicate on it);
  • webhook-timestamp: Unix seconds when this attempt was sent;
  • webhook-signature: v1,<base64 HMAC-SHA256> over {webhook-id}.{webhook-timestamp}.{raw body}, keyed with the base64-decoded part of the webhook's whsec_... secret. While a rotated secret is still honoured, one signature per secret is sent, space separated; accept the message when any one matches.

Answer 2xx within 5 s. 408, 429, 5xx, timeouts and network errors are retried after 30 s, 2 min, 10 min, 30 min, 1 h and 2 h (7 attempts); other 4xx answers are not retried. A webhook that keeps failing (50 failures in a row, or failing for 24 h) is turned off and an account_alert (webhook_disabled) goes to the user's remaining webhooks that subscribe to account_alert.

Fields

  • id string required

    Event id, evt_ + 32 hex characters. Deterministic: the same event always has the same id (it is also the webhook-id header), so receivers can de-duplicate on it.

  • type string required

    The message type.

    Values:
    • ping
  • created_at string (date-time) required

    When it happened (RFC 3339, UTC, milliseconds).

  • data object required

    The test delivery's content.

Example
{
  "id": "evt_789c8513c48f23ed0d0012dde1f53850",
  "type": "ping",
  "created_at": "2026-09-28T04:05:07.120Z",
  "data": {
    "webhook_id": "pb_01k6d8zq4x9v2m3n5p7r8s9t0w",
    "message": "Test event: your endpoint is reachable."
  }
}
Every event
{
  "id": "evt_5f0c2a7d9b1e4c3a8f6d2b0e9a7c5d31",
  "type": "order_update",
  "created_at": "2026-09-28T09:15:03.482Z",
  "data": { }
}
order_update · data
{
  "order_tag": "01J9X3K9ZC4N8Q2W6E5R7T1Y3U",
  "order_id": "250928000123456",
  "account": "AB1234",
  "broker": "zerodha",
  "instrument_token": "<instrument_token>",
  "tradingsymbol": "RELIANCE",
  "exchange": "NSE",
  "side": "BUY",
  "order_type": "LIMIT",
  "product": "MIS",
  "quantity": 10,
  "price": 2500.0,
  "trigger_price": null,
  "state": "PARTIALLY_FILLED",
  "filled_qty": 4,
  "average_price": 2499.5,
  "status_message": null,
  "status_history": [
    {"from": "OPEN", "to": "PARTIALLY_FILLED", "at": "2026-09-28T09:15:03.470Z", "message": null}
  ],
  "ltp": 2500.1
}
trade · data
{
  "account": "AB1234",
  "order_id": "250928000123456",
  "order_tag": "01J9X3K9ZC4N8Q2W6E5R7T1Y3U",
  "instrument_token": "<instrument_token>",
  "tradingsymbol": "RELIANCE",
  "side": "BUY",
  "quantity": 6,
  "price": 2500.83,
  "filled_qty": 10,
  "order_quantity": 10,
  "average_price": 2500.3,
  "order_state": "FILLED",
  "filled_at": "2026-09-28T09:15:04.120Z"
}
account_alert · data
{
  "activity_id": "01J9X3M0A6B7C8D9E0F1G2H3J4",
  "kind": "session_expired",
  "account": "AB1234",
  "broker": "zerodha",
  "summary": "Zerodha login expired: log in to Zerodha again",
  "message": "Orders and updates for this account stop until you log in again",
  "outcome": "failed",
  "at": "2026-09-28T09:20:00.000Z"
}
positions · data
{
  "account": "AB1234",
  "positions": [{
    "instrument_token": "<instrument_token>",
    "tradingsymbol": "RELIANCE",
    "product": "MIS",
    "net_qty": 10,
    "average_price": 2500.3,
    "pnl": -2.0,
    "ltp": 2500.1
  }]
}

POST /v1/postbacks #

app session

Create a webhook. The reply carries the signing secret, shown only once.

Body (JSON)

  • url string required

    The receiver: an https:// URL on a public host (private, loopback and link-local addresses are refused).

  • events string[] required

    Events to send, at least one: order_update, trade, account_alert, positions (ping cannot be subscribed to).

    Values:
    • order_update
    • trade
    • account_alert
    • positions
    • ping
  • accounts string[] optional nullable

    Only these of the user's accounts (non-empty, the user's own); leave out or null for every account, including ones added later.

  • description string optional nullable

    A note for the owner, at most 200 characters.

Response data · 201

  • id string required
  • url string required
  • events string[] required
    Values:
    • order_update
    • trade
    • account_alert
    • positions
    • ping
  • accounts string[] required nullable

    null = every account.

  • description string required nullable
  • enabled boolean required
  • disabled_reason string required nullable

    disabled_by_user, too_many_failures or failing_for_24_hours.

  • disabled_at string required nullable
  • consecutive_failures integer (int32) required
  • last_success_at string required nullable
  • last_failure_at string required nullable
  • previous_secret_expires_at string required nullable

    After a rotation: until then deliveries are also signed with the previous secret.

  • secret_rotated_at string required nullable
  • created_at string required
  • updated_at string required
  • secret string required

    The signing secret (whsec_...): verify each delivery's webhook-signature with it. Store it now: it is never shown again.

  • note string required

    A reminder that the secret is shown only once.

Errors

HTTPCodeWhen
400malformed_json

The body is not JSON, or does not fit the request shape (unknown or missing fields, wrong types).

401unauthorized

No credential, or the credential is invalid, expired or revoked.

403own_session_required

Credentials can only be created from the user's own signed-in session.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

409webhook_limit_reached

The webhook limit is reached; delete one first.

422invalid_request

The request is well-formed but not valid; field names the input.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503webhooks_disabled

Webhooks are not enabled on this server.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

  • 409 webhook_limit_reached at 5 webhooks; 422 invalid_request names the field; 403 own_session_required when support staff act for you.
POST
curl -X POST "https://api.cirrus.trade/v1/postbacks" \
  -H "Authorization: token $CIRRUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "http://127.0.0.1:60716/hooks/orders",
    "events": [
      "order_update",
      "trade"
    ],
    "accounts": [
      "P1"
    ],
    "description": "Order desk"
  }'
{
  "status": "success",
  "data": {
    "id": "pb_01m3mqzx4p5tfpgbyqjgnz52kv",
    "url": "http://127.0.0.1:60716/hooks/orders",
    "events": [
      "order_update",
      "trade"
    ],
    "accounts": [
      "P1"
    ],
    "description": "Order desk",
    "enabled": true,
    "disabled_reason": null,
    "disabled_at": null,
    "consecutive_failures": 0,
    "last_success_at": null,
    "last_failure_at": null,
    "previous_secret_expires_at": null,
    "secret_rotated_at": null,
    "created_at": "2026-09-28T19:31:12.150Z",
    "updated_at": "2026-09-28T19:31:12.150Z",
    "secret": "whsec_txDdiAC9WzSW48p9DLpz8/Y/pqIXJOzIWhiB7agVaC0=",
    "note": "Store the secret now: it is not shown again."
  },
  "error": null
}

GET /v1/postbacks #

app session

Your webhooks (never the secret).

Response data · 200

  • id string required

    Webhook id.

  • url string required

    The receiver: an https:// URL on a public host.

  • events string[] required

    The events it gets (see the document's webhooks section for each body).

    Values:
    • order_update
    • trade
    • account_alert
    • positions
    • ping
  • accounts string[] required nullable

    The accounts whose events it gets; null = every account of the user (including ones added later). Events about the user rather than one account go to every webhook subscribed to them.

  • description string required nullable

    The owner's note.

  • enabled boolean required

    Whether deliveries are sent.

  • disabled_reason string required nullable

    Why it is off: disabled_by_user, too_many_failures (50 failed attempts in a row) or failing_for_24_hours; null while enabled.

  • disabled_at string required nullable
  • consecutive_failures integer (int32) required

    Failed attempts in a row (any success resets it).

  • last_success_at string required nullable
  • last_failure_at string required nullable
  • previous_secret_expires_at string required nullable

    While set, deliveries are also signed with the previous secret (for 24 hours after a rotation).

  • secret_rotated_at string required nullable
  • created_at string required
  • updated_at string required

Errors

HTTPCodeWhen
401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503webhooks_disabled

Webhooks are not enabled on this server.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

GET
curl "https://api.cirrus.trade/v1/postbacks" \
  -H "Authorization: token $CIRRUS_API_KEY"
{
  "status": "success",
  "data": [
    {
      "id": "pb_01m3mqzx4p5tfpgbyqjgnz52kv",
      "url": "http://127.0.0.1:60716/hooks/orders",
      "events": [
        "order_update",
        "trade"
      ],
      "accounts": [
        "P1"
      ],
      "description": "Order desk",
      "enabled": true,
      "disabled_reason": null,
      "disabled_at": null,
      "consecutive_failures": 0,
      "last_success_at": null,
      "last_failure_at": null,
      "previous_secret_expires_at": null,
      "secret_rotated_at": null,
      "created_at": "2026-09-28T19:31:12.150Z",
      "updated_at": "2026-09-28T19:31:12.150Z"
    }
  ],
  "error": null
}

GET /v1/postbacks/{id} #

app session

One webhook (never the secret): its settings and delivery health.

Path parameters

  • id string required

    The webhook's id.

Response data · 200

  • id string required

    Webhook id.

  • url string required

    The receiver: an https:// URL on a public host.

  • events string[] required

    The events it gets (see the document's webhooks section for each body).

    Values:
    • order_update
    • trade
    • account_alert
    • positions
    • ping
  • accounts string[] required nullable

    The accounts whose events it gets; null = every account of the user (including ones added later). Events about the user rather than one account go to every webhook subscribed to them.

  • description string required nullable

    The owner's note.

  • enabled boolean required

    Whether deliveries are sent.

  • disabled_reason string required nullable

    Why it is off: disabled_by_user, too_many_failures (50 failed attempts in a row) or failing_for_24_hours; null while enabled.

  • disabled_at string required nullable
  • consecutive_failures integer (int32) required

    Failed attempts in a row (any success resets it).

  • last_success_at string required nullable
  • last_failure_at string required nullable
  • previous_secret_expires_at string required nullable

    While set, deliveries are also signed with the previous secret (for 24 hours after a rotation).

  • secret_rotated_at string required nullable
  • created_at string required
  • updated_at string required

Errors

HTTPCodeWhen
401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

404webhook_not_found

No webhook with this id for the caller.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503webhooks_disabled

Webhooks are not enabled on this server.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

GET
curl "https://api.cirrus.trade/v1/postbacks/<id>" \
  -H "Authorization: token $CIRRUS_API_KEY"
{
  "status": "success",
  "data": {
    "id": "pb_01m3mqzx4p5tfpgbyqjgnz52kv",
    "url": "http://127.0.0.1:60716/hooks/orders",
    "events": [
      "order_update",
      "trade"
    ],
    "accounts": [
      "P1"
    ],
    "description": "Order desk",
    "enabled": true,
    "disabled_reason": null,
    "disabled_at": null,
    "consecutive_failures": 0,
    "last_success_at": null,
    "last_failure_at": null,
    "previous_secret_expires_at": null,
    "secret_rotated_at": null,
    "created_at": "2026-09-28T19:31:12.150Z",
    "updated_at": "2026-09-28T19:31:12.150Z"
  },
  "error": null
}

PATCH /v1/postbacks/{id} #

app session

Change url, events, accounts, description or enabled. "enabled": false pauses; true turns it back on and resets the failure count; "accounts": null means every account.

Path parameters

  • id string required

    The webhook's id.

Body (JSON)

  • url string optional nullable

    A new receiver URL (same rules as on creation).

  • events string[] optional nullable

    A new event list (replaces the old one; at least one).

    Values:
    • order_update
    • trade
    • account_alert
    • positions
    • ping
  • accounts string[] optional nullable

    A new account list; null goes back to every account.

  • description string optional nullable

    A new note; null or empty removes it.

  • enabled boolean optional nullable

    false turns deliveries off; true turns them back on (also after the webhook was turned off for failing).

Response data · 200

  • id string required

    Webhook id.

  • url string required

    The receiver: an https:// URL on a public host.

  • events string[] required

    The events it gets (see the document's webhooks section for each body).

    Values:
    • order_update
    • trade
    • account_alert
    • positions
    • ping
  • accounts string[] required nullable

    The accounts whose events it gets; null = every account of the user (including ones added later). Events about the user rather than one account go to every webhook subscribed to them.

  • description string required nullable

    The owner's note.

  • enabled boolean required

    Whether deliveries are sent.

  • disabled_reason string required nullable

    Why it is off: disabled_by_user, too_many_failures (50 failed attempts in a row) or failing_for_24_hours; null while enabled.

  • disabled_at string required nullable
  • consecutive_failures integer (int32) required

    Failed attempts in a row (any success resets it).

  • last_success_at string required nullable
  • last_failure_at string required nullable
  • previous_secret_expires_at string required nullable

    While set, deliveries are also signed with the previous secret (for 24 hours after a rotation).

  • secret_rotated_at string required nullable
  • created_at string required
  • updated_at string required

Errors

HTTPCodeWhen
400malformed_json

The body is not JSON, or does not fit the request shape (unknown or missing fields, wrong types).

401unauthorized

No credential, or the credential is invalid, expired or revoked.

403own_session_required

Credentials can only be created from the user's own signed-in session.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

404webhook_not_found

No webhook with this id for the caller.

422invalid_request

The request is well-formed but not valid; field names the input.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503webhooks_disabled

Webhooks are not enabled on this server.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

PATCH
curl -X PATCH "https://api.cirrus.trade/v1/postbacks/<id>" \
  -H "Authorization: token $CIRRUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "events": [
      "order_update",
      "trade",
      "positions"
    ],
    "accounts": null,
    "enabled": false
  }'
{
  "status": "success",
  "data": {
    "id": "pb_01m3mqzx4p5tfpgbyqjgnz52kv",
    "url": "http://127.0.0.1:60716/hooks/orders",
    "events": [
      "order_update",
      "trade",
      "positions"
    ],
    "accounts": null,
    "description": "Order desk",
    "enabled": false,
    "disabled_reason": "disabled_by_user",
    "disabled_at": "2026-09-28T19:31:12.156Z",
    "consecutive_failures": 0,
    "last_success_at": null,
    "last_failure_at": null,
    "previous_secret_expires_at": null,
    "secret_rotated_at": null,
    "created_at": "2026-09-28T19:31:12.150Z",
    "updated_at": "2026-09-28T19:31:12.156Z"
  },
  "error": null
}

DELETE /v1/postbacks/{id} #

app session

Delete a webhook. Events stop at once.

Path parameters

  • id string required

    The webhook's id.

Response data · 200

  • id string required
  • deleted boolean required

    Always true.

Errors

HTTPCodeWhen
401unauthorized

No credential, or the credential is invalid, expired or revoked.

403own_session_required

Credentials can only be created from the user's own signed-in session.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

404webhook_not_found

No webhook with this id for the caller.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503webhooks_disabled

Webhooks are not enabled on this server.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

DELETE
curl -X DELETE "https://api.cirrus.trade/v1/postbacks/<id>" \
  -H "Authorization: token $CIRRUS_API_KEY"
{
  "status": "success",
  "data": {
    "id": "pb_01m3mqzx58t3ezceq1ecb498yv",
    "deleted": true
  },
  "error": null
}

POST /v1/postbacks/{id}/rotate-secret #

app session

A new secret, shown once. For 24 hours every delivery carries two signatures (new and old); previous_secret_expires_at says when the old one stops.

Path parameters

  • id string required

    The webhook's id.

Response data · 200

  • id string required
  • url string required
  • events string[] required
    Values:
    • order_update
    • trade
    • account_alert
    • positions
    • ping
  • accounts string[] required nullable

    null = every account.

  • description string required nullable
  • enabled boolean required
  • disabled_reason string required nullable

    disabled_by_user, too_many_failures or failing_for_24_hours.

  • disabled_at string required nullable
  • consecutive_failures integer (int32) required
  • last_success_at string required nullable
  • last_failure_at string required nullable
  • previous_secret_expires_at string required nullable

    After a rotation: until then deliveries are also signed with the previous secret.

  • secret_rotated_at string required nullable
  • created_at string required
  • updated_at string required
  • secret string required

    The signing secret (whsec_...): verify each delivery's webhook-signature with it. Store it now: it is never shown again.

  • note string required

    A reminder that the secret is shown only once.

Errors

HTTPCodeWhen
401unauthorized

No credential, or the credential is invalid, expired or revoked.

403own_session_required

Credentials can only be created from the user's own signed-in session.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

404webhook_not_found

No webhook with this id for the caller.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503webhooks_disabled

Webhooks are not enabled on this server.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

POST
curl -X POST "https://api.cirrus.trade/v1/postbacks/<id>/rotate-secret" \
  -H "Authorization: token $CIRRUS_API_KEY"
{
  "status": "success",
  "data": {
    "id": "pb_01m3mqzx58t3ezceq1ecb498yv",
    "url": "http://127.0.0.1:60710/hooks/alerts",
    "events": [
      "account_alert"
    ],
    "accounts": null,
    "description": null,
    "enabled": true,
    "disabled_reason": null,
    "disabled_at": null,
    "consecutive_failures": 0,
    "last_success_at": null,
    "last_failure_at": null,
    "previous_secret_expires_at": "2026-09-29T19:31:12.173Z",
    "secret_rotated_at": "2026-09-28T19:31:12.173Z",
    "created_at": "2026-09-28T19:31:12.168Z",
    "updated_at": "2026-09-28T19:31:12.173Z",
    "secret": "whsec_fqwXMrqAHOadNV3Pw3Lq83azTavNzXwNNP+D8P6jU6Q=",
    "note": "Store the secret now: it is not shown again."
  },
  "error": null
}

POST /v1/postbacks/{id}/test #

app session

Send a signed ping now and report the result.

Path parameters

  • id string required

    The webhook's id.

Response data · 200

  • event_id string required

    The ping event's id (its webhook-id header).

  • delivered boolean required

    Whether the receiver answered 2xx.

  • status_code integer (int32) required nullable

    The receiver's HTTP status; null when no answer came.

  • duration_ms integer (int64) required

    How long the attempt took.

  • error_kind string required nullable

    Why it failed: timeout, connect, blocked_address, http_status, redirect...; null when delivered.

Errors

HTTPCodeWhen
401unauthorized

No credential, or the credential is invalid, expired or revoked.

403own_session_required

Credentials can only be created from the user's own signed-in session.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

404webhook_not_found

No webhook with this id for the caller.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503webhooks_disabled

Webhooks are not enabled on this server.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

POST
curl -X POST "https://api.cirrus.trade/v1/postbacks/<id>/test" \
  -H "Authorization: token $CIRRUS_API_KEY"
{
  "status": "success",
  "data": {
    "event_id": "evt_bbde86f7815e23802cf72965f007b693",
    "delivered": true,
    "status_code": 200,
    "duration_ms": 0,
    "error_kind": null
  },
  "error": null
}

GET /v1/postbacks/{id}/deliveries #

app session

Delivery attempts of the last 7 days, newest first. Payloads are not stored.

Path parameters

  • id string required

    The webhook's id.

Query parameters

  • limit integer optional

    Page size, 1-200 (default 50).

  • cursor string optional

    next_cursor of the previous page.

Response data · 200

  • deliveries object[] required
  • next_cursor string required nullable

    Pass as cursor for the next page; null on the last page.

Errors

HTTPCodeWhen
400—

The query string does not parse (e.g. limit is not a number); plain-text reply.

401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

404webhook_not_found

No webhook with this id for the caller.

422invalid_request

The request is well-formed but not valid; field names the input.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503webhooks_disabled

Webhooks are not enabled on this server.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

GET
curl "https://api.cirrus.trade/v1/postbacks/<id>/deliveries" \
  -H "Authorization: token $CIRRUS_API_KEY"
{
  "status": "success",
  "data": {
    "deliveries": [
      {
        "id": "01M3MQZX5CX39YTHPWEFADXTQN",
        "event_id": "evt_bbde86f7815e23802cf72965f007b693",
        "type": "ping",
        "attempt": 1,
        "outcome": "delivered",
        "status_code": 200,
        "duration_ms": 0,
        "error_kind": null,
        "at": "2026-09-28T19:31:12.172Z"
      }
    ],
    "next_cursor": null
  },
  "error": null
}

Verifying webhooks#

Every delivery is signed with your webhook's secret. Check the signature before trusting an event.

Headers
HeaderValueMeaning
webhook-idstringThe event id; the same on every retry.
webhook-timestampunix sWhen this attempt was signed.
webhook-signaturev1,<base64>During a secret rotation two, space separated. Accept when any one matches.
content-typestringapplication/json.
user-agentstring<product>-Webhooks/1.0.
  • The signature is base64(HMAC-SHA256(key, "{webhook-id}.{webhook-timestamp}.{raw body}")), where key is the base64-decoded part of your secret after whsec_.
  • Verify against the raw body, before any JSON parsing. Accept when any listed signature matches; compare in constant time.
  • Reject timestamps more than 5 minutes from your clock, and ignore an id you already handled.
  • Deliveries follow the Standard Webhooks specification, so its libraries (Node, Python, Go, Java, Ruby, PHP, Rust) verify them as they are.

Check your code against the specification's test vector: it must produce exactly that webhook-signature.

Rotating the secret#

  • Rotate on API access or with POST /v1/postbacks/{id}/rotate-secret: the new secret is shown once.
  • For the next 24 hours every delivery carries two signatures, new and old, so switch your receiver any time that day without missing an event. previous_secret_expires_at says when the old one stops.
Node.js
const crypto = require("crypto");

function verifyWebhook(secret, headers, rawBody) {
  const id = headers["webhook-id"];
  const timestamp = headers["webhook-timestamp"];
  const signatures = headers["webhook-signature"] || "";
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = crypto
    .createHmac("sha256", key)
    .update(`${id}.${timestamp}.${rawBody}`)
    .digest("base64");
  return signatures.split(" ").some((entry) => {
    const [version, signature = ""] = entry.split(",");
    return (
      version === "v1" &&
      signature.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
    );
  });
}

// Express: verify the raw body, then parse it.
// app.post("/hooks/trading", express.raw({ type: "application/json" }), (req, res) => {
//   if (!verifyWebhook(process.env.WEBHOOK_SECRET, req.headers, req.body.toString("utf8")))
//     return res.sendStatus(400);
//   const event = JSON.parse(req.body);
//   res.sendStatus(204);
// });
Python
import base64, hashlib, hmac, time

def verify_webhook(secret: str, headers: dict, raw_body: bytes) -> bool:
    webhook_id = headers["webhook-id"]
    timestamp = headers["webhook-timestamp"]
    signatures = headers.get("webhook-signature", "")
    if abs(time.time() - int(timestamp)) > 300:
        return False
    key = base64.b64decode(secret.removeprefix("whsec_"))
    signed = f"{webhook_id}.{timestamp}.".encode() + raw_body
    expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
    for entry in signatures.split(" "):
        version, _, signature = entry.partition(",")
        if version == "v1" and hmac.compare_digest(signature, expected):
            return True
    return False
Test vector
secret     whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw
webhook-id msg_p5jXN8AQM9LWM0D4loKWxJek
timestamp  1614265330
body       {"test": 2432232314}
signature  v1,g0hM9SsE+OTPJTGt/tmIKtSyZlE3uFJELVlNIOLJ1OE=

Signal URLs#

A signal URL lets TradingView, Chartink or Kuberhunt place orders for one of your strategies: `POST /v1/hooks/{provider}/{secret}`. The URL is the credential, so keep it private. Create one from a strategy in the app; manage them on API access or with these app-session routes.

TradingView#

  • Set the alert’s webhook URL to your signal URL and its message to the JSON on the right. entry places the strategy’s legs; exit closes every leg the entries placed.
  • secret is the body_secret from issue or rotate; missing or wrong answers 403. The same body within 5 seconds is a duplicate. Legs, product and accounts come from the strategy.

Chartink#

  • stocks and trigger_prices are comma-separated (JSON arrays also work), read in the same order. Each stock becomes one LIMIT order at its trigger price plus the strategy’s buffer, on the tick.
  • At most 50 stocks per alert; unknown stocks and stocks without a price are skipped. Chartink cannot sign requests: the URL is the only credential. The same alert within 10 minutes is a duplicate.

Kuberhunt#

  • Signed with the webhook secret set on the strategy; the timestamp must be within ±300 s. event_id is deduplicated for 24 hours and event_seq must increase per reco_id.
  • reco.activated enters (refused when older than 120 s); reco.sl_hit, reco.target_reached, reco.exit exit; reco.invalidated, reco.cancelled cancel; reco.trail_updated, reco.updated move the stop-loss or target.

Signal bodies#

Signal body

TradingViewAlert

TradingView alert body, sent to POST /v1/hooks/tradingview/{secret}. Put it in the alert's "Message" box as JSON. The URL secret and the body secret must both match. Other fields are ignored. The same alert arriving twice within 5 s is treated as one.

Fields

  • type string required

    TradingView alert type: entry places the strategy's legs, exit closes what the strategy's entries placed. Case and surrounding spaces are ignored ("Entry " works); any other type is acknowledged and ignored.

    Values:
    • entry
    • exit
  • secret string required

    The body secret (tvs_...) shown once when the URL was issued. A missing or wrong one is refused with 403.

Example
{
  "type": "entry",
  "secret": "tvs_3kq8v1n0x7c2m5z9"
}

Signal body

ChartinkAlert

Chartink scanner alert body, sent by Chartink to POST /v1/hooks/chartink/{secret}. Chartink cannot sign requests, so the URL secret is the credential. Fields other than these are ignored (scan_url, webhook_url, ...). The same alert (same triggered_at, stocks, scan_name, alert_name) arriving again within 10 minutes is treated as one.

Fields

  • stocks string | (string | number (double))[] required

    Stock symbols (NSE, else BSE), at most 50. Unknown symbols are skipped; the rest are still traded.

  • trigger_prices string | (string | number (double))[] optional nullable

    Trigger price of each stock, in the same order as stocks. A missing or zero price skips that stock.

  • triggered_at string optional nullable

    When the scan fired, as Chartink formats it ("2:34 pm").

  • scan_name string optional nullable
  • alert_name string optional nullable
Example
{
  "stocks": "SBIN,RELIANCE",
  "trigger_prices": "523.4,2890",
  "triggered_at": "2:34 pm",
  "scan_name": "Breakouts",
  "alert_name": "Breakouts"
}

Signal body

KuberhuntEvent

Kuberhunt event body, sent to POST /v1/hooks/kuberhunt/{secret} or to the pre-v1 URL POST /kuberhunt/execute-signal/{token} (same body, same headers; retired per strategy once its v1 URL has received an accepted delivery, and answered with Deprecation / Sunset headers).

Headers: X-Kuberhunt-Timestamp (Unix seconds, within ±300 s of now) and X-Kuberhunt-Signature: v1=<hex HMAC-SHA256> over {timestamp}.{raw body}, keyed with the strategy's Kuberhunt webhook secret. Every refusal is answered 200 (Kuberhunt retries any non-2xx); only a failure worth retrying is answered 503. The same event_id is handled once; an event_seq not above the last one seen for the recommendation is skipped as stale.

Fields

  • event string required

    Kuberhunt event name. reco.activated opens a position (entry); reco.sl_hit, reco.target_reached and reco.exit close it; reco.invalidated / reco.cancelled close it too; reco.updated / reco.trail_updated move its stop-loss / target; reco.scheduled, reco.pending, webhook.test and reco.t1_reached ... reco.t5_reached are acknowledged without action. Any other name is acknowledged and ignored.

    Values:
    • reco.activated
    • reco.scheduled
    • reco.pending
    • webhook.test
    • reco.trail_updated
    • reco.updated
    • reco.t1_reached
    • reco.t2_reached
    • reco.t3_reached
    • reco.t4_reached
    • reco.t5_reached
    • reco.sl_hit
  • event_id string | number (double) required

    Delivery id (a string or a number); repeats are ignored for 24 h.

  • event_seq string | number (double) optional nullable

    Per-recommendation sequence (an integer, a whole number, or a numeric string); anything else disables the ordering check.

  • timestamp string optional nullable

    When the event was produced (RFC 3339). An entry older than 120 s is skipped.

  • reco object required

    The recommendation an event is about. Prices may be numbers or numeric strings. instrument_token or its legacy name is accepted; send one (both with different values is refused).

  • transition any optional

    Provider detail about the state change; kept in the history, not used.

  • meta any optional

    Provider metadata; kept in the history, not used.

Example
{
  "event": "reco.activated",
  "event_id": "evt-1",
  "event_seq": 1,
  "timestamp": "2026-09-28T04:05:06.789+00:00",
  "reco": {
    "reco_id": "r1",
    "instrument_token": "CT:1:2:RELIANCE-EQ",
    "action": "BUY",
    "product": "INTRADAY",
    "lower_price": 2490,
    "entry_price": 2500,
    "stop_loss_price": 2450,
    "target_price": 2600
  }
}

Replies and limits#

Replies
StatusCaseWhen
200receivedThe body says what happened. Most rejections are also 200, so providers that retry every non-2xx do not retry them; the deliveries list has the outcome.
403TradingViewThe body secret is missing or wrong.
404unknown URLUnknown, revoked or rotated URL, or the wrong provider. Every such case is the same.
429rate limitedOver 10 requests a second on this URL. Not processed.
503retryA store was briefly unavailable, or a Kuberhunt entry / close can be retried.
Headers on every reply
HeaderValueMeaning
X-RateLimit-LimitintegerRequests allowed per second on this URL (10).
X-RateLimit-RemainingintegerLeft in the current second.
Retry-AftersecondsOn 429 only.
X-Request-IdstringMatches request_id in the deliveries list.

Rotate, deliveries and test#

  • Rotating gives a new URL with the same id and stops the old one at once on every server; revoking stops it for good.
  • Deliveries of the last 7 days show each outcome with a plain reason and the order ids placed. Payloads are never stored.
  • The dry run (test) checks a payload exactly like a real delivery and lists the orders it would place, without placing anything or changing any state.
Delivery outcome
outcomeOrdersMeaning
placedordersEvery order it asked for was placed.
partially_placedordersSome orders were placed and some refused; reason gives the counts.
failednoneOrders were attempted and none was placed (also when a store was briefly unavailable).
processednoneHandled; no new orders needed (for example a stop-loss or target moved).
ignorednoneValid, but the strategy’s rules meant nothing to do: paused, test mode, no open position, entries used up for the day…
refusednoneWrong secret or signature, timestamp outside the window, or an unreadable payload.
duplicatenoneThe same alert arrived again, or an older event after a newer one.
rate_limitednoneOver the URL’s rate limit. Recorded at most once a minute per URL.
POST Send a TradingView signal
curl -X POST "https://api.cirrus.trade/v1/hooks/tradingview/<url-secret>" \
  -H "Content-Type: application/json" \
  -d '{"type": "entry", "secret": "tvs_xxxx"}'
TradingView alert message
// Alert message (JSON). "secret" is the body secret issued with the URL.
{"type": "entry", "secret": "tvs_…"}
{"type": "exit",  "secret": "tvs_…"}
Chartink alert
{
  "stocks": "SBIN,RELIANCE",
  "trigger_prices": "812.5,2950.1",
  "triggered_at": "10:15 am",
  "scan_name": "Breakouts",
  "scan_url": "breakouts",
  "alert_name": "Breakout alert"
}
Kuberhunt delivery
X-Kuberhunt-Timestamp: 1759050000
X-Kuberhunt-Signature: v1=<hex HMAC-SHA256(secret, "{timestamp}.{raw body}")>

{
  "event": "reco.activated",
  "event_id": "evt_01J8…",
  "event_seq": 3,
  "timestamp": "2026-09-28T09:20:00Z",
  "reco": {
    "reco_id": "r-123",
    "action": "BUY",
    "product": "INTRADAY",
    "entry_price": 2500.0,
    "stop_loss_price": 2450.0,
    "target_price": 2600.0
  }
}

POST /v1/hooks/{provider}/{secret} #

no auth

The URL the provider calls. No Authorization: the secret in the URL is the credential (plus the TradingView body secret or the Kuberhunt signature). The body depends on the provider (see Signal bodies above).

Path parameters

  • kind string required

    The provider the URL was issued for.

    Values:
    • kuberhunt
    • tradingview
    • chartink
  • secret string required

    The URL secret (wh_...), from path when the URL was issued.

Headers

  • X-Kuberhunt-Signature string optional

    Kuberhunt: v1=<hex HMAC-SHA256> of {timestamp}.{raw body}, keyed with the strategy's Kuberhunt webhook secret.

  • X-Kuberhunt-Timestamp string optional

    Kuberhunt: Unix seconds, within 300 s of now.

Body (JSON)

  • type string required when TradingViewAlert

    TradingView alert type: entry places the strategy's legs, exit closes what the strategy's entries placed. Case and surrounding spaces are ignored ("Entry " works); any other type is acknowledged and ignored.

    Values:
    • entry
    • exit
  • secret string required when TradingViewAlert

    The body secret (tvs_...) shown once when the URL was issued. A missing or wrong one is refused with 403.

  • stocks string | (string | number (double))[] required when ChartinkAlert

    Stock symbols (NSE, else BSE), at most 50. Unknown symbols are skipped; the rest are still traded.

  • trigger_prices string | (string | number (double))[] optional nullable when ChartinkAlert

    Trigger price of each stock, in the same order as stocks. A missing or zero price skips that stock.

  • triggered_at string optional nullable when ChartinkAlert

    When the scan fired, as Chartink formats it ("2:34 pm").

  • scan_name string optional nullable when ChartinkAlert
  • alert_name string optional nullable when ChartinkAlert
  • event string required when KuberhuntEvent

    Kuberhunt event name. reco.activated opens a position (entry); reco.sl_hit, reco.target_reached and reco.exit close it; reco.invalidated / reco.cancelled close it too; reco.updated / reco.trail_updated move its stop-loss / target; reco.scheduled, reco.pending, webhook.test and reco.t1_reached ... reco.t5_reached are acknowledged without action. Any other name is acknowledged and ignored.

    Values:
    • reco.activated
    • reco.scheduled
    • reco.pending
    • webhook.test
    • reco.trail_updated
    • reco.updated
    • reco.t1_reached
    • reco.t2_reached
    • reco.t3_reached
    • reco.t4_reached
    • reco.t5_reached
    • reco.sl_hit
  • event_id string | number (double) required when KuberhuntEvent

    Delivery id (a string or a number); repeats are ignored for 24 h.

  • event_seq string | number (double) optional nullable when KuberhuntEvent

    Per-recommendation sequence (an integer, a whole number, or a numeric string); anything else disables the ordering check.

  • timestamp string optional nullable when KuberhuntEvent

    When the event was produced (RFC 3339). An entry older than 120 s is skipped.

  • reco object required when KuberhuntEvent

    The recommendation an event is about. Prices may be numbers or numeric strings. instrument_token or its legacy name is accepted; send one (both with different values is refused).

  • transition any optional when KuberhuntEvent

    Provider detail about the state change; kept in the history, not used.

  • meta any optional when KuberhuntEvent

    Provider metadata; kept in the history, not used.

Response data · 200

  • message string required

    Every message a signal URL answers with status 200. The delivery was received; nothing more is promised (what it came to is in the URL's delivery log and the activity log).

    • Webhook received: handled (orders placed, closed, updated, or skipped by a strategy rule).
    • Duplicate: the same alert arrived twice; the copy was skipped.
    • Stale: an older Kuberhunt event arrived after a newer one.
    • Signal is disabled / Strategy paused: the strategy is paused.
    • Strategy not found: the strategy no longer exists.
    • Invalid Payload / Invalid payload / Invalid JSON / Empty body: the body could not be read.
    • Invalid signature, Timestamp out of window, Webhook secret missing: Kuberhunt signature checks.
    • Only entry and exit signals are processed: TradingView type was neither.
    • Too many stocks in one alert, No valid stocks found, No accounts configured: Chartink alerts that could not be traded.
    • instrument_token ... differ; send one, order_tag ... differ; send one, exits ... differ; send one: a field was sent under both its names with different values.
    • Invalid ID: the pre-v1 Kuberhunt URL's token is invalid.
    Values:
    • Webhook received
    • Duplicate
    • Stale
    • Signal is disabled
    • Strategy paused
    • Strategy not found
    • Invalid Payload
    • Invalid payload
    • Invalid JSON
    • Empty body
    • Invalid signature
    • Timestamp out of window

Errors

HTTPCodeWhen
403—

A TradingView alert with the wrong body secret

403oms_not_enabled

Order management is not enabled for this user.

404—

An unknown or revoked URL

429—

Over the URL's rate limit

503—

Temporarily unavailable; send the same delivery again later.

  • Replies are made for the providers, not the REST envelope: 200 means received, whatever it came to (see the URL’s deliveries and the activity log); only 503 asks for the same delivery again.
POST
curl -X POST "https://api.cirrus.trade/v1/hooks/<kind>/<secret>" \
  -H "Content-Type: application/json" \
  -d '{
    "stocks": "TESTSU",
    "trigger_prices": "100.03",
    "triggered_at": "9:20 am",
    "scan_name": "Breakouts",
    "alert_name": "Breakout alert"
  }'
{
  "message": "Webhook received"
}

GET /v1/hooks/endpoints #

app session

Your live URLs: endpoint_id, kind, strategy_id, created_at, has_body_secret. Never the secrets.

Response data · 200

  • endpoint_id string required

    Public id (we_...), kept across rotations.

  • kind string required

    The signal provider a URL is for: tradingview (TradingView alerts), chartink (Chartink scanner alerts) or kuberhunt (Kuberhunt recommendation events).

    Values:
    • kuberhunt
    • tradingview
    • chartink
  • strategy_id string required

    The strategy the URL's signals trade.

  • created_at string required

    When this URL (its current secret) was issued (RFC 3339).

  • has_body_secret boolean required

    TradingView: alerts must carry the body secret.

Errors

HTTPCodeWhen
401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

GET
curl "https://api.cirrus.trade/v1/hooks/endpoints" \
  -H "Authorization: token $CIRRUS_API_KEY"
{
  "status": "success",
  "data": [
    {
      "endpoint_id": "we_K64Mxu5q5wJHhyDd",
      "kind": "tradingview",
      "strategy_id": "nifty-breakout-01M3MQZX2RZXFZ4BA4K46TY12A",
      "created_at": "2026-09-28T19:31:12.090922+00:00",
      "has_body_secret": true
    }
  ],
  "error": null
}

POST /v1/hooks/endpoints #

app session

Issue a URL for a strategy. Any older URL for the same strategy stops working.

Body (JSON)

  • kind string required

    The provider that will call the URL.

    Values:
    • kuberhunt
    • tradingview
    • chartink
  • strategy_id string required

    The strategy's id: 1-64 letters, digits, - or _.

Response data · 201

  • endpoint_id string required

    Public id (we_...), kept across rotations.

  • kind string required

    The signal provider a URL is for: tradingview (TradingView alerts), chartink (Chartink scanner alerts) or kuberhunt (Kuberhunt recommendation events).

    Values:
    • kuberhunt
    • tradingview
    • chartink
  • strategy_id string required
  • created_at string required

    When this URL was issued (RFC 3339).

  • has_body_secret boolean required

    TradingView: alerts must carry body_secret.

  • path string required

    The path to give the provider, prefixed with the API origin (/v1/hooks/{kind}/wh_...). It is the credential: keep it private.

  • body_secret string required nullable

    TradingView only: put "secret": "<this>" (tvs_...) in the alert message. Null for other providers.

  • note string required

    A reminder that the URL is shown only once.

Errors

HTTPCodeWhen
400malformed_json

The body is not JSON, or does not fit the request shape (unknown or missing fields, wrong types).

401unauthorized

No credential, or the credential is invalid, expired or revoked.

403own_session_required

Credentials can only be created from the user's own signed-in session.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

422invalid_request

The request is well-formed but not valid; field names the input.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

POST
curl -X POST "https://api.cirrus.trade/v1/hooks/endpoints" \
  -H "Authorization: token $CIRRUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "tradingview",
    "strategy_id": "nifty-breakout-01M3MQZX2RZXFZ4BA4K46TY12A"
  }'
{
  "status": "success",
  "data": {
    "endpoint_id": "we_K64Mxu5q5wJHhyDd",
    "kind": "tradingview",
    "strategy_id": "nifty-breakout-01M3MQZX2RZXFZ4BA4K46TY12A",
    "created_at": "2026-09-28T19:31:12.090922+00:00",
    "has_body_secret": true,
    "path": "/v1/hooks/tradingview/wh_j3eXpOAgQQ8ZbCwZLudcszn3KWc4V1sC3rIHcXTV2Le",
    "body_secret": "tvs_7QaFOF9HjMwawCKqUxiSl5nnIMYI4Sue",
    "note": "Keep this URL private: anyone with it can send signals. Issuing a new one revokes this."
  },
  "error": null
}

POST /v1/hooks/endpoints/{endpoint_id}/rotate #

app session

A new URL (and TradingView body secret) with the same endpoint_id, shown once. The old URL stops working at once on every server.

Path parameters

  • endpoint_id string required

    The signal URL's endpoint_id.

Response data · 200

  • endpoint_id string required

    Public id (we_...), kept across rotations.

  • kind string required

    The signal provider a URL is for: tradingview (TradingView alerts), chartink (Chartink scanner alerts) or kuberhunt (Kuberhunt recommendation events).

    Values:
    • kuberhunt
    • tradingview
    • chartink
  • strategy_id string required
  • created_at string required

    When this URL was issued (RFC 3339).

  • has_body_secret boolean required

    TradingView: alerts must carry body_secret.

  • path string required

    The path to give the provider, prefixed with the API origin (/v1/hooks/{kind}/wh_...). It is the credential: keep it private.

  • body_secret string required nullable

    TradingView only: put "secret": "<this>" (tvs_...) in the alert message. Null for other providers.

  • note string required

    A reminder that the URL is shown only once.

Errors

HTTPCodeWhen
401unauthorized

No credential, or the credential is invalid, expired or revoked.

403own_session_required

Credentials can only be created from the user's own signed-in session.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

404endpoint_not_found

No signal URL with this id (or this secret) for the caller.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

POST
curl -X POST "https://api.cirrus.trade/v1/hooks/endpoints/<endpoint_id>/rotate" \
  -H "Authorization: token $CIRRUS_API_KEY"
{
  "status": "success",
  "data": {
    "endpoint_id": "we_K64Mxu5q5wJHhyDd",
    "kind": "tradingview",
    "strategy_id": "nifty-breakout-01M3MQZX2RZXFZ4BA4K46TY12A",
    "created_at": "2026-09-28T19:31:12.102166+00:00",
    "has_body_secret": true,
    "path": "/v1/hooks/tradingview/wh_PcykPFU3b0Mb1P1W7tKaTqbO1VHULzPXd92rqxKZL8l",
    "body_secret": "tvs_VCszf9naN0LnUt2SujREKjNTtsac8dHg",
    "note": "Give the provider this new URL now: the old one no longer works. It is shown only once."
  },
  "error": null
}

DELETE /v1/hooks/endpoints/{endpoint_id} #

app session

Revoke the URL. It stops working at once; later calls on this id return 404.

Path parameters

  • endpoint_id string required

    The signal URL's endpoint_id.

Response data · 200

  • endpoint_id string required
  • revoked boolean required

    Always true.

Errors

HTTPCodeWhen
401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

404endpoint_not_found

No signal URL with this id (or this secret) for the caller.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

DELETE
curl -X DELETE "https://api.cirrus.trade/v1/hooks/endpoints/<endpoint_id>" \
  -H "Authorization: token $CIRRUS_API_KEY"
{
  "status": "success",
  "data": {
    "endpoint_id": "we_K64Mxu5q5wJHhyDd",
    "revoked": true
  },
  "error": null
}

GET /v1/hooks/endpoints/{endpoint_id}/deliveries #

app session

What the URL received in the last 7 days, newest first. Payloads are never stored.

Path parameters

  • endpoint_id string required

    The signal URL's endpoint_id.

Query parameters

  • limit integer optional

    Page size, at least 1 (default 50; above 200 counts as 200).

  • cursor string optional

    next_cursor of the previous page.

Response data · 200

  • items object[] required
  • next_cursor string required nullable

    Pass as cursor for the next page; null on the last page.

Errors

HTTPCodeWhen
400—

The query string does not parse (unknown parameter, limit not a number); plain-text reply.

401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

404endpoint_not_found

No signal URL with this id (or this secret) for the caller.

422invalid_request

The request is well-formed but not valid; field names the input.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

GET
curl "https://api.cirrus.trade/v1/hooks/endpoints/<endpoint_id>/deliveries" \
  -H "Authorization: token $CIRRUS_API_KEY"
{
  "status": "success",
  "data": {
    "items": [
      {
        "delivery_id": "01M3MQZX4AT9WSGE09S4CWXHSX",
        "at": "2026-09-28T19:31:12.138Z",
        "provider": "chartink",
        "outcome": "rate_limited",
        "reason": "too many alerts in a short time; extra ones were refused",
        "orders_placed": 0,
        "order_ids": [],
        "http_status": 429,
        "request_id": "01M3MQZX48WJ21J1E118EVG2RD"
      },
      {
        "delivery_id": "01M3MQZX48QJW2DXVP6XD85QRZ",
        "at": "2026-09-28T19:31:12.136Z",
        "provider": "chartink",
        "outcome": "duplicate",
        "reason": "the same alert arrived twice; the copy was skipped",
        "orders_placed": 0,
        "order_ids": [],
        "http_status": 200,
        "request_id": "01M3MQZX46RRRBQ3D201CTZH6P"
      },
      {
        "delivery_id": "01M3MQZX46B5BP5AXHF8YS4HE0",
        "at": "2026-09-28T19:31:12.134Z",
        "provider": "chartink",
        "outcome": "duplicate",
        "reason": "the same alert arrived twice; the copy was skipped",
        "orders_placed": 0,
        "order_ids": [],
        "http_status": 200,
        "request_id": "01M3MQZX45JJPVMR5GRH6Z1T77"
      },
      {
        "delivery_id": "01M3MQZX455MFC6KCR2TWPM699",
        "at": "2026-09-28T19:31:12.133Z",
        "provider": "chartink",
        "outcome": "placed",
        "reason": null,
        "orders_placed": 1,
        "order_ids": [
          "PAPER-1"
        ],
        "http_status": 200,
        "request_id": "01M3MQZX40EW38WXHT3CDF1KAE"
      }
    ],
    "next_cursor": null
  },
  "error": null
}

POST /v1/hooks/endpoints/{endpoint_id}/test #

app session

Dry run: the same checks, parsing, strategy rules and sizing as a real delivery, without placing orders or touching any state. An empty body checks a sample alert for the provider.

Path parameters

  • endpoint_id string required

    The signal URL's endpoint_id.

Headers

  • X-Kuberhunt-Signature string optional

    Kuberhunt only: v1=<hex HMAC-SHA256>; without it the signature is not checked (a warning says so).

  • X-Kuberhunt-Timestamp string optional

    Kuberhunt only: Unix seconds the signature was made at.

Body (JSON)

  • type string required when TradingViewAlert

    TradingView alert type: entry places the strategy's legs, exit closes what the strategy's entries placed. Case and surrounding spaces are ignored ("Entry " works); any other type is acknowledged and ignored.

    Values:
    • entry
    • exit
  • secret string required when TradingViewAlert

    The body secret (tvs_...) shown once when the URL was issued. A missing or wrong one is refused with 403.

  • stocks string | (string | number (double))[] required when ChartinkAlert

    Stock symbols (NSE, else BSE), at most 50. Unknown symbols are skipped; the rest are still traded.

  • trigger_prices string | (string | number (double))[] optional nullable when ChartinkAlert

    Trigger price of each stock, in the same order as stocks. A missing or zero price skips that stock.

  • triggered_at string optional nullable when ChartinkAlert

    When the scan fired, as Chartink formats it ("2:34 pm").

  • scan_name string optional nullable when ChartinkAlert
  • alert_name string optional nullable when ChartinkAlert
  • event string required when KuberhuntEvent

    Kuberhunt event name. reco.activated opens a position (entry); reco.sl_hit, reco.target_reached and reco.exit close it; reco.invalidated / reco.cancelled close it too; reco.updated / reco.trail_updated move its stop-loss / target; reco.scheduled, reco.pending, webhook.test and reco.t1_reached ... reco.t5_reached are acknowledged without action. Any other name is acknowledged and ignored.

    Values:
    • reco.activated
    • reco.scheduled
    • reco.pending
    • webhook.test
    • reco.trail_updated
    • reco.updated
    • reco.t1_reached
    • reco.t2_reached
    • reco.t3_reached
    • reco.t4_reached
    • reco.t5_reached
    • reco.sl_hit
  • event_id string | number (double) required when KuberhuntEvent

    Delivery id (a string or a number); repeats are ignored for 24 h.

  • event_seq string | number (double) optional nullable when KuberhuntEvent

    Per-recommendation sequence (an integer, a whole number, or a numeric string); anything else disables the ordering check.

  • timestamp string optional nullable when KuberhuntEvent

    When the event was produced (RFC 3339). An entry older than 120 s is skipped.

  • reco object required when KuberhuntEvent

    The recommendation an event is about. Prices may be numbers or numeric strings. instrument_token or its legacy name is accepted; send one (both with different values is refused).

  • transition any optional when KuberhuntEvent

    Provider detail about the state change; kept in the history, not used.

  • meta any optional when KuberhuntEvent

    Provider metadata; kept in the history, not used.

Response data · 200

  • valid boolean required

    True when errors is empty: a real delivery would be acted on.

  • errors string[] required

    Why a real delivery would place nothing, in plain words.

  • warnings string[] required

    What it would skip, or could not check (e.g. an unsigned Kuberhunt test, a sample payload, test mode on the strategy).

  • would_place object[] required

    The orders it would place.

Errors

HTTPCodeWhen
401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

404endpoint_not_found

No signal URL with this id (or this secret) for the caller.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

  • A Kuberhunt signature is checked only if you send the X-Kuberhunt-Signature and X-Kuberhunt-Timestamp headers; a TradingView body secret only if you send your own payload.
POST
curl -X POST "https://api.cirrus.trade/v1/hooks/endpoints/<endpoint_id>/test" \
  -H "Authorization: token $CIRRUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "stocks": "TESTSU",
    "trigger_prices": "100.03",
    "triggered_at": "9:30 am",
    "scan_name": "Breakouts",
    "alert_name": "Breakout alert"
  }'
{
  "status": "success",
  "data": {
    "valid": true,
    "errors": [],
    "warnings": [],
    "would_place": [
      {
        "account": "P1",
        "symbol": "TESTSU-EQ",
        "exchange": "NSE",
        "side": "BUY",
        "qty": 3,
        "order_type": "LIMIT",
        "price": 100.55,
        "product": "MIS"
      }
    ]
  },
  "error": null
}

API keys#

Create and revoke the API keys scripts use (`Authorization: token <key_id>:<secret>`). Most people use API access in the app; these are the routes behind it.

GET /v1/api-keys #

app session

Your active API keys (revoked ones are not listed). Secrets are never returned.

Response data · 200

  • key_id string required

    Public key id (ck_...): the part before : in Authorization: token <key_id>:<key_secret>.

  • name string required

    The owner's label for the key.

  • scopes string[] required

    What the key may do.

    Values:
    • read
    • orders
    • triggers
  • ip_allowlist string[] required

    IP addresses the key may be used from; empty = any.

  • created_at string required

    When the key was created (RFC 3339).

  • last_used_at string required nullable

    When the key was last used (RFC 3339, updated at most once a minute); null when never used.

Errors

HTTPCodeWhen
401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

GET
curl "https://api.cirrus.trade/v1/api-keys" \
  -H "Authorization: token $CIRRUS_API_KEY"
{
  "status": "success",
  "data": [
    {
      "key_id": "ck_MpGKDrd64Jc2XT2CyQGW",
      "name": "Trading bot",
      "scopes": [
        "read",
        "orders"
      ],
      "ip_allowlist": [
        "203.0.113.7"
      ],
      "created_at": "2026-09-28T19:31:11.878729+00:00",
      "last_used_at": null
    }
  ],
  "error": null
}

POST /v1/api-keys #

app session

Create a key with the chosen scopes (and optionally an IP allowlist). The secret is in this reply only. At most 10 active keys.

Body (JSON)

  • name string required

    A label for the key, 1-64 characters (trimmed).

  • scopes string[] required

    What the key may do; at least one.

    Values:
    • read
    • orders
    • triggers
  • ip_allowlist string[] optional

    IP addresses (v4 or v6) the key may be used from, at most 20; empty or absent = any.

Response data · 201

  • key_id string required

    Public key id (ck_...).

  • name string required
  • scopes string[] required
    Values:
    • read
    • orders
    • triggers
  • ip_allowlist string[] required

    IP addresses the key may be used from; empty = any.

  • created_at string required

    When the key was created (RFC 3339).

  • last_used_at string required nullable

    Always null for a new key.

  • api_secret string required

    The key secret (cs_...). Store it now: it is never shown again. Authenticate with Authorization: token <key_id>:<api_secret>.

  • note string required

    A reminder that the secret is shown only once.

Errors

HTTPCodeWhen
400malformed_json

The body is not JSON, or does not fit the request shape (unknown or missing fields, wrong types).

401unauthorized

No credential, or the credential is invalid, expired or revoked.

403own_session_required

Credentials can only be created from the user's own signed-in session.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

422invalid_request

The request is well-formed but not valid; field names the input.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

  • An operator acting as the user gets 403 own_session_required.
POST
curl -X POST "https://api.cirrus.trade/v1/api-keys" \
  -H "Authorization: token $CIRRUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Trading bot",
    "scopes": [
      "read",
      "orders"
    ],
    "ip_allowlist": [
      "203.0.113.7"
    ]
  }'
{
  "status": "success",
  "data": {
    "key_id": "ck_MpGKDrd64Jc2XT2CyQGW",
    "name": "Trading bot",
    "scopes": [
      "read",
      "orders"
    ],
    "ip_allowlist": [
      "203.0.113.7"
    ],
    "created_at": "2026-09-28T19:31:11.878729+00:00",
    "last_used_at": null,
    "api_secret": "cs_9vnItUrl5NTPwetZAFJRnRRJup9U5KC8lsHvSmu8",
    "note": "Store the secret now: it is not shown again."
  },
  "error": null
}

DELETE /v1/api-keys/{key_id} #

app session

Revoke a key at once: every server refuses it within a few seconds.

Path parameters

  • key_id string required

    The key's key_id (ck_...).

Response data · 200

  • key_id string required
  • revoked boolean required

    Always true.

Errors

HTTPCodeWhen
401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

404api_key_not_found

No API key with this id for the caller.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

DELETE
curl -X DELETE "https://api.cirrus.trade/v1/api-keys/<key_id>" \
  -H "Authorization: token $CIRRUS_API_KEY"
{
  "status": "success",
  "data": {
    "key_id": "ck_MpGKDrd64Jc2XT2CyQGW",
    "revoked": true
  },
  "error": null
}

Partner apps & connections#

Register OAuth apps, rotate their secrets, and manage which apps may act for you. The consent screen uses the two `/v1/oauth` routes. See OAuth for partner apps for the whole flow.

GET /v1/partner-apps #

app session

The partner apps you registered, oldest first, disabled ones included.

Response data · 200

  • client_id string required

    OAuth client id (cp_...).

  • owner string required

    Username of the user who registered the app.

  • name string required

    Name shown to users on the consent screen.

  • verified boolean required

    Whether the operator has confirmed that the app's name belongs to its owner; never set through the API. Unverified apps are flagged on the consent screen.

  • logo_url string required nullable

    HTTPS URL of the app's logo.

  • redirect_uris string[] required

    Exact redirect URIs a user may be sent back to.

  • scopes string[] required

    The most a user can grant this partner.

    Values:
    • read
    • orders
    • triggers
  • created_at string required

    RFC 3339 time of registration.

  • disabled boolean required

    true once the owner disabled the app (its tokens stop working).

Errors

HTTPCodeWhen
401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

GET
curl "https://api.cirrus.trade/v1/partner-apps" \
  -H "Authorization: token $CIRRUS_API_KEY"
{
  "status": "success",
  "data": [
    {
      "client_id": "cp_YtJe2ihlan6rj5LcW7KI",
      "owner": "user-01M3NVXMEG6YJNY2F10FCRF2Q9",
      "name": "Charts Co",
      "verified": false,
      "logo_url": "https://partner.example/logo.png",
      "redirect_uris": [
        "https://partner.example/callback"
      ],
      "scopes": [
        "read",
        "orders"
      ],
      "created_at": "2026-09-29T05:59:06.498541+00:00",
      "disabled": false
    }
  ],
  "error": null
}

POST /v1/partner-apps #

app session

Register an OAuth app (at most 10 active). The reply carries client_secret once. New apps are unverified.

Body (JSON)

  • name string required

    Name shown to users on the consent screen (1-80 characters).

  • redirect_uris string[] required

    1-10 redirect URIs, matched exactly (no prefixes, no wildcards): https://, or http://localhost / http://127.0.0.1 for development; no fragment, at most 512 characters.

  • scopes string[] required

    The most a user can grant the app (at least one).

    Values:
    • read
    • orders
    • triggers
  • logo_url string optional nullable

    HTTPS URL of the app's logo.

Response data · 201

  • client_id string required

    OAuth client id (cp_...).

  • owner string required

    Username of the user who registered the app.

  • name string required

    Name shown to users on the consent screen.

  • verified boolean required

    Always false for a new app (see PartnerApp.verified).

  • logo_url string required nullable

    HTTPS URL of the app's logo.

  • redirect_uris string[] required

    Exact redirect URIs a user may be sent back to.

  • scopes string[] required

    The most a user can grant this partner.

    Values:
    • read
    • orders
    • triggers
  • created_at string required

    RFC 3339 time of registration.

  • disabled boolean required

    Always false for a new app.

  • client_secret string required

    The client secret (cps_...). Shown only in this response: store it now. Only its keyed hash is kept.

  • note string required

    A reminder to store the secret.

Errors

HTTPCodeWhen
400malformed_json

The body is not JSON, or does not fit the request shape (unknown or missing fields, wrong types).

401unauthorized

No credential, or the credential is invalid, expired or revoked.

403own_session_required

Credentials can only be created from the user's own signed-in session.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

422invalid_request

The request is well-formed but not valid; field names the input.

422invalid_scope

OAuth: a requested scope is unknown or not allowed for the app.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

POST
curl -X POST "https://api.cirrus.trade/v1/partner-apps" \
  -H "Authorization: token $CIRRUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Charts Co",
    "redirect_uris": [
      "https://partner.example/callback"
    ],
    "scopes": [
      "read",
      "orders"
    ],
    "logo_url": "https://partner.example/logo.png"
  }'
{
  "status": "success",
  "data": {
    "client_id": "cp_YtJe2ihlan6rj5LcW7KI",
    "owner": "user-01M3NVXMEG6YJNY2F10FCRF2Q9",
    "name": "Charts Co",
    "verified": false,
    "logo_url": "https://partner.example/logo.png",
    "redirect_uris": [
      "https://partner.example/callback"
    ],
    "scopes": [
      "read",
      "orders"
    ],
    "created_at": "2026-09-29T05:59:06.498541+00:00",
    "disabled": false,
    "client_secret": "cps_hXhW1Bnl4McDJuAMpoGqQVEYPG6b4We9lATUXNKJ7CqigI1y",
    "note": "Store the client secret now: it is not shown again."
  },
  "error": null
}

POST /v1/partner-apps/{client_id}/rotate-secret #

app session

A new client secret, shown once. The previous secret keeps working until previous_secret_expires_at (24 hours).

Path parameters

  • client_id string required

    The app's client id (cp_...).

Response data · 200

  • client_id string required
  • client_secret string required

    The new secret (cps_...); store it now, it is never shown again.

  • previous_secret_expires_at string (date-time) required

    Until when the previous secret is still accepted.

Errors

HTTPCodeWhen
401unauthorized

No credential, or the credential is invalid, expired or revoked.

403own_session_required

Credentials can only be created from the user's own signed-in session.

404unknown_app

Unknown or disabled partner app.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

POST
curl -X POST "https://api.cirrus.trade/v1/partner-apps/<client_id>/rotate-secret" \
  -H "Authorization: token $CIRRUS_API_KEY"
{
  "status": "success",
  "data": {
    "client_id": "cp_ddSuxziFxtMzFwliLeK7",
    "client_secret": "cps_MOIPAboqpyrIceK6zY3QpdbtBbZMqbhlorqoAcLY4ysPBJCb",
    "previous_secret_expires_at": "2026-09-30T05:59:06Z"
  },
  "error": null
}

DELETE /v1/partner-apps/{client_id} #

app session

Disable one of your apps: every token of every connected user stops working at once. Cannot be undone.

Path parameters

  • client_id string required

    The app's client id (cp_...).

Response data · 200

  • client_id string required
  • disabled boolean required

    Always true.

Errors

HTTPCodeWhen
401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

404unknown_app

Unknown or disabled partner app.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

DELETE
curl -X DELETE "https://api.cirrus.trade/v1/partner-apps/<client_id>" \
  -H "Authorization: token $CIRRUS_API_KEY"
{
  "status": "success",
  "data": {
    "client_id": "cp_YtJe2ihlan6rj5LcW7KI",
    "disabled": true
  },
  "error": null
}

POST /v1/oauth/authorize #

app session

Consent screen, step 2: the user approved. Returns the redirect URL carrying a single-use code (60 seconds).

Body (JSON)

  • client_id string required

    The partner app's client id.

  • redirect_uri string required

    One of the app's registered redirect URIs, exactly.

  • scope string required

    Requested scopes, space separated (read, orders, triggers); each must be allowed for the app.

  • state string optional nullable

    The partner's opaque state, returned unchanged (URL-encoded) in the redirect.

  • code_challenge string required

    PKCE challenge: base64url (no padding) SHA-256 of the partner's code_verifier (43 characters).

  • code_challenge_method string required

    Must be S256 (plain PKCE is refused).

Response data · 200

  • redirect_url string required

    The registered redirect_uri with code (single use, valid 60 seconds) and, when one was sent, state (URL-encoded) appended to its query, e.g. https://partner.example/cb?code=oc_...&state=xyz.

Errors

HTTPCodeWhen
400malformed_json

The body is not JSON, or does not fit the request shape (unknown or missing fields, wrong types).

401unauthorized

No credential, or the credential is invalid, expired or revoked.

403own_session_required

Credentials can only be created from the user's own signed-in session.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

404unknown_app

Unknown or disabled partner app.

422invalid_request

The request is well-formed but not valid; field names the input.

422invalid_scope

OAuth: a requested scope is unknown or not allowed for the app.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

POST
curl -X POST "https://api.cirrus.trade/v1/oauth/authorize" \
  -H "Authorization: token $CIRRUS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "cp_XwdEn1dFZHLEjiQdFZrN",
    "redirect_uri": "https://partner.example/callback",
    "scope": "read orders",
    "state": "af0ifjsldkj",
    "code_challenge": "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM",
    "code_challenge_method": "S256"
  }'
{
  "status": "success",
  "data": {
    "redirect_url": "https://partner.example/callback?code=oc_kKvlHo8I44VhkZzyfq28JnXWS2NjrIp05txf7DFj&state=af0ifjsldkj"
  },
  "error": null
}

GET /v1/connections #

app session

The partner apps you connected (live connections only).

Response data · 200

  • client_id string required

    The partner app's client id.

  • app_name string required nullable

    The app's name; null when the app has since been disabled.

  • verified boolean required

    Whether the app is verified (see PartnerApp.verified).

  • scopes string[] required

    Scopes the user granted.

    Values:
    • read
    • orders
    • triggers
  • connected_at string required

    RFC 3339 time of the first connection.

  • updated_at string required

    RFC 3339 time of the latest grant.

Errors

HTTPCodeWhen
401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

GET
curl "https://api.cirrus.trade/v1/connections" \
  -H "Authorization: token $CIRRUS_API_KEY"
{
  "status": "success",
  "data": [
    {
      "client_id": "cp_5C5uooATLM4C2m4Vd41e",
      "app_name": "Charts Co",
      "verified": false,
      "scopes": [
        "read",
        "orders"
      ],
      "connected_at": "2026-09-29T05:59:06.502090+00:00",
      "updated_at": "2026-09-29T05:59:06.502090+00:00"
    }
  ],
  "error": null
}

DELETE /v1/connections/{client_id} #

app session

Disconnect an app: its access tokens stop at once and its refresh tokens are deleted.

Path parameters

  • client_id string required

    The app's client id (cp_...).

Response data · 200

  • client_id string required
  • revoked boolean required

    Always true.

Errors

HTTPCodeWhen
401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

404connection_not_found

No connected partner app with this client id.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

DELETE
curl -X DELETE "https://api.cirrus.trade/v1/connections/<client_id>" \
  -H "Authorization: token $CIRRUS_API_KEY"
{
  "status": "success",
  "data": {
    "client_id": "cp_5C5uooATLM4C2m4Vd41e",
    "revoked": true
  },
  "error": null
}

Shared portfolios#

Portfolios other users shared with you: who shares what, one screen across every shared account, and each view limited to what the share allows.

GET /v1/shared #

app session

The shares granted to you: owner, scopes and accounts. Use owner_username in the routes below.

Response data · 200

  • owner_username string required

    Username of the owner (the path segment {owner} of the other shared routes).

  • owner_name string required

    The owner's display name (their username when none is set).

  • scopes string[] required

    What the owner shared.

    Values:
    • login
    • funds
    • positions
    • holdings
    • open_orders
    • order_history
  • account_scope string | string[] required

    Which of the owner's accounts the share covers: "all" or a list of account ids.

Errors

HTTPCodeWhen
401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

GET
curl "https://api.cirrus.trade/v1/shared" \
  -H "Authorization: token $CIRRUS_API_KEY"
{
  "status": "success",
  "data": [
    {
      "owner_username": "owner-01M3MQZX0M1RYSP2CE8YHM2N65",
      "owner_name": "Asha Owner",
      "scopes": [
        "login",
        "funds",
        "positions",
        "holdings",
        "open_orders",
        "order_history"
      ],
      "account_scope": "all"
    }
  ],
  "error": null
}

GET /v1/shared/consolidated #

app session

One card per owner and shared account (balances, P&L, counts), plus every shared position.

Response data · 200

  • owners object[] required
  • positions object[] required

    Every shared position, across owners and accounts.

Errors

HTTPCodeWhen
401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

GET
curl "https://api.cirrus.trade/v1/shared/consolidated" \
  -H "Authorization: token $CIRRUS_API_KEY"
{
  "status": "success",
  "data": {
    "owners": [
      {
        "owner_username": "owner-01M3MQZX0M1RYSP2CE8YHM2N65",
        "owner_name": "Asha Owner",
        "scopes": [
          "login",
          "funds",
          "positions",
          "holdings",
          "open_orders",
          "order_history"
        ],
        "account_scope": "all",
        "accounts": [
          {
            "client_id": "O1",
            "has_login": true,
            "has_funds": true,
            "has_positions": true,
            "opening_balance": 100000,
            "usable_balance": 80000,
            "utilisation": 20000,
            "pnl": 120.5,
            "positions_count": 1,
            "holdings_count": 1,
            "broker": "paper",
            "account_tag": "main",
            "last_login_at": "1790623872",
            "logged_in_today": true
          },
          {
            "client_id": "O2",
            "has_login": true,
            "has_funds": true,
            "has_positions": true,
            "opening_balance": null,
            "usable_balance": null,
            "utilisation": null,
            "pnl": null,
            "positions_count": 0,
            "holdings_count": 0,
            "broker": "paper",
            "account_tag": null,
            "last_login_at": null,
            "logged_in_today": false
          }
        ]
      }
    ],
    "positions": [
      {
        "account": "O1",
        "tradingsymbol": "RELIANCE-EQ",
        "product": "MIS",
        "net_qty": 5,
        "buy_qty": 5,
        "sell_qty": 0,
        "buy_value": 12500,
        "sell_value": 0,
        "average_price": 2500,
        "buy_average_price": 2500,
        "sell_average_price": 0,
        "pnl": 120.5,
        "lot_size": 1,
        "exchange": "NSE",
        "instrument_type": "EQUITY",
        "symbol": "RELIANCE",
        "expiry": "",
        "strike": 0,
        "option_type": "",
        "ltp": 2500,
        "prev_close": 0,
        "owner_username": "owner-01M3MQZX0M1RYSP2CE8YHM2N65",
        "owner_name": "Asha Owner",
        "instrument_token": "CT:TEST:RELIANCE"
      }
    ]
  },
  "error": null
}

POST /v1/shared/consolidated/refresh #

app session

Ask for fresh data for every owner sharing with you (at most once per owner every 10 seconds).

Response data · 202

  • owners integer (int64) required

    Owners sharing with the caller.

  • dispatched integer (int64) required

    Owners actually refreshed (the others were refreshed moments ago).

Errors

HTTPCodeWhen
401unauthorized

No credential, or the credential is invalid, expired or revoked.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

POST
curl -X POST "https://api.cirrus.trade/v1/shared/consolidated/refresh" \
  -H "Authorization: token $CIRRUS_API_KEY"
{
  "status": "success",
  "data": {
    "owners": 1,
    "dispatched": 1
  },
  "error": null
}

POST /v1/shared/{owner}/refresh #

app session

Ask for fresh data for one owner. Within 10 seconds of the last refresh nothing is asked and requested is null.

Path parameters

  • owner string required

    The owner's username (owner_username from GET /v1/shared).

Response data · 202

  • requested integer (int64) required nullable

    Accounts asked to refresh; null when the owner was refreshed moments ago (within 10 seconds, by anyone) and nothing was asked.

Errors

HTTPCodeWhen
401unauthorized

No credential, or the credential is invalid, expired or revoked.

403not_shared

That user has not shared this data with the caller.

403oms_not_enabled

Order management is not enabled for this user.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

POST
curl -X POST "https://api.cirrus.trade/v1/shared/<owner>/refresh" \
  -H "Authorization: token $CIRRUS_API_KEY"
{
  "status": "success",
  "data": {
    "requested": 2
  },
  "error": null
}

GET /v1/shared/{owner}/{view} #

app session

One view of the owner’s shared accounts. Each view needs its scope: accounts → login, margins → funds, the rest by name.

Path parameters

  • owner string required

    The owner's username (owner_username from GET /v1/shared).

  • view string required

    What to read: accounts (needs login), positions, holdings, margins (needs funds), open_orders, order_history.

    Values:
    • accounts
    • positions
    • holdings
    • margins
    • open_orders
    • order_history

Response data · 200 · SharedAccounts

  • accounts object[] required

Response data · 200 · SharedPortfolio

  • accounts object[] required
  • missing object[] required

Errors

HTTPCodeWhen
401unauthorized

No credential, or the credential is invalid, expired or revoked.

403not_shared

That user has not shared this data with the caller.

403oms_not_enabled

Order management is not enabled for this user.

403forbidden

The credential may not use this route (missing scope, app-session-only route, IP not allowed).

422invalid_request

The request is well-formed but not valid; field names the input.

429rate_limited

Too many requests for this credential (or too many failed attempts); retry later.

503service_unavailable

A dependency is temporarily unavailable, or the feature is off on this server; retry later.

GET
curl "https://api.cirrus.trade/v1/shared/<owner>/<view>" \
  -H "Authorization: token $CIRRUS_API_KEY"
{
  "status": "success",
  "data": {
    "accounts": [
      {
        "client_id": "O1",
        "broker": "paper",
        "account_tag": "main",
        "last_login_at": "1790623872",
        "logged_in_today": true
      },
      {
        "client_id": "O2",
        "broker": "paper",
        "account_tag": null,
        "last_login_at": null,
        "logged_in_today": false
      }
    ]
  },
  "error": null
}

Values reference#

Every set of allowed values in requests, responses, stream messages and webhook bodies, from the OpenAPI spec. Values are case-sensitive; send them exactly as listed. Where a field is documented, its values also show next to it.

40 of 40 value sets

  • AccountReadiness 4 values

    Whether the account can trade now: ready, login_required (log in to the broker again), setup_incomplete (a required field is missing, see missing) or unsupported (broker not supported for trading).

    AccountReadiness values:
    • ready
    • login_required
    • setup_incomplete
    • unsupported
    Used in 2 places
    • GET /v1/accounts response items[].status
    • GET /v1/accounts/{account} response status
  • AccountStatus 5 values

    What happened in one account: placed (accepted by the broker and working), done (completed), rejected (refused by the broker or exchange), failed (could not be sent, or did not complete) or unknown (no answer in time; the broker may or may not have it).

    AccountStatus values:
    • placed
    • done
    • rejected
    • failed
    • unknown
    Used in 3 places
    • GET /v1/activity response items[].accounts[].status
    • GET /v1/activity/{id} response accounts[].status
    • stream activity data.accounts[].status
  • AccountUpdates 3 values

    How order updates arrive right now: stream (the broker's live stream), postback (the broker calls back) or polling (the order book is read every few seconds).

    AccountUpdates values:
    • stream
    • postback
    • polling
    Used in 2 places
    • GET /v1/accounts response items[].updates
    • GET /v1/accounts/{account} response updates
  • ActivityCategory 5 values

    Filter group of a record, derived from its kind: orders, protection, signals, account or access.

    ActivityCategory values:
    • orders
    • protection
    • signals
    • account
    • access
    Used in 3 places
    • GET /v1/activity response items[].category
    • GET /v1/activity/{id} response category
    • stream activity data.category
  • ActivityKind 43 values

    What happened. Serialized snake_case (order_filled, gtt_create...).

    ActivityKind values:
    • place
    • bracket
    • modify
    • cancel
    • convert
    • order_filled
    • order_partially_filled
    • order_rejected
    • order_cancelled_by_broker
    • external_order
    • order_unknown
    • gtt_create
    Used in 3 places
    • GET /v1/activity response items[].kind
    • GET /v1/activity/{id} response kind
    • stream activity data.kind
  • ActivityOutcome 3 values

    ok (every account succeeded), partial or failed (none did).

    ActivityOutcome values:
    • ok
    • partial
    • failed
    Used in 4 places
    • GET /v1/activity response items[].outcome
    • GET /v1/activity/{id} response outcome
    • webhook Something about an account needs the user data.outcome
    • stream activity data.outcome
  • ActivityPageSource 2 values

    Where a page was read from: hot (the live store) or archive (the day's archive, for older days).

    ActivityPageSource values:
    • hot
    • archive
    Used in 1 place
    • GET /v1/activity response source
  • ActivitySourceType 7 values

    Who or what started it: app (signed-in app session), api_key, partner, signal (a signal URL), trigger (a stop-loss / target trigger), broker (the broker or exchange) or system (housekeeping).

    ActivitySourceType values:
    • app
    • api_key
    • partner
    • signal
    • trigger
    • broker
    • system
    Used in 3 places
    • GET /v1/activity response items[].source.type
    • GET /v1/activity/{id} response source.type
    • stream activity data.source.type
  • BracketOrderKind 2 values

    BO (bracket: entry + stop-loss + target) or CO (cover: entry + stop-loss).

    BracketOrderKind values:
    • BO
    • CO
    Used in 2 places
    • POST /v1/orders/bracket request kind
    • PATCH /v1/orders/bracket/{order_id} request kind
  • ErrorCode 39 values

    Stable error code: branch on this, never on the message.

    codeHTTP statusmeaning
    malformed_json400The body is not JSON, or does not fit the request shape (unknown or missing fields, wrong types).
    invalid_query400The query string does not fit the route's parameters.
    idempotency_key_required400The Idempotency-Key header is missing (use a new UUID per order request).
    invalid_idempotency_key400The Idempotency-Key header is not 1-128 printable characters.
    unauthorized401No credential, or the credential is invalid, expired or revoked.
    forbidden403The credential may not use this route (missing scope, app-session-only route, IP not allowed).
    own_session_required403Credentials can only be created from the user's own signed-in session.
    oms_not_enabled403Order management is not enabled for this user.
    not_shared403That user has not shared this data with the caller.
    account_not_found404No linked broker account with this id for the caller.
    instrument_not_found404No instrument with this token (unknown or expired).
    trigger_not_found404No trigger with this id for the caller.
    api_key_not_found404No API key with this id for the caller.
    connection_not_found404No connected partner app with this client id.
    unknown_app404Unknown or disabled partner app.
    endpoint_not_found404No signal URL with this id (or this secret) for the caller.
    webhook_not_found404No webhook with this id for the caller.
    activity_not_found404No activity record with this id for the caller.
    request_in_progress409A request with this Idempotency-Key is still being processed.
    trigger_busy409The trigger changed while it was being updated; retry.
    trigger_fired409The trigger already fired; its exit is being handled.
    broker_held409The protection is held by the broker; change the broker order instead.
    webhook_limit_reached409The webhook limit is reached; delete one first.
    invalid_request422The request is well-formed but not valid; field names the input.
    idempotency_key_reused422This Idempotency-Key was already used with a different request.
    archive_day_too_large422That day's archived activity is too large to serve at once.
    invalid_client422OAuth: unknown partner app, or wrong client credentials.
    invalid_grant422OAuth: the authorization code or refresh token is invalid, used or expired.
    unsupported_grant_type400OAuth token endpoint: grant_type is missing or not supported.
    slow_down429OAuth token endpoint: too many failed attempts from this source; retry after Retry-After.
    invalid_scope422OAuth: a requested scope is unknown or not allowed for the app.
    temporarily_unavailable503OAuth: the authorization server is temporarily unavailable.
    rate_limited429Too many requests for this credential (or too many failed attempts); retry later.
    broker_refused502The broker refused the change.
    service_unavailable503A dependency is temporarily unavailable, or the feature is off on this server; retry later.
    instruments_unavailable503The instrument master is not loaded yet; retry shortly.
    trigger_worker_down503No trigger exit worker is running; nothing was created.
    webhooks_disabled503Webhooks are not enabled on this server.
    unavailable503Stream only: live updates are unavailable; reconnect.
    ErrorCode values:
    • malformed_json
    • invalid_query
    • idempotency_key_required
    • invalid_idempotency_key
    • unauthorized
    • forbidden
    • own_session_required
    • oms_not_enabled
    • not_shared
    • account_not_found
    • instrument_not_found
    • trigger_not_found
  • ErrorStatus 1 values

    Always error.

    ErrorStatus values:
    • error
  • kind 2 values

    BO or CO.

    kind values:
    • BO
    • CO
    Used in 1 place
    • DELETE /v1/orders/bracket/{order_id} query kind
  • kind 5 values

    Which data: orders, positions, holdings, trades or margins.

    kind values:
    • orders
    • positions
    • holdings
    • trades
    • margins
    Used in 1 place
    • GET /v1/portfolio/{kind} path kind
  • kind 3 values

    The provider the URL was issued for.

    kind values:
    • kuberhunt
    • tradingview
    • chartink
    Used in 1 place
    • POST /v1/hooks/{kind}/{secret} path kind
  • KuberhuntEventName 16 values

    Kuberhunt event name. reco.activated opens a position (entry); reco.sl_hit, reco.target_reached and reco.exit close it; reco.invalidated / reco.cancelled close it too; reco.updated / reco.trail_updated move its stop-loss / target; reco.scheduled, reco.pending, webhook.test and reco.t1_reached ... reco.t5_reached are acknowledged without action. Any other name is acknowledged and ignored.

    KuberhuntEventName values:
    • reco.activated
    • reco.scheduled
    • reco.pending
    • webhook.test
    • reco.trail_updated
    • reco.updated
    • reco.t1_reached
    • reco.t2_reached
    • reco.t3_reached
    • reco.t4_reached
    • reco.t5_reached
    • reco.sl_hit
    Used in 4 places
    • POST /v1/hooks/endpoints/{endpoint_id}/test request event
    • POST /v1/hooks/{kind}/{secret} request event
    • POST /kuberhunt/execute-signal/{token} request event
    • signal KuberhuntEvent event
  • OAuthErrorCode 6 values

    Token endpoint error codes (RFC 6749 section 5.2) and their status: invalid_request (400), invalid_grant (400), invalid_client (401), temporarily_unavailable (503), unsupported_grant_type (400), slow_down (429, with Retry-After).

    OAuthErrorCode values:
    • invalid_request
    • invalid_grant
    • invalid_client
    • temporarily_unavailable
    • unsupported_grant_type
    • slow_down
  • OAuthGrantType 2 values

    Supported grant_type values; any other is refused with invalid_request.

    OAuthGrantType values:
    • authorization_code
    • refresh_token
    Used in 1 place
    • POST /v1/oauth/token request grant_type
  • OrderState 9 values

    Order state: SUBMITTED (sent, not yet acknowledged), OPEN, TRIGGER_PENDING (stop-loss waiting for its trigger), PARTIALLY_FILLED, FILLED, CANCELLED, REJECTED, EXPIRED (day order ended unfilled) or UNKNOWN (not confirmed yet; never a guessed terminal state).

    OrderState values:
    • SUBMITTED
    • OPEN
    • TRIGGER_PENDING
    • PARTIALLY_FILLED
    • FILLED
    • CANCELLED
    • REJECTED
    • EXPIRED
    • UNKNOWN
    Used in 7 places
    • webhook An order changed data.state
    • webhook An order changed data.status_history[].from
    • webhook An order changed data.status_history[].to
    • webhook An order filled (fully or partly) data.order_state
    • stream order_update data.state
    • stream order_update data.status_history[].from
    • stream order_update data.status_history[].to
  • OrderType 4 values

    MARKET, LIMIT, SL (stop-loss limit) or SL_M (stop-loss market; SL-M and SLM are accepted in requests).

    OrderType values:
    • MARKET
    • LIMIT
    • SL
    • SL_M
    Used in 9 places
    • POST /v1/orders request orders[].order_type
    • POST /v1/orders response results[].order_type
    • PATCH /v1/orders/{order_id} request order_type
    • POST /v1/margins/orders request orders[].order_type
    • POST /v1/orders/bracket request order_type
    • POST /v1/hooks/endpoints/{endpoint_id}/test response would_place[].order_type
    • webhook An order changed data.order_type
    • webhook An order filled (fully or partly) data.order_type
    • stream order_update data.order_type
  • PortfolioKind 4 values

    Portfolio data kinds kept as per-account snapshots.

    PortfolioKind values:
    • positions
    • holdings
    • trades
    • margins
    Used in 1 place
    • stream portfolio kind
  • Product 4 values

    MIS (intraday), CARRYFORWARD (overnight derivatives, NRML), DELIVERY (equity delivery, CNC) or MTF (margin trading facility).

    Product values:
    • MIS
    • CARRYFORWARD
    • DELIVERY
    • MTF
    Used in 16 places
    • POST /v1/orders request orders[].product
    • PATCH /v1/orders/{order_id} request product
    • POST /v1/margins/orders request orders[].product
    • POST /v1/orders/bracket request product
    • POST /v1/positions/convert request from
    • POST /v1/positions/convert request to
    • GET /v1/gtt response [].gtts[].product
    • POST /v1/gtt request product
    • PATCH /v1/gtt/{trigger_id} request product
    • POST /v1/triggers request product
    • POST /v1/hooks/endpoints/{endpoint_id}/test response would_place[].product
    • GET /v1/shared/consolidated response positions[].product
    • webhook An order changed data.product
    • webhook An order filled (fully or partly) data.product
    • webhook An account's positions changed data.positions[].product
    • stream order_update data.product
  • ProtectionRoute 4 values

    Where an order's protection lives: a broker bracket or cover order placed with the entry, a broker gtt placed once the entry fills, or a server-side trigger armed once the entry fills.

    ProtectionRoute values:
    • bracket
    • cover
    • gtt
    • trigger
    Used in 1 place
    • POST /v1/orders response results[].protection
  • RuleType 3 values

    How a rule's value is read: percentage of the entry price, points from it, or an absolute price. Case-insensitive in requests.

    RuleType values:
    • percentage
    • points
    • price
    Used in 21 places
    • POST /v1/orders request orders[].protection.target.type
    • POST /v1/orders request orders[].protection.stop_loss.type
    • POST /v1/orders request orders[].protection.trail.type
    • POST /v1/margins/orders request orders[].protection.target.type
    • POST /v1/margins/orders request orders[].protection.stop_loss.type
    • POST /v1/margins/orders request orders[].protection.trail.type
    • GET /v1/triggers response [].rules.target.type
    • GET /v1/triggers response [].rules.stop_loss.type
    • GET /v1/triggers response [].rules.trail.type
    • POST /v1/triggers request rules.target.type
    • POST /v1/triggers request rules.stop_loss.type
    • POST /v1/triggers request rules.trail.type
    • POST /v1/triggers response rules.target.type
    • POST /v1/triggers response rules.stop_loss.type
    • POST /v1/triggers response rules.trail.type
    • PATCH /v1/triggers/{trigger_id} request rules.target.type
    • PATCH /v1/triggers/{trigger_id} request rules.stop_loss.type
    • PATCH /v1/triggers/{trigger_id} request rules.trail.type
    • PATCH /v1/triggers/{trigger_id} response rules.target.type
    • PATCH /v1/triggers/{trigger_id} response rules.stop_loss.type
    • PATCH /v1/triggers/{trigger_id} response rules.trail.type
  • Scope 3 values

    What an API key or partner token may do: read (portfolio, orders, triggers, GTT and activity reads; the stream), orders (place, modify, cancel and convert orders, bracket orders, GTT) or triggers (create, change and remove server-side triggers). App sessions may do everything.

    Scope values:
    • read
    • orders
    • triggers
    Used in 9 places
    • GET /v1/user/profile response auth.scopes
    • GET /v1/api-keys response [].scopes
    • POST /v1/api-keys request scopes
    • POST /v1/api-keys response scopes
    • GET /v1/partner-apps response [].scopes
    • POST /v1/partner-apps request scopes
    • POST /v1/partner-apps response scopes
    • GET /v1/oauth/consent response scopes
    • GET /v1/connections response [].scopes
  • ShareScope 6 values

    What an owner shared: login (account list and login status), funds, positions, holdings, open_orders, order_history.

    ShareScope values:
    • login
    • funds
    • positions
    • holdings
    • open_orders
    • order_history
    Used in 2 places
    • GET /v1/shared response [].scopes
    • GET /v1/shared/consolidated response owners[].scopes
  • Side 2 values

    Order side.

    Side values:
    • BUY
    • SELL
    Used in 17 places
    • POST /v1/orders request orders[].side
    • PATCH /v1/orders/{order_id} request side
    • POST /v1/margins/orders request orders[].side
    • POST /v1/orders/bracket request side
    • PATCH /v1/orders/bracket/{order_id} request side
    • POST /v1/positions/convert request side
    • GET /v1/gtt response [].gtts[].side
    • POST /v1/gtt request side
    • PATCH /v1/gtt/{trigger_id} request side
    • POST /v1/triggers request side
    • GET /v1/activity response items[].side
    • GET /v1/activity/{id} response side
    • POST /v1/hooks/endpoints/{endpoint_id}/test response would_place[].side
    • webhook An order changed data.side
    • webhook An order filled (fully or partly) data.side
    • stream order_update data.side
    • stream activity data.side
  • SignalAckMessage 21 values

    Every message a signal URL answers with status 200. The delivery was received; nothing more is promised (what it came to is in the URL's delivery log and the activity log).

    • Webhook received: handled (orders placed, closed, updated, or skipped by a strategy rule).
    • Duplicate: the same alert arrived twice; the copy was skipped.
    • Stale: an older Kuberhunt event arrived after a newer one.
    • Signal is disabled / Strategy paused: the strategy is paused.
    • Strategy not found: the strategy no longer exists.
    • Invalid Payload / Invalid payload / Invalid JSON / Empty body: the body could not be read.
    • Invalid signature, Timestamp out of window, Webhook secret missing: Kuberhunt signature checks.
    • Only entry and exit signals are processed: TradingView type was neither.
    • Too many stocks in one alert, No valid stocks found, No accounts configured: Chartink alerts that could not be traded.
    • instrument_token ... differ; send one, order_tag ... differ; send one, exits ... differ; send one: a field was sent under both its names with different values.
    • Invalid ID: the pre-v1 Kuberhunt URL's token is invalid.
    SignalAckMessage values:
    • Webhook received
    • Duplicate
    • Stale
    • Signal is disabled
    • Strategy paused
    • Strategy not found
    • Invalid Payload
    • Invalid payload
    • Invalid JSON
    • Empty body
    • Invalid signature
    • Timestamp out of window
    Used in 2 places
    • POST /v1/hooks/{kind}/{secret} response message
    • POST /kuberhunt/execute-signal/{token} response message
  • SignalDeliveryOutcome 8 values

    A delivery's result in one word: placed (every order it asked for was placed), partially_placed, failed (orders were attempted, none placed), processed (handled without new orders: levels updated, lifecycle event...), ignored (valid, but the strategy's rules meant nothing to do), refused (wrong secret or signature, unreadable payload), duplicate (the same alert again; skipped) or rate_limited (over the URL's limit; not processed).

    SignalDeliveryOutcome values:
    • placed
    • partially_placed
    • failed
    • processed
    • ignored
    • refused
    • duplicate
    • rate_limited
    Used in 1 place
    • GET /v1/hooks/endpoints/{endpoint_id}/deliveries response items[].outcome
  • SignalErrorStatus 1 values

    Always error.

    SignalErrorStatus values:
    • error
  • SignalForbiddenMessage 1 values

    Messages answered with 403 (Invalid secret: the TradingView body secret is missing or wrong).

    SignalForbiddenMessage values:
    • Invalid secret
  • SignalProvider 3 values

    The signal provider a URL is for: tradingview (TradingView alerts), chartink (Chartink scanner alerts) or kuberhunt (Kuberhunt recommendation events).

    SignalProvider values:
    • kuberhunt
    • tradingview
    • chartink
    Used in 5 places
    • GET /v1/hooks/endpoints response [].kind
    • POST /v1/hooks/endpoints request kind
    • POST /v1/hooks/endpoints response kind
    • POST /v1/hooks/endpoints/{endpoint_id}/rotate response kind
    • GET /v1/hooks/endpoints/{endpoint_id}/deliveries response items[].provider
  • SignalRetryMessage 3 values

    Messages answered with 503; the provider should send the delivery again: temporarily unavailable (a store is down), Order handling failed; please redeliver (a Kuberhunt entry or close failed in a way worth retrying; its de-duplication was undone), signal strategies unavailable (strategies cannot be read on this server).

    SignalRetryMessage values:
    • temporarily unavailable
    • Order handling failed; please redeliver
    • signal strategies unavailable
  • SliceStatus 3 values

    accepted (the broker took it; fills arrive on the stream), rejected (refused before or by the broker; nothing was placed) or unknown (the broker may or may not have it, e.g. a timeout; reconciliation settles it: never retry blindly).

    SliceStatus values:
    • accepted
    • rejected
    • unknown
    Used in 10 places
    • POST /v1/orders response results[].status
    • PATCH /v1/orders/{order_id} response status
    • DELETE /v1/orders/{order_id} response status
    • POST /v1/orders/bracket response status
    • PATCH /v1/orders/bracket/{order_id} response status
    • DELETE /v1/orders/bracket/{order_id} response status
    • POST /v1/positions/convert response status
    • POST /v1/gtt response status
    • PATCH /v1/gtt/{trigger_id} response status
    • DELETE /v1/gtt/{trigger_id} response status
  • SnapshotKind 5 values

    Snapshot kind: orders (today's order book) or a portfolio kind.

    SnapshotKind values:
    • orders
    • positions
    • holdings
    • trades
    • margins
    Used in 1 place
    • stream snapshot kind
  • StreamErrorCode 1 values

    Why the stream cannot go on: unavailable (live updates could not be set up; reconnect).

    StreamErrorCode values:
    • unavailable
    Used in 1 place
    • stream error code
  • SuccessStatus 1 values

    Always success.

    SuccessStatus values:
    • success
  • TradingViewSignalType 2 values

    TradingView alert type: entry places the strategy's legs, exit closes what the strategy's entries placed. Case and surrounding spaces are ignored ("Entry " works); any other type is acknowledged and ignored.

    TradingViewSignalType values:
    • entry
    • exit
    Used in 3 places
    • POST /v1/hooks/endpoints/{endpoint_id}/test request type
    • POST /v1/hooks/{kind}/{secret} request type
    • signal TradingViewAlert type
  • view 6 values

    What to read: accounts (needs login), positions, holdings, margins (needs funds), open_orders, order_history.

    view values:
    • accounts
    • positions
    • holdings
    • margins
    • open_orders
    • order_history
    Used in 1 place
    • GET /v1/shared/{owner}/{view} path view
  • WebhookAlertKind 8 values

    Activity kinds sent as account_alert: session_expired (the broker login ended; log in again), relogin_detected (the account was logged in elsewhere), live_updates_unavailable / live_updates_restored (the broker's live order feed dropped / came back), protection_failed (a stop-loss / target could not be placed or kept), order_rejected, account_setup_incomplete (a field one feature needs is missing) and webhook_disabled (one of the user's webhooks was turned off after repeated failures).

    WebhookAlertKind values:
    • session_expired
    • relogin_detected
    • live_updates_unavailable
    • live_updates_restored
    • protection_failed
    • order_rejected
    • account_setup_incomplete
    • webhook_disabled
    Used in 1 place
    • webhook Something about an account needs the user data.kind
  • WebhookEventType 5 values

    Event names. A receiver subscribes to any of order_update (any change to an order), trade (a fill), account_alert (something about an account that needs the user) and positions (an account's positions changed, at most once per second per account). ping is sent only by the webhook's test endpoint and cannot be subscribed to.

    WebhookEventType values:
    • order_update
    • trade
    • account_alert
    • positions
    • ping
    Used in 8 places
    • GET /v1/postbacks response [].events
    • POST /v1/postbacks request events
    • POST /v1/postbacks response events
    • GET /v1/postbacks/{id} response events
    • PATCH /v1/postbacks/{id} request events
    • PATCH /v1/postbacks/{id} response events
    • POST /v1/postbacks/{id}/rotate-secret response events
    • GET /v1/postbacks/{id}/deliveries response deliveries[].type