Skip to content
Cirrus home
Sign in

Changelog

Every change you can see, newest first. Additive changes never break a client that ignores unknown fields; breaking ones are announced before they ship.

  1. Additive

    Connection limits say which tokens they bind

    GET|PUT /v1/connections/{client_id}/limits responses gain applies_to (array, currently always ["mcp"]): the limits bind mcp (AI agent) tokens only; classic partner tokens of the connection are not limited by them. Field and operation descriptions reworded.

  2. Additive

    v1 records on `/v2/triggers`: trail progress kept, unfilled ones pending, a split's target ends on every stop cancel

    • PATCH /v2/triggers/{trigger_id} (modify_trigger_v2) on a trailing protection held as a v1 record: a stop-loss looser than where the trail moved it is refused with 409 trigger_busy (an existing code) and field rules ("the trail moved the stop to X; to set a looser stop, cancel and protect again"); nothing is sent. Before, re-sending the level read before the trail moved (e.g. with a target-only change) set the stop back there and lost the trail's progress. A tighter stop moves as before; a stop without a trail keeps v1's rules. POST

    /v2/triggers/{trigger_id}/validate reports the same as a spec error.

    • GET /v2/triggers and GET /v2/triggers/{trigger_id}: a protection held as a v1 record whose entry has not filled (live false on /v1/triggers) shows state pending (already a documented value), not active. The state description now lists the v1 statuses an ended v1 record can show (target_reached, invalidated, exit) and that it may show why it ended.
    • A split protection's server-side target now also ends when the broker cancels its stop at once on DELETE /v1/triggers/{trigger_id} (and position exits that remove protection), as on DELETE

    /v2/triggers/{trigger_id} and after a refused cancel ("2026-10-31: A protection's target ends with its stop on every record"): the same request has one outcome whichever way the broker answers. Correction to that entry: when the broker shows the stop fired, the target ends with reason stop_loss_executed_by_broker (as when a broker stop-loss GTT executes), not cancelled. Only a server-held target naming the stop back is ended.

  3. Additive

    A protection's target ends with its stop on every record

    • A split protection (a broker GTT holding the stop-loss, a server-side trigger holding the target) whose stop cancel the broker refused while still holding it (broker_cancel pending) keeps its target until the broker ends the stop. Once the broker shows the stop cancelled, ended or fired, the target now ends too (reason cancelled) for every record: made before 2026-10-28 through POST /v1/triggers, the order ticket or a signal entry, or through /v2/triggers, and whether it was cancelled through DELETE /v1/triggers/{trigger_id}, DELETE /v2/triggers/{trigger_id} or a signal's exit. Before, this held only for records made through /v2/triggers; for the others the target stayed live after the stop was gone, and a later target fill could open a reverse position.
  4. Change

    Pocketful closed positions report exact realised P&L

    Behaviour (no field added, removed or retyped; no new enum value)

    • Pocketful realized_mtm is whole rupees per trade (29 for 29.25), so a closed day-only row with many fills drifted past a rupee (prod 2026-10-30: 2130 for a 2132 round trip). A closed, day-only, non-delivery row now reports realised_pnl = sold value - bought value, rounded to paise; an open row keeps the broker's FIFO figure. 19 Pocketful positions-* expectations re-recorded, realised_pnl values only.
  5. Additive

    Connection limits also gate trigger resume, conversions and entry signals

    For mcp-audience partner tokens only, three more operations are checked against the connection's limits and may answer 403 forbidden naming the limit: POST /v2/triggers/{trigger_id}/resume (trading, accounts, paper only), POST /v1/positions/convert (trading, accounts, paper only in both directions; a conversion from MIS to an overnight product is also valued against max_order_value) and POST /v2/deployments/{deployment_id}/signals with action enter (trading, accounts, paper only, live strategies). Modifies are valued as requested (the new quantity and price), and the day's order count is taken before anything is sent, so concurrent requests never exceed max_orders_per_day. The OpenAPI document lists the 403.

  6. Additive

    Edit a connection's limits

    New operations GET /v1/connections/{client_id}/limits (get_connection_limits) and PUT /v1/connections/{client_id}/limits (set_connection_limits), signed-in app session only (an operator acting as the user, API keys and partner tokens get 403). GET returns {client_id, limits, is_default}; with no limits set it returns the defaults of a dynamically registered client (paper accounts only, no account picked, 5 orders a day, one deployment, nothing live) and is_default: true. PUT takes the limits object (trading, accounts, paper_only, max_order_value in whole rupees, max_orders_per_day, live_strategies, max_deployments), applies it at once on every server and records a connection_limits_changed activity with the before and after. Errors: 404 connection_not_found (unknown or revoked connection), 422 invalid_request with field (accounts: not the user's, duplicate, empty or more than 50; live_strategies: needs trading, paper_only off and the strategies_live scope; max_order_value: too large). New examples.

  7. Additive

    Cirrus MCP connection limits are enforced

    Only for mcp-audience partner tokens (requests through the Cirrus MCP); app sessions, API keys and classic partner tokens are unchanged. Order place (POST /v1/orders, every order in every account), modify (PATCH /v1/orders/{order_id}), bracket place / modify, GTT create / modify, trigger create / modify (/v1/triggers, /v2/triggers), deploy, go-live and resume are checked against the connection's limits before anything is sent; a refusal is 403 with the existing forbidden code and a message naming the limit (e.g. "This connection may place orders up to ₹10,000; this one is ₹12,500"), and is recorded in activity as a failed record of the attempted kind. Exits, cancels, pause, stop, exit-now and stop-all are never refused by a limit. Orders, GTT and triggers created count against max_orders_per_day per IST day (Redis oms:oauth:limits:count:{username}|{client_id}:{YYYYMMDD}, kept two days). The OpenAPI document lists the 403 on these operations. Ships before any MCP connection exists (readers first).

  8. Breaking

    v1 protection routes stop working on 30 Dec 2026

    • POST /v1/triggers, GET /v1/triggers, GET /v1/triggers/{trigger_id}, PATCH /v1/triggers/{trigger_id}, DELETE /v1/triggers/{trigger_id} (create_trigger, list_triggers, get_trigger, modify_trigger, delete_trigger) stop working on 30 Dec 2026, start of day IST (Tue, 29 Dec 2026 18:30:00 GMT). Use /v2/triggers (migration guide /docs/guides/protect-a-position, docs/api/MIGRATION-v1-protection.md).
    • From now on every answer of these routes (errors included) also carries Sunset: Tue, 29 Dec 2026 18:30:00 GMT (RFC 8594), next to Deprecation and Link.
    • From that instant every request to them (any method, signed in or not) answers 410 with the new error code gone (a new ErrorCode value), a message naming /v2/triggers and the migration guide, and the same headers; nothing is done. Until then they answer exactly as before.
    • The operations stay documented (deprecated: true); their descriptions state the date and the 410, documented with an example.
    • /v1/gtt and GET /v1/orders/{order_id}/protection are not affected. Notified: 2026-10-31, owner: no external consumer affected — partners with read access (Kuberhunt) only send webhooks and do not read /v1/activity or call /v1/triggers; Pocketful/5Paisa users have no public API; the app moves to /v2 in app-cirrus#322.
  9. Additive

    v1 protection calls are counted per day and caller

    Stored-record change only: every authenticated call of the deprecated /v1/triggers routes adds one to the Redis hash oms:v1_triggers:calls:{YYYY-MM-DD} (IST day, kept 120 days), field app:{username}, key:{key_id} or partner:{client_id}, so the owner can see who still calls them (docs/runbooks/v1-protection-sunset.md). After the sunset a signed-in caller's 410 is counted in the same hash as after_sunset:{caller}. Counting never fails or slows a call, and changes nothing in their answers (the sunset is the entry above).

  10. Additive

    `/v2/triggers` manages every protection `/v1/triggers` shows

    • Correction to "2026-10-28: Every new protection is a v2 trigger": it said protections made before 2026-10-28 are "listed, read, changed and deleted on /v1/triggers and /v2/triggers". On /v2/triggers that held only for those made through /v2/triggers; the others (made through POST /v1/triggers, the order ticket or a signal entry), the display records of a broker bracket / cover placed with an entry (still written today) and /v1/gtt records were on /v1/triggers only (404 on /v2/triggers/{trigger_id}).
    • GET /v2/triggers (list_triggers_v2) now also lists every protection GET /v1/triggers lists that is held as a v1 record, live and ended (7 days, as on v1), as a bracket with plain price levels: route gtt, stop_order, bracket, cover or trigger (values already in the ProtectionRoute schema), a trailing stop-loss as spec.stop_loss.trail.step (points as they are, a percentage as entry_px * <pct> / 100), a GTT stop-loss and its server-side target as one trigger. Its trigger_id is v1's (not a ULID), created_via is absent; an ended one shows why in state and reason (v1's ended_reason, e.g. cancelled).
    • GET /v2/triggers/{trigger_id} (get_trigger_v2) reads a live one by its v1 id (either half of a split).
    • PATCH /v2/triggers/{trigger_id} (modify_trigger_v2) and POST /v2/triggers/{trigger_id}/validate change one through v1's own code: at the broker first where the broker holds it, with v1's rules and answers (502 broker_refused, 504 broker_outcome_unknown, 409 trigger_fired; a bracket keeps both parts, a cover a stop-loss only). Its spec stays plain price levels with the same parts and no valid_until; its quantity cannot change; a trailing stop-loss is kept and must be sent as shown (422 invalid_request otherwise). A level sent as it stands keeps its v1 rule (points, percentage, trailing progress). route bracket / cover is accepted on a bracket / cover.
    • DELETE /v2/triggers/{trigger_id} (cancel_trigger_v2) cancels one through v1's own code (broker first; 502 keeps it, 409 when it fired, broker_cancel pending while the broker still holds it), with its paired target.
    • POST /v2/triggers/{trigger_id}/pause / resume (pause_trigger_v2, resume_trigger_v2) on such a protection: 422 invalid_request (field trigger_id), nothing changed (v1 never paused one; a paused stop-loss would leave the position unprotected). Was 404.
    • /v1/triggers is unchanged. /v2/triggers is still only for the users OMS_TRIGGERS_V2_USERS allows (*: everyone; see docs/runbooks/v2-triggers-for-everyone.md).
  11. Additive

    Partner connections store optional limits

    Stored-record change only: partner connections (Mongo partner_connections) gain an optional limits object (trading, accounts, paper_only, max_order_value in whole rupees, max_orders_per_day, live_strategies, max_deployments), mirrored in Redis oms:oauth:limits:{username}|{client_id} (JSON, null when none set). Connections made before read as having none. Nothing sets or enforces them yet; no REST response, webhook or stream message changes. New fixtures and reader tests.

  12. Additive

    Connected apps show last used and calls today

    GET /v1/connections items gain last_used_at (RFC 3339 or null; updated at most once a minute) and calls_today (requests the app made today, IST day).

  13. Additive

    What a disconnected app left running

    New operation GET /v1/connections/{client_id}/left (get_connection_left_by): the open orders, active GTTs, open v2 triggers and not-stopped strategy deployments the app created for the signed-in user, by who created them. Works after the connection is revoked; the owner's app session only. Revoking a connection now records its partner_revoked activity as Disconnected “{name}”. Left running: {n} open orders, {g} GTT, {t} triggers, {d} strategies. (it was Disconnected partner app “{name}”); the revoke response is unchanged.

  14. Additive

    Stored GTT mirror records who created it

    The stored GTT mirror's trigger meta gains an optional created_by. Old records read as none.

  15. Additive

    Activity summaries format rupee amounts with Indian grouping

    Rupee amounts in activity summaries use Indian digit grouping, e.g. ₹5000 becomes ₹5,000 and ₹5000.5 becomes ₹5,000.50.

  16. Additive

    Activity filter by connection; recently revoked connections listed

    • GET /v1/activity accepts an optional client_id query parameter: only records made by that partner connection (the client_id of the record's source). Without it nothing changes; other unknown parameters are still refused with 400.
    • GET /v1/connections also lists connections revoked in the last 30 days. Each carries a new optional revoked_at (RFC 3339); live connections do not have it. Revoking an already-revoked connection still answers 404.
  17. Additive

    GTT and convert answers say when a refusal may pass

    • GttResult (POST /v1/gtt, PATCH and DELETE /v1/gtt/{trigger_id}) and ConvertResult (POST /v1/positions/convert) gain an optional reason, present only with status: rejected when nothing reached the broker and a retry may pass: rate_limited (the account's order budget) or no_session (log the account in again). Absent in every other answer, so existing answers are unchanged.
    • With it, a same-key retry of these operations after such a refusal runs again instead of replaying it (see the entry below).
  18. Additive

    Idempotency-Key on every remaining order, GTT and v2 trigger write

    • DELETE /v1/orders/{order_id}, PATCH and DELETE

    /v1/orders/bracket/{order_id}, DELETE /v1/gtt/{trigger_id}, PATCH and DELETE /v2/triggers/{trigger_id}, POST /v2/triggers/{trigger_id}/pause and /resume take the same optional Idempotency-Key header, with the same answers (replay with Idempotent-Replayed: true, 400 invalid_idempotency_key, 409 request_in_progress, 422 idempotency_key_reused). On a DELETE the key is bound to the path and query. Requests without the header behave exactly as before.

    • On every operation that takes the key (these and those in the entry below), a rejected answer whose reason may pass on a retry (rate_limited, no_session, stale_quote) gives the key back: a retry with the same key runs again instead of replaying the refusal (as order modify already did).
  19. Additive

    Idempotency-Key on GTT, convert, v2 trigger create and basket writes

    • POST /v1/gtt, PATCH /v1/gtt/{trigger_id}, POST /v1/positions/convert, POST /v2/triggers, POST /v1/baskets, PUT and DELETE

    /v1/baskets/{basket_id} take an optional Idempotency-Key header (1-128 printable characters), as order placement and modify do. A repeat with the same key and body gets the first answer back with Idempotent-Replayed: true and does nothing again; the same key with a different body or route is refused with 422 idempotency_key_reused; a repeat while the first still runs gets 409 request_in_progress. A refusal (4xx / 5xx) gives the key back, except broker_outcome_unknown (POST /v2/triggers, and any keyed write that answers it): that answer is kept like a success, so a repeat within 24 hours gets the same 504 back (Idempotent-Replayed: true) and acts nothing again. Requests without the header behave exactly as before.

    • New documented responses on these operations: 400 invalid_idempotency_key, 409 request_in_progress, 422 idempotency_key_reused (only when the header is sent).
    • Automated-strategy writes, which already took the key, share the same claim and rules (no strategy answer is an unknown broker outcome or a transient rejected today, so their behaviour does not change). The API description now declares their optional Idempotency-Key header too (it was only in their descriptions): create_auto_strategy, update_auto_strategy, archive_auto_strategy, save_auto_strategy_version, clone_auto_strategy, import_auto_strategy, deploy_auto_strategy, pause_auto_deployment, resume_auto_deployment, stop_auto_deployment, exit_auto_deployment_now, go_live_auto_deployment, stop_all_auto_strategies, set_auto_risk_limits, create_auto_backtest, cancel_auto_backtest.
    • The Cirrus MCP offers idempotency_key (and promises a safe retry) only on tools whose operation declares the header; other write tools send no key and, when the outcome is unknown, tell the agent to check the current state before acting again.
  20. Additive

    OAuth dynamic client registration and server metadata for AI agents

    • New POST /v1/oauth/register (register_oauth_client, RFC 7591): an AI agent registers itself as a public OAuth client (no secret, token_endpoint_auth_method: none; https, loopback http on any port or a known desktop scheme as redirect URIs; at most 10 an hour per client IP, then 429 slow_down). Errors use the token endpoint's existing {error, error_description} codes (invalid_request, slow_down, temporarily_unavailable).
    • New GET /.well-known/oauth-authorization-server (get_oauth_server_metadata, RFC 8414).
    • Both answer 404 with no body unless agent access is fully set up (agent_access on, OMS_MCP_SERVICE_SECRET, OMS_MCP_PUBLIC_URL, OMS_OAUTH_ISSUER_URL and OMS_OAUTH_CONSENT_URL set): no change for deployments without it.
    • POST /v1/oauth/token: a client registered this way authenticates with client_id only (PKCE binds its code); a registered partner app still needs its secret. Its loopback redirect URIs match on any port (RFC 8252 §7.3), and scope names that are not scopes here (mcp:tools, offline_access) are ignored on consent. Its access tokens (also after a refresh) have the mcp audience: they work only through the MCP server. A client_name naming a known AI vendor (Claude, ChatGPT, Cursor, ...) is refused with invalid_request unless every https redirect URI is that vendor's. Registrations nobody connected through are deleted after 30 days, and then get 401 invalid_client.
    • Stored partner apps (Mongo partner_apps) gain optional dynamic (default false) and resource. Readers first: older pods read these records (owner @dcr, empty secret_hash) and refuse their token requests; turn registration on (the settings above) only once every pod runs this build. GET /v1/partner-apps never lists them.