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.
- Additive
Strategies read API (`GET /v1/strategies`, `/v1/strategies/{name}`)
Two new operations (tag
strategies, scoperead; app sessions, API keys and partner tokens withread): 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 (namenull =Unassigned) lists per account its rows (cirrus_token,symbol,tradingsymbol,product,net_qty,avg_price,realized,ltp,ltp_stale,unrealized) and totals, overalltotals(realized,unrealized,total,ltp_stale), areconsummary (statusok/pending/mismatchwith a plain-Englishnote) andat. Accounts without a ledger today are undermissing(codenot_available_yetorunreadable); with none at allavailableis false and the answer is still 200.GET /v1/strategies/{name}(~unassignedfor the rest) adds the strategy's v2triggers(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 examplesget_strategies,get_strategy. No existing operation changes. - Additive
Manual orders and baskets can name a strategy
POST /v1/orders: each order item takes an optionalstrategy(1-64 letters, digits,_,.,:or-, asclient_tag). It is stored on the order (the existing optionalstrategyon 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 422invalid_request,fieldorders[N].strategy.POST/PUT /v1/basketstake an optional basket-levelstrategy(same rule,fieldstrategy), returned byGET /v1/baskets/{basket_id}when set; a replace without it clears it. Requests withoutstrategybehave as before. The 400malformed_jsonmessage for an unknown basket field now listsstrategyamong the expected fields (examplescreate_basket,replace_basketupdated). - Additive
Order protection held by Cirrus can be a v2 trigger (`trigger_v2_id`)
POST /v1/orders: a result (results[]) gains an optionaltrigger_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 (onGET /v2/triggers).protectionstaystrigger; no other field changes, and the field is absent otherwise. New exampleprotected_by_v2_triggerofplace_orders.GET/PATCH/DELETE /v1/triggers/{trigger_id}with a v2 trigger's id still answer 404trigger_not_found, now saying to use/v2/triggers/{trigger_id}. New stored recordticket_protection(oms:protect2:{user|account}:{tag}, enumheld_by:v2,v1; optionalstrategy, the order's, carried to its brackets); written only with the switch on, after every pod reads it. - Additive
Kotak Neo adapter; its conformance pack
Kotak Neo accounts (
BrokerKotakNeo;ACCOUNT_LIST_DATAAccess Token,SID,Data CenterandBase 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/accountsreports themsupported, updatesstream(Kotak's order & position feed nudges an order-book read; open orders are also checked one by one). Calls go to the account's ownBase URL(only anhttps://*.kotaksecurities.comhost;KOTAKNEO_BASE_URL, defaulthttps://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 newkotakneopack atreplay_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 maskSid/Authfields. - 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'sended_reasoncan beday_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) orposition_closed(the position it protected is confirmed flat). v2 (GET /v2/triggers,/v2/triggers/{trigger_id}, thetrigger_updatestream message and webhook): such a trigger moves to the existing statecancelledwithreasonday_endedorcontract_expired; its journey's transition summary says it in words ("Ended at market close — intraday position", "Ended — contract expired").ended_reasonandreasonwere already open strings; no field, state or enum value is added. - Additive
v2 engine events carry the engine's clock (engine_at)
Stored record
trg_event({ENV}.trg.evt.…, also intrg_snapshotoutboxes) gains an optionalengine_at(epoch ms): the engine's clock when it evaluated the tick that made the transition, set when it differs fromat(atstays the tick's exchange time, whole seconds on most feeds). Absent on older events and whenatis already the engine's clock; readers fall back toat, and older readers ignore it. The v2 exit'shit_to_placed_mslog field andoms_trg_hit_to_order_sent_secondsnow 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. - 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/streamsnapshot/order_updatemessages and theorder_updatewebhook now show one order per part, each with its own brokerorder_id,quantity,filled_qty,average_price,stateandstatus_historyas 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 optionalsplit_from(SplitFrom: the placedorder_id,order_tagandquantity), absent on every other order. The first part keeps the placedorder_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 (422invalid_request,field: quantity), and so is any call when no part is open (409order_not_open). Those answers carry the new optionalsplit_orders(PartResult:order_id,status,message,reasonper part);statusisacceptedonly 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.messageof 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, thenrejectedonce the book settles it) is unchanged. - Additive
Dependent orders stamped with their own send time in the activity log
GET /v1/activity,GET /v1/activity/{id}and the/v1/streamactivitymessage: the record of a dependent order (depends_on) now hasat= when its wait for the parent ended and it was sent (or refused withdependency_failed), the earliest such moment across its accounts, andduration_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 withoutdepends_on(single orders, plain batches, the first wave of a sequenced request) are unchanged. New optional fielddepends_on(ActivityDependsOn) on dependent orders' records only, absent on every other record:order_ref(the parent's activity recordid),tradingsymbolandsideof the parent,waited_ms(longest wait in any account;nullwhen the parent was never placed) andoutcome(filled,partial,failedortimeout; across accounts the least complete). - 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.
- 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 returnswebhook_secret(whsec_...) once.POST /v1/partner-apps/{client_id}/rotate-webhook-secretissues a new one; the previous one keeps signing for 24 hours. Own app session only. Both can answer 409conflict(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, theorder_updatewebhook):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 carryusernameat the top level, and theiraccount_alertmay have kindaccount_removed. Users' own webhooks get no new event, alert kind or top-level field; the only change they see ispartner_client_idon orders a partner app placed (above). - Partner app bodies are documented as their own schemas:
PartnerWebhookOrderUpdate,PartnerWebhookTrade,PartnerWebhookAccountAlert(kindsPartnerWebhookAlertKind: the user alert kinds exceptwebhook_disabled, plusaccount_removed) andWebhookConnectionRevoked, togetherPartnerWebhookBody, each requiringusername; referenced fromPATCH /v1/partner-apps/{client_id}.WebhookConnectionRevokedand theconnection_revokedwebhook now describe the partner app's receiver and its owner (not the user's webhooks). User webhook schemas,WebhookAlertKindandWebhookEventare unchanged. - Rollout: the two webhook routes answer 503 and no partner app delivery (
order_update,trade,account_alert,connection_revoked,account_removed) is sent untilOMS_PARTNER_WEBHOOKSis turned on; the routes are enabled only with that flag.partner_client_idon orders is written and shown regardless of the flag.
- 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 webhookeventsnor listed inWebhookEventType. Nothing writes it untilOMS_PARTNER_WEBHOOKSis turned on, after every pod runs this build. - Additive
MARKET orders priced from the last price when it is not recent
POST /v1/orders,PATCH /v1/orders/{order_id}andPOST /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 withstale_quote; it goes out as a LIMIT protected from the last price held, as a fresh price would set it.stale_quoteis 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. TheOMS_MARKET_WITHOUT_FRESH_PRICEsetting is removed. - 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" isstatic_ip_not_registered. - 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}:sland a target leg{id}:tp; exiting a filled one squares it off./v1/accountsprotection for Nubra now showsbracket,coverandgtttrue. - Additive
Dependent orders can follow a partial fill; saved baskets
POST /v1/orders: two optional fields on an order withdepends_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 422invalid_requestwithoutdepends_on;depends_on_timeout_msoutside 1000-30000 is refused with 422invalid_request(fieldorders[i].depends_on_timeout_ms).depends_on_partial: "filled_qty"on an order that another order of the request depends on is refused with 422invalid_request(fieldorders[i].depends_on_partial). Withfilled_qtythe wait ends once every slice of the parent in that account has ended, or at the timeout.POST /v1/margins/ordersshares 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 carriesreduced_from(the planned units); nothing else in the response changes and no newreasonvalue exists. NewGET/POST /v1/basketsandGET/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'sbasket_idanswers 404 with the new error codebasket_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). - Additive
Alice Blue positions count carried legs in buy / sell value
Positions of Alice Blue accounts (
GET /v1/positions, thepositionswebhook and/v1/stream):buy_value/sell_valuenow include a position carried in from an earlier session, valued at its entry price, as they already did for the other brokers and asbuy_qty/sell_qtyalready did. Before, they held only today's trades, sosell_value - buy_value + net_qty x last priceleft out the carried leg. Rows without a carried leg,day_buy_value/day_sell_value,average_priceandpnlare unchanged. - Additive
Up to 100 orders per request
POST /v1/orders: a request may carry up to 100 orders (was 50); more is still refused withinvalid_requestonorders("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 (resultreasonrate_limited, withretry_after_ms), so a large basket on one account can be placed only in part.