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

    Strategies read API (`GET /v1/strategies`, `/v1/strategies/{name}`)

    Two new operations (tag strategies, scope read; app sessions, API keys and partner tokens with read): today's positions and P&L per strategy across the caller's own accounts, read from the per-strategy ledger the engine stores (never recomputed by the API). Each strategy (name null = Unassigned) lists per account its rows (cirrus_token, symbol, tradingsymbol, product, net_qty, avg_price, realized, ltp, ltp_stale, unrealized) and totals, overall totals (realized, unrealized, total, ltp_stale), a recon summary (status ok / pending / mismatch with a plain-English note) and at. Accounts without a ledger today are under missing (code not_available_yet or unreadable); with none at all available is false and the answer is still 200. GET /v1/strategies/{name} (~unassigned for the rest) adds the strategy's v2 triggers (trigger_id, account, kind, state, ended; null when v2 triggers are off for the user); a name with nothing today answers 200 with no accounts. New examples get_strategies, get_strategy. No existing operation changes.

  2. Additive

    Manual orders and baskets can name a strategy

    POST /v1/orders: each order item takes an optional strategy (1-64 letters, digits, _, ., : or -, as client_tag). It is stored on the order (the existing optional strategy on order rows, so far set only by v2 trigger orders) and groups the order's fills under that strategy in the per-strategy ledger. A name of another shape is refused with 422 invalid_request, field orders[N].strategy. POST / PUT /v1/baskets take an optional basket-level strategy (same rule, field strategy), returned by GET /v1/baskets/{basket_id} when set; a replace without it clears it. Requests without strategy behave as before. The 400 malformed_json message for an unknown basket field now lists strategy among the expected fields (examples create_basket, replace_basket updated).

  3. Additive

    Order protection held by Cirrus can be a v2 trigger (`trigger_v2_id`)

    POST /v1/orders: a result (results[]) gains an optional trigger_v2_id, present when the order's stop-loss / target is held by Cirrus as a v2 trigger (OMS_ORDER_PROTECTION_V2, off by default, then for users v2 triggers are enabled for): the v2 bracket (/v2/triggers/{trigger_id}) created when the order fills, for the filled quantity, levels measured from the fill price; units filled later get brackets of their own (on GET /v2/triggers). protection stays trigger; no other field changes, and the field is absent otherwise. New example protected_by_v2_trigger of place_orders. GET / PATCH / DELETE /v1/triggers/{trigger_id} with a v2 trigger's id still answer 404 trigger_not_found, now saying to use /v2/triggers/{trigger_id}. New stored record ticket_protection (oms:protect2:{user|account}:{tag}, enum held_by: v2, v1; optional strategy, the order's, carried to its brackets); written only with the switch on, after every pod reads it.

  4. Additive

    Kotak Neo adapter; its conformance pack

    Kotak Neo accounts (Broker KotakNeo; ACCOUNT_LIST_DATA Access Token, SID, Data Center and Base URL, written by broker-auth's Trade API login; a static IP is needed) are served like other brokers: same request and response shapes, GET /v1/accounts reports them supported, updates stream (Kotak's order & position feed nudges an order-book read; open orders are also checked one by one). Calls go to the account's own Base URL (only an https://*.kotaksecurities.com host; KOTAKNEO_BASE_URL, default https://mis.kotaksecurities.com, when it has none). GTT, bracket / cover (discontinued by Kotak since 2026-04-01) and position conversion are refused as for any broker without them; Kotak echoes no order tag, so orders are matched by order number. No existing field, type, enum value or status code changes. The broker conformance corpus gains a new kotakneo pack at replay_green: synthetic recordings of the Neo SDK docs' samples (order book, trades, holdings, limits, margin check, place / modify / cancel) and of Kotak's Trade API docs' examples (new holdings shape, margin check, place / modify / cancel replies and refusals, "Invalid session", the two static-IP refusals, the order feed's connection string and frames), their expectations, error templates, and the documented order statuses frozen. A non-JSON {key:value} stream message we send is checked as its fields, so its token is redacted by name. Logs now mask Sid / Auth fields.

  5. Additive

    Stale triggers end on their own (`day_ended`, `contract_expired`, `position_closed`)

    Server-kept triggers now end on their own, and say why, instead of staying active past their use (prod 2026-10-16: an intraday stop-loss of the day before fired at the next open). GET /v1/triggers: an ended protection's ended_reason can be day_ended (an intraday, MIS, trigger after its exchange session's close or made on an earlier trading day), contract_expired (its contract expired before today) or position_closed (the position it protected is confirmed flat). v2 (GET /v2/triggers, /v2/triggers/{trigger_id}, the trigger_update stream message and webhook): such a trigger moves to the existing state cancelled with reason day_ended or contract_expired; its journey's transition summary says it in words ("Ended at market close — intraday position", "Ended — contract expired"). ended_reason and reason were already open strings; no field, state or enum value is added.

  6. Additive

    v2 engine events carry the engine's clock (engine_at)

    Stored record trg_event ({ENV}.trg.evt.…, also in trg_snapshot outboxes) gains an optional engine_at (epoch ms): the engine's clock when it evaluated the tick that made the transition, set when it differs from at (at stays the tick's exchange time, whole seconds on most feeds). Absent on older events and when at is already the engine's clock; readers fall back to at, and older readers ignore it. The v2 exit's hit_to_placed_ms log field and oms_trg_hit_to_order_sent_seconds now measure from it, so they no longer overstate by up to 1 s (same names; expect a drop of about half a second on average after the rollout). Shadow events (trg_shadow_event) are unchanged. No REST, webhook or stream shape changes.

  7. Additive

    An order the broker split is shown as the orders it lists

    When a broker splits a placed order into several orders (its freeze-quantity cap: Pocketful at 500 units, Zerodha's autoslice, ...) and lists only those, GET /v1/portfolio/orders, GET /v1/orders/{order_id}, the /v1/stream snapshot / order_update messages and the order_update webhook now show one order per part, each with its own broker order_id, quantity, filled_qty, average_price, state and status_history as the broker lists it. Before, they showed one order under the id the placement returned, which the broker never lists, with the parts' fills summed; modifying or cancelling it failed. Each part carries the new optional split_from (SplitFrom: the placed order_id, order_tag and quantity), absent on every other order. The first part keeps the placed order_tag (so a client keyed by it sees the order replaced by that part); the others get a tag of their own, the same on every read. PATCH / DELETE /v1/orders/{order_id} take each part's own id as usual. For the placed id they act on each open part: a cancel cancels each one; a modify that keeps the placed quantity (price, trigger, order type) changes each one at its own quantity; a modify of the quantity is refused (422 invalid_request, field: quantity), and so is any call when no part is open (409 order_not_open). Those answers carry the new optional split_orders (PartResult: order_id, status, message, reason per part); status is accepted only when every part was. Orders placed in the broker's own app that it split the same way (they share one broker tag) are each their own outside order with only their own fills (they showed a sibling's fills). Orders the broker did not split are unchanged.

    Activity log (AccountResult.message of a modify or cancel): a broker that answers with an error not saying whether the change was done (Pocketful's fast HTTP 500 "Something went wrong") now reads "{Broker} answered with an error (HTTP 500); check the order book before trying again", and once the order book shows the order unchanged "{Broker} answered with an error (HTTP 500); the order book shows it unchanged". It said "No answer from {Broker} in time", which stays for real timeouts and connection failures; the status (unknown, then rejected once the book settles it) is unchanged.

  8. Additive

    Dependent orders stamped with their own send time in the activity log

    GET /v1/activity, GET /v1/activity/{id} and the /v1/stream activity message: the record of a dependent order (depends_on) now has at = when its wait for the parent ended and it was sent (or refused with dependency_failed), the earliest such moment across its accounts, and duration_ms = from then to its brokers' last answer. Before, every order of a request carried the request's start and the whole request's duration, so a hedge-first basket's legs shared one time and listed in no particular order; newest-first now lists a dependent leg above the parent it waited for. Records of orders without depends_on (single orders, plain batches, the first wave of a sequenced request) are unchanged. New optional field depends_on (ActivityDependsOn) on dependent orders' records only, absent on every other record: order_ref (the parent's activity record id), tradingsymbol and side of the parent, waited_ms (longest wait in any account; null when the parent was never placed) and outcome (filled, partial, failed or timeout; across accounts the least complete).

  9. Additive

    Broker shapes recorded; gateway 429 refusals; held orders settle

    No request or response field changes.

    • New reply, request and stream shapes seen in production (Angel One's first order-stream frames, Alice Blue, 5Paisa, Zerodha, Tradejini, Dhan; Pocketful on both tenants) join the conformance corpus, so they no longer alert. Pocketful MCX rows (MCX is disabled in the Pocketful master) are no longer reported.
    • An order refused by a broker gateway's HTML "429 Too Many Requests" page (Pocketful) is REJECTED at once with the reason "the broker's gateway refused the request (HTTP 429 page, too many requests, not an API reply); try again shortly", instead of "Order placement failed" after ~4.5 s of order-book reads. State values are unchanged.
    • An UNKNOWN order that never reached the broker, held back during its day by the user's own untagged look-alike order, now gets its existing REJECTED order_update ("never reached the broker: ...") on the first book read of the next trading day instead of staying UNKNOWN.
  10. Additive

    Partner app webhooks and the placing app on orders

    • PATCH /v1/partner-apps/{client_id} sets, changes, turns off or removes a partner app's webhook (webhook_url, webhook_events, webhook_enabled); the first URL returns webhook_secret (whsec_...) once. POST /v1/partner-apps/{client_id}/rotate-webhook-secret issues a new one; the previous one keeps signing for 24 hours. Own app session only. Both can answer 409 conflict (new error code) when a concurrent change keeps winning.
    • PartnerApp.webhook (list, register, update): present once a webhook is set.
    • Orders (GET /v1/orders, /v1/stream, the order_update webhook): partner_client_id, present only on orders a partner app placed.
    • New webhook connection_revoked ({username, client_id, reason}), sent only to partner app webhooks. Partner app deliveries also carry username at the top level, and their account_alert may have kind account_removed. Users' own webhooks get no new event, alert kind or top-level field; the only change they see is partner_client_id on orders a partner app placed (above).
    • Partner app bodies are documented as their own schemas: PartnerWebhookOrderUpdate, PartnerWebhookTrade, PartnerWebhookAccountAlert (kinds PartnerWebhookAlertKind: the user alert kinds except webhook_disabled, plus account_removed) and WebhookConnectionRevoked, together PartnerWebhookBody, each requiring username; referenced from PATCH /v1/partner-apps/{client_id}. WebhookConnectionRevoked and the connection_revoked webhook now describe the partner app's receiver and its owner (not the user's webhooks). User webhook schemas, WebhookAlertKind and WebhookEvent are unchanged.
    • Rollout: the two webhook routes answer 503 and no partner app delivery (order_update, trade, account_alert, connection_revoked, account_removed) is sent until OMS_PARTNER_WEBHOOKS is turned on; the routes are enabled only with that flag. partner_client_id on orders is written and shown regardless of the flag.
  11. Additive

    connection_revoked event type (partner webhooks)

    New internal webhook event type connection_revoked, recorded in webhook jobs and delivery logs. It is for partner app webhooks only and is never accepted in a user's webhook events nor listed in WebhookEventType. Nothing writes it until OMS_PARTNER_WEBHOOKS is turned on, after every pod runs this build.

  12. Additive

    MARKET orders priced from the last price when it is not recent

    POST /v1/orders, PATCH /v1/orders/{order_id} and POST /v1/positions/exit (and trigger exits): a protected MARKET order whose last price is older than 60 s, or of unknown time, is no longer refused with stale_quote; it goes out as a LIMIT protected from the last price held, as a fresh price would set it. stale_quote is now answered only when there is no price at all, or when the book is too far from the last price. Some instruments' feeds carry no exchange time (e.g. MCX options), which left every MARKET order and trigger exit on them refused while the price itself was live. The OMS_MARKET_WITHOUT_FRESH_PRICE setting is removed.

  13. Additive

    Angel One live shapes and refusals

    Angel One's conformance pack gains live recordings (book reads, order, GTT, convert and margin replies) and two error templates: the gateway's plain-text HTTP 403 throttle is rate_limited (no longer read as a refused login, and its text is the refusal message) and AG7002 "not a registered IP" is static_ip_not_registered.

  14. Additive

    Nubra live order updates, order status, GTT, bracket and cover

    Nubra accounts report updates: "stream" (Nubra's order socket nudges a book read; polling resumes while it is down) and gain single-order status, GTT (single leg, delivery: Nubra's good-till order; two-leg GTT is refused) and bracket / cover orders (intraday: Nubra's entry-and-exit order). A bracket / cover's exits are listed as a stop leg {id}:sl and a target leg {id}:tp; exiting a filled one squares it off. /v1/accounts protection for Nubra now shows bracket, cover and gtt true.

  15. Additive

    Shared data email alerts kept

    The removal of the Shared Data alert operations announced earlier today was reverted (#105): GET / PUT /v1/shared/alerts, POST /v1/shared/alerts/test and GET /v1/shared/alerts/log stay available, as documented on 2026-10-14.

  16. Additive

    Dependent orders can follow a partial fill; saved baskets

    POST /v1/orders: two optional fields on an order with depends_on. depends_on_partial: "filled_qty" sends the dependent order for the share its parent filled in that account by the end of the wait (rounded down to whole lots of the dependent order); "reject" (default) keeps today's behaviour. depends_on_timeout_ms (1000-30000, default 30000) sets how long it waits. Both are refused with 422 invalid_request without depends_on; depends_on_timeout_ms outside 1000-30000 is refused with 422 invalid_request (field orders[i].depends_on_timeout_ms). depends_on_partial: "filled_qty" on an order that another order of the request depends on is refused with 422 invalid_request (field orders[i].depends_on_partial). With filled_qty the wait ends once every slice of the parent in that account has ended, or at the timeout. POST /v1/margins/orders shares the order type: it accepts both fields, refuses them by the same rules, and otherwise ignores them (margins are unchanged). A result sent with fewer units carries reduced_from (the planned units); nothing else in the response changes and no new reason value exists. New GET/POST /v1/baskets and GET/PUT/DELETE /v1/baskets/{basket_id} (app sessions only) store named baskets of up to 20 legs, 50 per user. An unknown or another user's basket_id answers 404 with the new error code basket_not_found (error codes are open: handle unknown ones by their HTTP status). Unreadable stored baskets are reported to the watchdog; order placement now also logs and counts unreadable order-snapshot rows while waiting for a parent order (no response change).

  17. Additive

    Shared data email alerts

    New operations (app session only), for a viewer of accounts shared with them:

    • GET /v1/shared/alerts: the caller's email alert settings (enabled, emails, events rejected / failed / session_expired, scope all / logged_in_today, timing instant / digest, updated_at); defaults before the first save.
    • PUT /v1/shared/alerts: saves them (422 invalid_request naming emails, events, scope, timing or enabled).
    • POST /v1/shared/alerts/test: one test email to the saved addresses (sent_to); 422 without saved addresses, 429 rate_limited within 30 s, 503 with the new error code email_unavailable when email is not set up on the server (error codes are open: handle unknown ones by their HTTP status).
    • GET /v1/shared/alerts/log: today's alert emails (IST), newest first, at most 100, each with kind, status (sent / failed / queued), summary, recipients and the order's order_tag (also cirrus_tag). Emails go when an order on a shared account is rejected or could not be confirmed, or (optionally) when its broker login expires while it holds positions. Existing operations are unchanged.
  18. Additive

    Shared data: data age per account, order counts, one view across owners

    GET /v1/shared/consolidated: every account card gains updated_at (positions / margins / holdings: when each was last read from the broker, null when not shared or no current data), missing (each shared kind among positions, margins, holdings without data, with its code and reason), open_orders_count (null without open_orders), failed_orders_count (rejected or never confirmed; null without order_history) and orders_updated_at. Existing fields are unchanged. New operation GET /v1/shared/consolidated/{view} (positions, holdings, margins, open_orders, order_history): the rows of GET /v1/shared/{owner}/{view} for every shared account, each tagged with owner_username / owner_name, plus missing and skipped_owners (not_shared / unavailable). App session only.

  19. Additive

    Alice Blue positions count carried legs in buy / sell value

    Positions of Alice Blue accounts (GET /v1/positions, the positions webhook and /v1/stream): buy_value / sell_value now include a position carried in from an earlier session, valued at its entry price, as they already did for the other brokers and as buy_qty / sell_qty already did. Before, they held only today's trades, so sell_value - buy_value + net_qty x last price left out the carried leg. Rows without a carried leg, day_buy_value / day_sell_value, average_price and pnl are unchanged.

  20. Additive

    Up to 100 orders per request

    POST /v1/orders: a request may carry up to 100 orders (was 50); more is still refused with invalid_request on orders ("at most 100 orders per request"). The per-account order rate limit is unchanged: orders that find no slot within its wait are refused one by one (result reason rate_limited, with retry_after_ms), so a large basket on one account can be placed only in part.