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
Connection limits say which tokens they bind
GET|PUT /v1/connections/{client_id}/limitsresponses gainapplies_to(array, currently always["mcp"]): the limits bindmcp(AI agent) tokens only; classic partner tokens of the connection are not limited by them. Field and operation descriptions reworded. - 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 with409trigger_busy(an existing code) andfieldrules("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 aspecerror.GET /v2/triggersandGET /v2/triggers/{trigger_id}: a protection held as a v1 record whose entry has not filled (livefalseon/v1/triggers) showsstatepending(already a documented value), notactive. Thestatedescription 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 onDELETE
/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 withreasonstop_loss_executed_by_broker(as when a broker stop-loss GTT executes), notcancelled. Only a server-held target naming the stop back is ended. - 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_cancelpending) keeps its target until the broker ends the stop. Once the broker shows the stop cancelled, ended or fired, the target now ends too (reasoncancelled) for every record: made before 2026-10-28 throughPOST /v1/triggers, the order ticket or a signal entry, or through/v2/triggers, and whether it was cancelled throughDELETE /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.
- 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 (
- Change
Pocketful closed positions report exact realised P&L
Behaviour (no field added, removed or retyped; no new enum value)
- Pocketful
realized_mtmis 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 reportsrealised_pnl= sold value - bought value, rounded to paise; an open row keeps the broker's FIFO figure. 19 Pocketfulpositions-*expectations re-recorded,realised_pnlvalues only.
- Pocketful
- 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 403forbiddennaming 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 fromMISto an overnight product is also valued againstmax_order_value) andPOST /v2/deployments/{deployment_id}/signalswithactionenter(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 exceedmax_orders_per_day. The OpenAPI document lists the 403. - Additive
Edit a connection's limits
New operations
GET /v1/connections/{client_id}/limits(get_connection_limits) andPUT /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) andis_default: true. PUT takes the limits object (trading,accounts,paper_only,max_order_valuein whole rupees,max_orders_per_day,live_strategies,max_deployments), applies it at once on every server and records aconnection_limits_changedactivity with the before and after. Errors: 404connection_not_found(unknown or revoked connection), 422invalid_requestwithfield(accounts: not the user's, duplicate, empty or more than 50;live_strategies: needstrading,paper_onlyoff and thestrategies_livescope;max_order_value: too large). New examples. - 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 existingforbiddencode 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 afailedrecord 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 againstmax_orders_per_dayper IST day (Redisoms: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). - 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 toDeprecationandLink. - From that instant every request to them (any method, signed in or not) answers
410with the new error codegone(a newErrorCodevalue), a message naming/v2/triggersand 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 the410, documented with an example. /v1/gttandGET /v1/orders/{order_id}/protectionare 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.
- Additive
v1 protection calls are counted per day and caller
Stored-record change only: every authenticated call of the deprecated
/v1/triggersroutes adds one to the Redis hashoms:v1_triggers:calls:{YYYY-MM-DD}(IST day, kept 120 days), fieldapp:{username},key:{key_id}orpartner:{client_id}, so the owner can see who still calls them (docs/runbooks/v1-protection-sunset.md). After the sunset a signed-in caller's410is counted in the same hash asafter_sunset:{caller}. Counting never fails or slows a call, and changes nothing in their answers (the sunset is the entry above). - 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/triggersand/v2/triggers". On/v2/triggersthat held only for those made through/v2/triggers; the others (made throughPOST /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/gttrecords were on/v1/triggersonly (404on/v2/triggers/{trigger_id}). GET /v2/triggers(list_triggers_v2) now also lists every protectionGET /v1/triggerslists that is held as a v1 record, live and ended (7 days, as on v1), as abracketwith plain price levels:routegtt,stop_order,bracket,coverortrigger(values already in theProtectionRouteschema), a trailing stop-loss asspec.stop_loss.trail.step(points as they are, a percentage asentry_px * <pct> / 100), a GTT stop-loss and its server-side target as one trigger. Itstrigger_idis v1's (not a ULID),created_viais absent; an ended one shows why instateandreason(v1'sended_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) andPOST /v2/triggers/{trigger_id}/validatechange one through v1's own code: at the broker first where the broker holds it, with v1's rules and answers (502broker_refused,504broker_outcome_unknown,409trigger_fired; a bracket keeps both parts, a cover a stop-loss only). Its spec stays plain price levels with the same parts and novalid_until; its quantity cannot change; a trailing stop-loss is kept and must be sent as shown (422invalid_requestotherwise). A level sent as it stands keeps its v1 rule (points, percentage, trailing progress).routebracket/coveris accepted on a bracket / cover.DELETE /v2/triggers/{trigger_id}(cancel_trigger_v2) cancels one through v1's own code (broker first;502keeps it,409when it fired,broker_cancelpendingwhile 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:422invalid_request(fieldtrigger_id), nothing changed (v1 never paused one; a paused stop-loss would leave the position unprotected). Was404./v1/triggersis unchanged./v2/triggersis still only for the usersOMS_TRIGGERS_V2_USERSallows (*: everyone; seedocs/runbooks/v2-triggers-for-everyone.md).
- 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
- Additive
Partner connections store optional limits
Stored-record change only: partner connections (Mongo
partner_connections) gain an optionallimitsobject (trading,accounts,paper_only,max_order_valuein whole rupees,max_orders_per_day,live_strategies,max_deployments), mirrored in Redisoms:oauth:limits:{username}|{client_id}(JSON,nullwhen 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. - Additive
Connected apps show last used and calls today
GET /v1/connectionsitems gainlast_used_at(RFC 3339 ornull; updated at most once a minute) andcalls_today(requests the app made today, IST day). - 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 itspartner_revokedactivity asDisconnected “{name}”. Left running: {n} open orders, {g} GTT, {t} triggers, {d} strategies.(it wasDisconnected partner app “{name}”); the revoke response is unchanged. - 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. - 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.
- Additive
Activity filter by connection; recently revoked connections listed
GET /v1/activityaccepts an optionalclient_idquery parameter: only records made by that partner connection (theclient_idof the record'ssource). Without it nothing changes; other unknown parameters are still refused with 400.GET /v1/connectionsalso lists connections revoked in the last 30 days. Each carries a new optionalrevoked_at(RFC 3339); live connections do not have it. Revoking an already-revoked connection still answers 404.
- Additive
GTT and convert answers say when a refusal may pass
GttResult(POST /v1/gtt,PATCHandDELETE /v1/gtt/{trigger_id}) andConvertResult(POST /v1/positions/convert) gain an optionalreason, present only withstatus: rejectedwhen nothing reached the broker and a retry may pass:rate_limited(the account's order budget) orno_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).
- Additive
Idempotency-Key on every remaining order, GTT and v2 trigger write
DELETE /v1/orders/{order_id},PATCHandDELETE
/v1/orders/bracket/{order_id}
,DELETE /v1/gtt/{trigger_id},PATCHandDELETE /v2/triggers/{trigger_id},POST /v2/triggers/{trigger_id}/pauseand/resumetake the same optionalIdempotency-Keyheader, with the same answers (replay withIdempotent-Replayed: true, 400invalid_idempotency_key, 409request_in_progress, 422idempotency_key_reused). On aDELETEthe 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
rejectedanswer whosereasonmay 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).
- 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,PUTandDELETE
/v1/baskets/{basket_id}
take an optionalIdempotency-Keyheader (1-128 printable characters), as order placement and modify do. A repeat with the same key and body gets the first answer back withIdempotent-Replayed: trueand does nothing again; the same key with a different body or route is refused with 422idempotency_key_reused; a repeat while the first still runs gets 409request_in_progress. A refusal (4xx / 5xx) gives the key back, exceptbroker_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, 409request_in_progress, 422idempotency_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
rejectedtoday, so their behaviour does not change). The API description now declares their optionalIdempotency-Keyheader 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.
- 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 429slow_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_accesson,OMS_MCP_SERVICE_SECRET,OMS_MCP_PUBLIC_URL,OMS_OAUTH_ISSUER_URLandOMS_OAUTH_CONSENT_URLset): no change for deployments without it. POST /v1/oauth/token: a client registered this way authenticates withclient_idonly (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 themcpaudience: they work only through the MCP server. Aclient_namenaming a known AI vendor (Claude, ChatGPT, Cursor, ...) is refused withinvalid_requestunless every https redirect URI is that vendor's. Registrations nobody connected through are deleted after 30 days, and then get 401invalid_client.- Stored partner apps (Mongo
partner_apps) gain optionaldynamic(defaultfalse) andresource. Readers first: older pods read these records (owner@dcr, emptysecret_hash) and refuse their token requests; turn registration on (the settings above) only once every pod runs this build.GET /v1/partner-appsnever lists them.
- New