Triggers
Alerts, stop-loss, targets, entries, ladders and loss limits the server watches tick by tick.
List v2 triggers #
/v2/triggers scope readEvery v2 trigger of the caller, newest first, with its state.
More about this
Ended triggers stay listed for 8 days. ?group= lists only the triggers created with that group.
Also every protection GET /v1/triggers lists that is held as a v1 record (made before 2026-10-28 through /v1/triggers, the order ticket, a signal entry or /v2/triggers; a broker bracket / cover placed with an entry; a /v1/gtt record), as a bracket with plain price levels: route gtt, stop_order, bracket, cover or trigger, a trailing stop-loss as spec.stop_loss.trail.step, a GTT stop-loss and its server-side target as one trigger (its id the GTT's record's). Its id is v1's (not a ULID); created_via is absent. Ended ones (7 days, as on /v1/triggers) show why in state and reason.
Parameters
- group query · string
Only the triggers created with this
group(exact match).
Errors
| HTTP | Code | When |
|---|---|---|
| 401 | unauthorized | No credential, or the credential is invalid, expired or revoked. |
| 403 | forbidden | The credential may not use this route (missing scope, app-session-only route, IP not allowed). |
| 429 | rate_limited | Too many requests for this credential (or too many failed attempts); retry after the |
| 503 | service_unavailable | A dependency is temporarily unavailable, or the feature is off on this server; retry later. |
All fields (request and response schema)
Loading fields…
curl "https://api.cirrus.trade/v2/triggers" \
-H "Authorization: token $CIRRUS_API_KEY"import os
import requests
response = requests.get(
"https://api.cirrus.trade/v2/triggers",
headers={
"Authorization": f"token {os.environ['CIRRUS_API_KEY']}",
},
timeout=10,
)
print(response.status_code, response.json())const response = await fetch('https://api.cirrus.trade/v2/triggers', {
method: 'GET',
headers: {
Authorization: `token ${process.env.CIRRUS_API_KEY}`,
},
});
const body = await response.json();
console.log(response.status, body);{
"status": "success",
"data": [
{
"trigger_id": "01M5VVFCDFABPK086FBZD11WZ5",
"account": "P1",
"strategy": null,
"kind": "bracket",
"legs": [
{
"side": "BUY",
"quantity": 5,
"product": "MIS",
"instrument_token": "CT:TEST:RELIANCE"
}
],
"spec": {
"entry_px": 2500,
"target": {
"when": "ltp >= entry_px * 1.02"
},
"stop_loss": {
"init_sl": "entry_px - 25",
"trail": {
"step": "5"
}
}
},
"valid_until": null,
"state": "submitted",
"transition_seq": 0,
"reason": null,
"created_at": "2026-10-26T15:48:09.715+05:30",
"updated_at": "2026-10-26T15:48:09.715+05:30",
"protection": {
"stop_loss": {
"held_by": "server",
"broker_order_id": null,
"level": 2475,
"adjusted_by_server": false,
"status": "held_by_server",
"note": null
},
"target": {
"held_by": "server",
"broker_order_id": null,
"level": 2550,
"adjusted_by_server": false,
"status": "held_by_server",
"note": null
},
"trail": {
"held_by": "server",
"broker_order_id": null,
"level": 2475,
"adjusted_by_server": false,
"status": "held_by_server",
"note": null
}
},
"created_via": "app",
"route": "trigger"
}
],
"error": null
}Create a v2 trigger #
/v2/triggers scope triggersCreates a v2 trigger: an alert, a bracket (entry, target, stop-loss, trailing), a ladder (several targets) or a risk_limit (loss / profit limits on an account, the user or a strategy), with conditions on any tick field.
More about this
The trigger is checked first (its conditions compile, the account is the caller's, every leg's instrument exists; bracket and ladder legs need product), then handed to the trigger engine: 202 with the new trigger's id, made by the server. Read it with GET /v2/triggers/{trigger_id}; state is submitted until the engine reports. A bracket protecting a position already held (entry_px, no entry) may set its stop-loss at or beyond entry_px to lock in profit (long: entry_px <= init_sl < the current price; short mirrored); a waiting bracket's stop stays on the protective side of its entry.
Where the protection lives (route, default auto): a bracket (or ladder) protecting a position already held is always a v2 trigger the server evaluates, whose plain price stop-loss also rests at the broker, broker-native first: a broker GTT for a positional product where the broker has GTTs (one never larger than the position; a broker whose GTT takes one leg holds the stop-loss, the trigger the target), a broker stop-loss order (SL) for an intraday (MIS) position where the broker's stop orders are proven. The broker order is placed, and read back, before the 202, which says route gtt / stop_order and why in route_reason; the trigger watches every part and cancels the broker order before any exit it places. If the broker refuses it, the server holds the protection at once (route trigger, the broker's reason in route_reason); if the broker does not answer, nothing else is placed and the trigger keeps watching (504 broker_outcome_unknown, naming the trigger). route gtt / stop_order ask for that route (refused with 422 route where the trigger cannot take it); route trigger keeps every part with the server.
Parameters
- Idempotency-Key header · string
Optional: a new UUID per request (1-128 printable characters). A repeat with the same key gets the first answer back instead of creating another trigger. A
broker_outcome_unknownanswer is kept like any other: a repeat within 24 hours gets the same 504 back and creates nothing.
Body fields
- account string required
Account id (see
/v1/accounts). - kind string required
The machine that runs a trigger.
One of: alert, bracket, ladder, risk_limit
- spec object required
Kind-specific rules, conditions written in the CEL subset (
ltp <= 120 && bid_qty > ask_qty). Checked before anything is sent: an unknown field or a condition that does not compile is a 422. - strategy string | null
Strategy the trigger belongs to (risk limits at strategy level).
- legs object[]
- valid_until string | null
RFC 3339; the trigger expires then.
- watch object[]
Instruments the conditions read but the trigger never trades (at most 3), for indicator conditions:
[{"name": "nifty", "symbol": "<instrument token>"}]lets a condition saynifty.rsi(14, '5m')ornifty.ltp. Names are[a-z][a-z0-9_]{0,15}, not a variable or function name. Each counts toward the distinct-instrument limit. Fixed at create (a change keeps them). Refused (422) while indicator conditions are not available. - route string
Where a
bracketprotecting a held position lives.auto(default) puts a simple protection (one leg already held, a plain price stop-loss and / or target, nothing else) at the broker with the rules ofPOST /v1/triggers: a broker GTT for a positional product where the broker has GTTs (a one-leg-GTT broker holds the stop-loss, a server-side trigger the target), a broker stop-loss order for an intraday (MIS) position where the broker's stop orders are proven; anything else is a v2 trigger the server evaluates.triggeralways makes the v2 trigger (the earlier behaviour).gtt/stop_orderask for that route and are refused (422) whereautowould not take it;bracket/coverare refused.One of: auto, bracket, cover, gtt, trigger, stop_order
- group string | null
A label of your own (1-64 characters), returned on reads and filterable on list (
?group=): triggers made as one set (several targets as separate triggers) show as one. Free-form; nothing else reads it.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | malformed_json | The body is not JSON, or does not fit the request shape (unknown or missing fields, wrong types). |
| 400 | invalid_idempotency_key | The |
| 401 | unauthorized | No credential, or the credential is invalid, expired or revoked. |
| 403 | forbidden | The credential may not use this route (missing scope, app-session-only route, IP not allowed). |
| 404 | account_not_found | No linked broker account with this id for the caller. |
| 409 | request_in_progress | A request with this |
| 422 | invalid_request | The request is well-formed but not valid; |
| 422 | idempotency_key_reused | This |
| 429 | rate_limited | Too many requests for this credential (or too many failed attempts); retry after the |
| 503 | service_unavailable | A dependency is temporarily unavailable, or the feature is off on this server; retry later. |
| 504 | broker_outcome_unknown | The broker did not answer in time: it may or may not have applied the change. Check before retrying. |
All fields (request and response schema)
Loading fields…
curl -X POST "https://api.cirrus.trade/v2/triggers" \
-H "Authorization: token $CIRRUS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"account": "P1",
"kind": "bracket",
"legs": [
{
"instrument_token": "CT:TEST:RELIANCE",
"side": "BUY",
"quantity": 5,
"product": "MIS"
}
],
"spec": {
"entry_px": 2500,
"target": {
"when": "ltp >= entry_px * 1.02"
},
"stop_loss": {
"init_sl": "entry_px - 25",
"trail": {
"step": "5"
}
}
}
}'import os
import requests
response = requests.post(
"https://api.cirrus.trade/v2/triggers",
headers={
"Authorization": f"token {os.environ['CIRRUS_API_KEY']}",
},
json={
"account": "P1",
"kind": "bracket",
"legs": [
{
"instrument_token": "CT:TEST:RELIANCE",
"side": "BUY",
"quantity": 5,
"product": "MIS",
},
],
"spec": {
"entry_px": 2500,
"target": {
"when": "ltp >= entry_px * 1.02",
},
"stop_loss": {
"init_sl": "entry_px - 25",
"trail": {
"step": "5",
},
},
},
},
timeout=10,
)
print(response.status_code, response.json())const response = await fetch('https://api.cirrus.trade/v2/triggers', {
method: 'POST',
headers: {
Authorization: `token ${process.env.CIRRUS_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"account": "P1",
"kind": "bracket",
"legs": [
{
"instrument_token": "CT:TEST:RELIANCE",
"side": "BUY",
"quantity": 5,
"product": "MIS"
}
],
"spec": {
"entry_px": 2500,
"target": {
"when": "ltp >= entry_px * 1.02"
},
"stop_loss": {
"init_sl": "entry_px - 25",
"trail": {
"step": "5"
}
}
}
}),
});
const body = await response.json();
console.log(response.status, body);{
"status": "success",
"data": {
"trigger_id": "01M65Q5EWB0KC0D6ZG7PT7QVB3",
"command_id": "01M65Q5EWKP63YC0162BADKHRE",
"status": "accepted",
"route": "stop_order",
"route_reason": "broker stop-loss order holds the stop-loss, a server-side trigger the target"
},
"error": null
}One v2 trigger #
/v2/triggers/{trigger_id} scope readOne v2 trigger of the caller.
More about this
Another user's id is not found. A live protection held as a v1 record (see GET /v2/triggers) is read by its v1 id (either half of a GTT stop-loss and its server-side target).
Parameters
- trigger_id path · string required
The trigger's id.
Errors
| HTTP | Code | When |
|---|---|---|
| 401 | unauthorized | No credential, or the credential is invalid, expired or revoked. |
| 403 | forbidden | The credential may not use this route (missing scope, app-session-only route, IP not allowed). |
| 404 | trigger_not_found | No trigger with this id for the caller. |
| 429 | rate_limited | Too many requests for this credential (or too many failed attempts); retry after the |
| 503 | service_unavailable | A dependency is temporarily unavailable, or the feature is off on this server; retry later. |
All fields (request and response schema)
Loading fields…
curl "https://api.cirrus.trade/v2/triggers/<trigger_id>" \
-H "Authorization: token $CIRRUS_API_KEY"import os
import requests
response = requests.get(
"https://api.cirrus.trade/v2/triggers/<trigger_id>",
headers={
"Authorization": f"token {os.environ['CIRRUS_API_KEY']}",
},
timeout=10,
)
print(response.status_code, response.json())const response = await fetch('https://api.cirrus.trade/v2/triggers/<trigger_id>', {
method: 'GET',
headers: {
Authorization: `token ${process.env.CIRRUS_API_KEY}`,
},
});
const body = await response.json();
console.log(response.status, body);{
"status": "success",
"data": {
"trigger_id": "01M5VVFCDF60NDFJ5SMKG675R2",
"account": "P1",
"strategy": null,
"kind": "bracket",
"legs": [
{
"side": "BUY",
"quantity": 5,
"product": "MIS",
"instrument_token": "CT:TEST:RELIANCE"
}
],
"spec": {
"entry_px": 2500,
"target": {
"when": "ltp >= entry_px * 1.02"
},
"stop_loss": {
"init_sl": "entry_px - 25",
"trail": {
"step": "5"
}
}
},
"valid_until": null,
"state": "active",
"transition_seq": 1,
"reason": null,
"created_at": "2026-10-26T15:48:09.715+05:30",
"updated_at": "2026-10-26T15:48:09.719+05:30",
"protection": {
"stop_loss": {
"held_by": "server",
"broker_order_id": null,
"level": 2480,
"adjusted_by_server": false,
"status": "held_by_server",
"note": null
},
"target": {
"held_by": "server",
"broker_order_id": null,
"level": 2550,
"adjusted_by_server": false,
"status": "held_by_server",
"note": null
},
"trail": {
"held_by": "server",
"broker_order_id": null,
"level": 2475,
"adjusted_by_server": false,
"status": "held_by_server",
"note": null
}
},
"created_via": "app",
"route": "trigger"
},
"error": null
}Change a v2 trigger #
/v2/triggers/{trigger_id} scope triggersReplaces a v2 trigger's spec (and optionally its expiry).
More about this
The new spec is checked like on create; the engine then applies what the trigger's state allows (for example a stop-loss is never loosened, an entry cannot change once active). 202: read the trigger to see the result. Protection the broker holds (route gtt / stop_order) is changed at the broker first and answered once the broker agreed: its spec must stay a plain price stop-loss / target with the same parts, and valid_until cannot be set. A protection held as a v1 record (see GET /v2/triggers) is changed through v1's own code, at the broker first where the broker holds it, with v1's rules (a bracket keeps both parts, a cover a stop-loss only): its spec stays plain price levels with the same parts, its quantity and a trailing stop-loss as shown (422 otherwise); a trailing stop-loss is never set looser than where its trail moved it (409 trigger_busy, field rules: cancel and protect again for a looser stop). valid_until: absent keeps the expiry, null removes it. route: the current route (or auto) changes nothing; another is refused (422 route: cancel and create the trigger again). POST /v2/triggers/{trigger_id}/validate checks a change without making it.
Parameters
- trigger_id path · string required
The trigger's id.
- Idempotency-Key header · string
Optional: a new UUID per request (1-128 printable characters). A repeat with the same key gets the first answer back instead of acting again.
Body fields
- spec object required
The whole new spec (same shape as on create).
- valid_until string | null
New expiry (RFC 3339); unchanged when absent;
nullremoves it (the trigger then lives until it ends). - route string | null
Where the protection lives (
routeof the trigger). The route is chosen at create: the current one (orauto) is accepted and changes nothing; any other is refused (422route) with how to move it.One of: auto, bracket, cover, gtt, trigger, stop_order
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | malformed_json | The body is not JSON, or does not fit the request shape (unknown or missing fields, wrong types). |
| 400 | invalid_idempotency_key | The |
| 401 | unauthorized | No credential, or the credential is invalid, expired or revoked. |
| 403 | forbidden | The credential may not use this route (missing scope, app-session-only route, IP not allowed). |
| 404 | trigger_not_found | No trigger with this id for the caller. |
| 404 | account_not_found | No linked broker account with this id for the caller. |
| 409 | trigger_fired | The trigger already fired; its exit is being handled. |
| 409 | broker_held | The protection is held by the broker; change the broker order instead. |
| 409 | trigger_busy | The trigger changed while it was being updated; retry. |
| 409 | request_in_progress | A request with this |
| 422 | invalid_request | The request is well-formed but not valid; |
| 422 | idempotency_key_reused | This |
| 429 | rate_limited | Too many requests for this credential (or too many failed attempts); retry after the |
| 502 | broker_refused | The broker refused the change. |
| 503 | service_unavailable | A dependency is temporarily unavailable, or the feature is off on this server; retry later. |
| 504 | broker_outcome_unknown | The broker did not answer in time: it may or may not have applied the change. Check before retrying. |
All fields (request and response schema)
Loading fields…
curl -X PATCH "https://api.cirrus.trade/v2/triggers/<trigger_id>" \
-H "Authorization: token $CIRRUS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"spec": {
"entry_px": 2500,
"target": {
"when": "ltp >= entry_px * 1.02"
},
"stop_loss": {
"init_sl": "entry_px - 20",
"trail": {
"step": "5"
}
}
}
}'import os
import requests
response = requests.patch(
"https://api.cirrus.trade/v2/triggers/<trigger_id>",
headers={
"Authorization": f"token {os.environ['CIRRUS_API_KEY']}",
},
json={
"spec": {
"entry_px": 2500,
"target": {
"when": "ltp >= entry_px * 1.02",
},
"stop_loss": {
"init_sl": "entry_px - 20",
"trail": {
"step": "5",
},
},
},
},
timeout=10,
)
print(response.status_code, response.json())const response = await fetch('https://api.cirrus.trade/v2/triggers/<trigger_id>', {
method: 'PATCH',
headers: {
Authorization: `token ${process.env.CIRRUS_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"spec": {
"entry_px": 2500,
"target": {
"when": "ltp >= entry_px * 1.02"
},
"stop_loss": {
"init_sl": "entry_px - 20",
"trail": {
"step": "5"
}
}
}
}),
});
const body = await response.json();
console.log(response.status, body);{
"status": "success",
"data": {
"trigger_id": "01M4GF38AQ39WVJZHH8YWWNNQZ",
"command_id": "01M4GF38BEE9Y3N9T9M8EHNXKX",
"status": "accepted"
},
"error": null
}Cancel a v2 trigger #
/v2/triggers/{trigger_id} scope triggersCancels a v2 trigger.
More about this
202: the engine ends it within moments (state cancelled); one that fires first ends as fired instead. Protection the broker holds (route gtt / stop_order) is cancelled at the broker first (a stop order is read back final; one that already went off is exiting and is kept: 502, retry), then the trigger (a protection held as a v1 record, see GET /v2/triggers, is removed through v1's own code with its paired target): broker_cancel says how the broker answered. A refusal never removes it on its own: shown ended at the broker it is removed (cancelled), shown fired it ends as fired (409), otherwise it stays live with state cancel_pending (broker_cancel pending) and is re-checked until the broker shows a final state.
Parameters
- trigger_id path · string required
The trigger's id.
- Idempotency-Key header · string
Optional: a new UUID per request (1-128 printable characters). A repeat with the same key gets the first answer back instead of acting again.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | invalid_idempotency_key | The |
| 401 | unauthorized | No credential, or the credential is invalid, expired or revoked. |
| 403 | forbidden | The credential may not use this route (missing scope, app-session-only route, IP not allowed). |
| 404 | trigger_not_found | No trigger with this id for the caller. |
| 409 | trigger_fired | The trigger already fired; its exit is being handled. |
| 409 | request_in_progress | A request with this |
| 422 | idempotency_key_reused | This |
| 429 | rate_limited | Too many requests for this credential (or too many failed attempts); retry after the |
| 502 | broker_refused | The broker refused the change. |
| 503 | service_unavailable | A dependency is temporarily unavailable, or the feature is off on this server; retry later. |
All fields (request and response schema)
Loading fields…
curl -X DELETE "https://api.cirrus.trade/v2/triggers/<trigger_id>" \
-H "Authorization: token $CIRRUS_API_KEY"import os
import requests
response = requests.delete(
"https://api.cirrus.trade/v2/triggers/<trigger_id>",
headers={
"Authorization": f"token {os.environ['CIRRUS_API_KEY']}",
},
timeout=10,
)
print(response.status_code, response.json())const response = await fetch('https://api.cirrus.trade/v2/triggers/<trigger_id>', {
method: 'DELETE',
headers: {
Authorization: `token ${process.env.CIRRUS_API_KEY}`,
},
});
const body = await response.json();
console.log(response.status, body);{
"status": "success",
"data": {
"trigger_id": "01M4GF38B4NDN7T4SDYK85MKNM",
"command_id": "01M4GF38BK9BQ5Z9NTES6C5R31",
"status": "accepted"
},
"error": null
}Pause a v2 trigger #
/v2/triggers/{trigger_id}/pause scope triggersPauses a v2 trigger: the engine stops evaluating it (state paused) until it is resumed.
More about this
A paused trigger does not act on any condition, its stop-loss included; a part the broker holds stays at the broker. 202: the engine applies it within moments; read the trigger to see the result. A protection held as a v1 record (made before 2026-10-28, or a broker bracket / cover placed with an entry) cannot be paused (422).
Parameters
- trigger_id path · string required
The trigger's id.
- Idempotency-Key header · string
Optional: a new UUID per request (1-128 printable characters). A repeat with the same key gets the first answer back instead of acting again.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | invalid_idempotency_key | The |
| 401 | unauthorized | No credential, or the credential is invalid, expired or revoked. |
| 403 | forbidden | The credential may not use this route (missing scope, app-session-only route, IP not allowed). |
| 404 | trigger_not_found | No trigger with this id for the caller. |
| 409 | trigger_fired | The trigger already fired; its exit is being handled. |
| 409 | request_in_progress | A request with this |
| 422 | invalid_request | The request is well-formed but not valid; |
| 422 | idempotency_key_reused | This |
| 429 | rate_limited | Too many requests for this credential (or too many failed attempts); retry after the |
| 503 | service_unavailable | A dependency is temporarily unavailable, or the feature is off on this server; retry later. |
All fields (request and response schema)
Loading fields…
curl -X POST "https://api.cirrus.trade/v2/triggers/<trigger_id>/pause" \
-H "Authorization: token $CIRRUS_API_KEY"import os
import requests
response = requests.post(
"https://api.cirrus.trade/v2/triggers/<trigger_id>/pause",
headers={
"Authorization": f"token {os.environ['CIRRUS_API_KEY']}",
},
timeout=10,
)
print(response.status_code, response.json())const response = await fetch('https://api.cirrus.trade/v2/triggers/<trigger_id>/pause', {
method: 'POST',
headers: {
Authorization: `token ${process.env.CIRRUS_API_KEY}`,
},
});
const body = await response.json();
console.log(response.status, body);{
"status": "success",
"data": {
"trigger_id": "01M4QNBZ1SPDJ4F3K9NA9H72SC",
"command_id": "01M4QNBZ21PD87BG3EHS08AN78",
"status": "accepted"
},
"error": null
}Resume a paused v2 trigger #
/v2/triggers/{trigger_id}/resume scope triggersResumes a paused v2 trigger: the engine evaluates it again from the next tick, in the state it was paused in.
More about this
202: read the trigger to see the result (the engine refuses a resume of a trigger that is not paused). A protection held as a v1 record is never paused (422).
Parameters
- trigger_id path · string required
The trigger's id.
- Idempotency-Key header · string
Optional: a new UUID per request (1-128 printable characters). A repeat with the same key gets the first answer back instead of acting again.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | invalid_idempotency_key | The |
| 401 | unauthorized | No credential, or the credential is invalid, expired or revoked. |
| 403 | forbidden | The credential may not use this route (missing scope, app-session-only route, IP not allowed). |
| 404 | trigger_not_found | No trigger with this id for the caller. |
| 409 | trigger_fired | The trigger already fired; its exit is being handled. |
| 409 | request_in_progress | A request with this |
| 422 | invalid_request | The request is well-formed but not valid; |
| 422 | idempotency_key_reused | This |
| 429 | rate_limited | Too many requests for this credential (or too many failed attempts); retry after the |
| 503 | service_unavailable | A dependency is temporarily unavailable, or the feature is off on this server; retry later. |
All fields (request and response schema)
Loading fields…
curl -X POST "https://api.cirrus.trade/v2/triggers/<trigger_id>/resume" \
-H "Authorization: token $CIRRUS_API_KEY"import os
import requests
response = requests.post(
"https://api.cirrus.trade/v2/triggers/<trigger_id>/resume",
headers={
"Authorization": f"token {os.environ['CIRRUS_API_KEY']}",
},
timeout=10,
)
print(response.status_code, response.json())const response = await fetch('https://api.cirrus.trade/v2/triggers/<trigger_id>/resume', {
method: 'POST',
headers: {
Authorization: `token ${process.env.CIRRUS_API_KEY}`,
},
});
const body = await response.json();
console.log(response.status, body);{
"status": "success",
"data": {
"trigger_id": "01M4QNBZ1SH6BRRRMPJ7RRSVMY",
"command_id": "01M4QNBZ1Z8KTB78CXT3BMKEBB",
"status": "accepted"
},
"error": null
}Check a v2 trigger without creating it #
/v2/triggers/validate scope triggersA dry run of POST /v2/triggers with the same body: nothing is sent to the engine, stored or counted against the account's order budget.
More about this
200 with valid and every reason create would refuse it (errors, the same field and message as create's 422), what its conditions read (reads), where it stands against the limits (limits) and, for bracket / ladder, who would hold each part of its protection on this account and product (protection: the broker or the server).
**Expressions** (a strict CEL subset; numbers are doubles). Variables: leg 0's tick fields ltp, bid, ask, bid_qty, ask_qty, volume, oi, atp, open, high, low; prev.<field> (previous tick); legs[i].<field> and legs[i].prev.<field>; entry_px, sl_px, state; account.mtm, user.mtm, strategy.mtm, position.qty; now_ist (minutes since midnight IST, 09:15 = 555). Functions: crosses_above(a, b) / crosses_below(a, b) (against the previous tick; a gap counts), min, max, abs. Operators: || && ! == != < <= > >= + - * / and c ? a : b. Prices (init_sl, trail.step, ladder price) read only entry_px and literals. Limits: 2048 bytes and 32 levels of nesting per condition; the per-trigger limits in limits. Errors name the spec path and byte, e.g. when: unknown variable ltpp (at byte 0). Full reference with examples per kind: docs/api/trigger-expressions.md.
Body fields
- account string required
Account id (see
/v1/accounts). - kind string required
The machine that runs a trigger.
One of: alert, bracket, ladder, risk_limit
- spec object required
Kind-specific rules, conditions written in the CEL subset (
ltp <= 120 && bid_qty > ask_qty). Checked before anything is sent: an unknown field or a condition that does not compile is a 422. - strategy string | null
Strategy the trigger belongs to (risk limits at strategy level).
- legs object[]
- valid_until string | null
RFC 3339; the trigger expires then.
- watch object[]
Instruments the conditions read but the trigger never trades (at most 3), for indicator conditions:
[{"name": "nifty", "symbol": "<instrument token>"}]lets a condition saynifty.rsi(14, '5m')ornifty.ltp. Names are[a-z][a-z0-9_]{0,15}, not a variable or function name. Each counts toward the distinct-instrument limit. Fixed at create (a change keeps them). Refused (422) while indicator conditions are not available. - route string
Where a
bracketprotecting a held position lives.auto(default) puts a simple protection (one leg already held, a plain price stop-loss and / or target, nothing else) at the broker with the rules ofPOST /v1/triggers: a broker GTT for a positional product where the broker has GTTs (a one-leg-GTT broker holds the stop-loss, a server-side trigger the target), a broker stop-loss order for an intraday (MIS) position where the broker's stop orders are proven; anything else is a v2 trigger the server evaluates.triggeralways makes the v2 trigger (the earlier behaviour).gtt/stop_orderask for that route and are refused (422) whereautowould not take it;bracket/coverare refused.One of: auto, bracket, cover, gtt, trigger, stop_order
- group string | null
A label of your own (1-64 characters), returned on reads and filterable on list (
?group=): triggers made as one set (several targets as separate triggers) show as one. Free-form; nothing else reads it.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | malformed_json | The body is not JSON, or does not fit the request shape (unknown or missing fields, wrong types). |
| 401 | unauthorized | No credential, or the credential is invalid, expired or revoked. |
| 403 | forbidden | The credential may not use this route (missing scope, app-session-only route, IP not allowed). |
| 404 | account_not_found | No linked broker account with this id for the caller. |
| 429 | rate_limited | Too many requests for this credential (or too many failed attempts); retry after the |
| 503 | service_unavailable | A dependency is temporarily unavailable, or the feature is off on this server; retry later. |
All fields (request and response schema)
Loading fields…
curl -X POST "https://api.cirrus.trade/v2/triggers/validate" \
-H "Authorization: token $CIRRUS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"account": "P1",
"kind": "bracket",
"legs": [
{
"instrument_token": "CT:TEST:RELIANCE",
"side": "BUY",
"quantity": 5,
"product": "MIS"
}
],
"spec": {
"entry_px": 2500,
"target": {
"when": "ltp >= entry_px * 1.02"
},
"stop_loss": {
"init_sl": "entry_px - 25"
}
}
}'import os
import requests
response = requests.post(
"https://api.cirrus.trade/v2/triggers/validate",
headers={
"Authorization": f"token {os.environ['CIRRUS_API_KEY']}",
},
json={
"account": "P1",
"kind": "bracket",
"legs": [
{
"instrument_token": "CT:TEST:RELIANCE",
"side": "BUY",
"quantity": 5,
"product": "MIS",
},
],
"spec": {
"entry_px": 2500,
"target": {
"when": "ltp >= entry_px * 1.02",
},
"stop_loss": {
"init_sl": "entry_px - 25",
},
},
},
timeout=10,
)
print(response.status_code, response.json())const response = await fetch('https://api.cirrus.trade/v2/triggers/validate', {
method: 'POST',
headers: {
Authorization: `token ${process.env.CIRRUS_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"account": "P1",
"kind": "bracket",
"legs": [
{
"instrument_token": "CT:TEST:RELIANCE",
"side": "BUY",
"quantity": 5,
"product": "MIS"
}
],
"spec": {
"entry_px": 2500,
"target": {
"when": "ltp >= entry_px * 1.02"
},
"stop_loss": {
"init_sl": "entry_px - 25"
}
}
}),
});
const body = await response.json();
console.log(response.status, body);{
"status": "success",
"data": {
"valid": true,
"errors": [],
"reads": {
"symbols": [
"CT:TEST:RELIANCE"
],
"fields": [
"ltp"
],
"variables": [
"entry_px",
"sl_px"
]
},
"limits": {
"ok": true,
"reason": null,
"legs": 1,
"max_legs": 8,
"expressions": 2,
"max_expressions": 32,
"bytes": 310,
"max_bytes": 16384
},
"protection": {
"parts": {
"stop_loss": {
"held_by": "server",
"level": 2475,
"adjusted_by_server": false,
"note": null
},
"target": {
"held_by": "server",
"level": 2550,
"adjusted_by_server": false,
"note": null
}
},
"reason": "the position has not been checked yet"
},
"route": "stop_order",
"route_reason": "broker stop-loss order holds the stop-loss, a server-side trigger the target"
},
"error": null
}Check a change to a v2 trigger without making it #
/v2/triggers/{trigger_id}/validate scope triggersA dry run of PATCH /v2/triggers/{trigger_id} with the same body: nothing is sent to the engine or the broker.
More about this
200 with valid and every reason the change would be refused (errors, the same field and message as the change's 422), what changes in the spec (diff) and the expiry the trigger would have.
Parameters
- trigger_id path · string required
The trigger's id.
Body fields
- spec object required
The whole new spec (same shape as on create).
- valid_until string | null
New expiry (RFC 3339); unchanged when absent;
nullremoves it (the trigger then lives until it ends). - route string | null
Where the protection lives (
routeof the trigger). The route is chosen at create: the current one (orauto) is accepted and changes nothing; any other is refused (422route) with how to move it.One of: auto, bracket, cover, gtt, trigger, stop_order
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | malformed_json | The body is not JSON, or does not fit the request shape (unknown or missing fields, wrong types). |
| 401 | unauthorized | No credential, or the credential is invalid, expired or revoked. |
| 403 | forbidden | The credential may not use this route (missing scope, app-session-only route, IP not allowed). |
| 404 | trigger_not_found | No trigger with this id for the caller. |
| 404 | account_not_found | No linked broker account with this id for the caller. |
| 409 | trigger_fired | The trigger already fired; its exit is being handled. |
| 429 | rate_limited | Too many requests for this credential (or too many failed attempts); retry after the |
| 503 | service_unavailable | A dependency is temporarily unavailable, or the feature is off on this server; retry later. |
All fields (request and response schema)
Loading fields…
curl -X POST "https://api.cirrus.trade/v2/triggers/<trigger_id>/validate" \
-H "Authorization: token $CIRRUS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"spec": {
"entry_px": 2500,
"target": {
"when": "ltp >= entry_px * 1.02"
},
"stop_loss": {
"init_sl": "entry_px - 20"
}
},
"valid_until": null
}'import os
import requests
response = requests.post(
"https://api.cirrus.trade/v2/triggers/<trigger_id>/validate",
headers={
"Authorization": f"token {os.environ['CIRRUS_API_KEY']}",
},
json={
"spec": {
"entry_px": 2500,
"target": {
"when": "ltp >= entry_px * 1.02",
},
"stop_loss": {
"init_sl": "entry_px - 20",
},
},
"valid_until": None,
},
timeout=10,
)
print(response.status_code, response.json())const response = await fetch('https://api.cirrus.trade/v2/triggers/<trigger_id>/validate', {
method: 'POST',
headers: {
Authorization: `token ${process.env.CIRRUS_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"spec": {
"entry_px": 2500,
"target": {
"when": "ltp >= entry_px * 1.02"
},
"stop_loss": {
"init_sl": "entry_px - 20"
}
},
"valid_until": null
}),
});
const body = await response.json();
console.log(response.status, body);{
"status": "success",
"data": {
"valid": true,
"errors": [],
"diff": [
{
"path": "stop_loss.init_sl",
"from": "entry_px - 25",
"to": "entry_px - 20"
}
],
"valid_until": null
},
"error": null
}What v2 triggers can do #
/v2/triggers/capabilities scope readThe kinds and their spec fields, the expression language (variables, functions, operators and its size limits), the limits this deployment runs with and, with ?account=, what that account's broker can hold (order types, and each protection fact verified or unverified) and whether broker protection is on.
More about this
See docs/api/trigger-expressions.md.
Parameters
- account query · string
One of the caller's accounts: adds what its broker can hold.
Errors
| HTTP | Code | When |
|---|---|---|
| 401 | unauthorized | No credential, or the credential is invalid, expired or revoked. |
| 403 | forbidden | The credential may not use this route (missing scope, app-session-only route, IP not allowed). |
| 404 | account_not_found | No linked broker account with this id for the caller. |
| 429 | rate_limited | Too many requests for this credential (or too many failed attempts); retry after the |
All fields (request and response schema)
Loading fields…
curl "https://api.cirrus.trade/v2/triggers/capabilities" \
-H "Authorization: token $CIRRUS_API_KEY"import os
import requests
response = requests.get(
"https://api.cirrus.trade/v2/triggers/capabilities",
headers={
"Authorization": f"token {os.environ['CIRRUS_API_KEY']}",
},
timeout=10,
)
print(response.status_code, response.json())const response = await fetch('https://api.cirrus.trade/v2/triggers/capabilities', {
method: 'GET',
headers: {
Authorization: `token ${process.env.CIRRUS_API_KEY}`,
},
});
const body = await response.json();
console.log(response.status, body);{
"status": "success",
"data": {
"kinds": [
{
"kind": "alert",
"description": "Notifies once `when` holds, then ends (`fired`).",
"legs": "one or more",
"spec_fields": [
{
"name": "when",
"type": "condition",
"required": true,
"description": "Tick fields of every leg, risk variables, `now_ist`."
},
{
"name": "on",
"type": "string",
"required": false,
"description": "When the condition next to it is checked: `close` (default when it reads an indicator: at each candle close, with closed values) or `tick` (every price tick, with the forming candle)."
}
]
},
{
"kind": "bracket",
"description": "Entry (a condition, or a known `entry_px`), target, stop-loss and trailing stop on one position.",
"legs": "one",
"spec_fields": [
{
"name": "entry.when",
"type": "condition",
"required": false,
"description": "Waits (`pending`) until it holds; `entry_px` becomes that tick's LTP. Exclusive with `entry_px`."
},
{
"name": "entry.on",
"type": "string",
"required": false,
"description": "When the condition next to it is checked: `close` (default when it reads an indicator: at each candle close, with closed values) or `tick` (every price tick, with the forming candle)."
}
]
}
],
"expressions": {
"variables": [
{
"name": "ltp",
"description": "Leg 0's tick field; `prev.<field>` the previous tick's, `legs[i].<field>` leg i's."
},
{
"name": "bid",
"description": "Leg 0's tick field; `prev.<field>` the previous tick's, `legs[i].<field>` leg i's."
}
],
"functions": [
{
"name": "crosses_above",
"description": "crosses_above(a, b[, band]): a was at or below b on the previous tick and is above it now. Over an indicator, or with a dead band `band` (a number 0 or more), it fires once per real crossing: values inside b ± band keep the last side, and the first side seen only arms. Tick mode on a bracket defaults the band to 1 point (RSI, Stochastic) or 0.05 % of the level (prices)."
},
{
"name": "crosses_below",
"description": "crosses_below(a, b[, band]): a was at or above b on the previous tick and is below it now; `band` as for crosses_above."
}
],
"operators": [
"||",
"&&"
],
"max_source_bytes": 2048,
"max_depth": 32,
"max_nodes": 256
},
"limits": {
"max_triggers": 500,
"max_legs": 8,
"max_expressions": 32,
"max_symbols": 100,
"max_trigger_bytes": 16384,
"max_rungs": 20
},
"entries_enabled": false,
"risk_targets_enabled": false,
"products": [
{
"segment": "equity",
"exchanges": [
"NSE",
"BSE"
],
"products": [
"MIS",
"DELIVERY"
]
},
{
"segment": "derivatives",
"exchanges": [
"NFO",
"BFO"
],
"products": [
"MIS",
"CARRYFORWARD"
]
}
],
"account": null
},
"error": null
}A v2 trigger's journey #
/v2/triggers/{trigger_id}/journey scope readEverything that happened to one of the caller's v2 triggers, newest first: what was asked (api: created, modified with the spec diff, paused, resumed, cancelled, with who asked), what the engine did and why (engine: each transition with the condition that decided it and the values it read, e.g.
More about this
ltp=2479.5, sl_px=2480 in ltp <= sl_px; notes such as an input not known), commands it refused (result), exit orders (exit) and the broker protection (protection). Kept for 90 days by default, also after the trigger ended. Another user's trigger is not found.
Parameters
- trigger_id path · string required
The trigger's id.
- limit query · integer
Entries per page, 1 to 200 (default 50).
- cursor query · string
next_cursorof the previous page. - source query · string
Only entries from
api,engine,result,exitorprotection.
Errors
| HTTP | Code | When |
|---|---|---|
| 400 | invalid_query | The query string does not fit the route's parameters. |
| 401 | unauthorized | No credential, or the credential is invalid, expired or revoked. |
| 403 | forbidden | The credential may not use this route (missing scope, app-session-only route, IP not allowed). |
| 404 | trigger_not_found | No trigger with this id for the caller. |
| 429 | rate_limited | Too many requests for this credential (or too many failed attempts); retry after the |
| 503 | service_unavailable | A dependency is temporarily unavailable, or the feature is off on this server; retry later. |
All fields (request and response schema)
Loading fields…
curl "https://api.cirrus.trade/v2/triggers/<trigger_id>/journey" \
-H "Authorization: token $CIRRUS_API_KEY"import os
import requests
response = requests.get(
"https://api.cirrus.trade/v2/triggers/<trigger_id>/journey",
headers={
"Authorization": f"token {os.environ['CIRRUS_API_KEY']}",
},
timeout=10,
)
print(response.status_code, response.json())const response = await fetch('https://api.cirrus.trade/v2/triggers/<trigger_id>/journey', {
method: 'GET',
headers: {
Authorization: `token ${process.env.CIRRUS_API_KEY}`,
},
});
const body = await response.json();
console.log(response.status, body);{
"status": "success",
"data": {
"entries": [
{
"entry_id": "evt:01M4QRFDBAQC442FN7H0ATGR3A:1",
"at": "2026-10-12T15:23:05.396+05:30",
"seq": 1,
"source": "engine",
"kind": "transition",
"summary": "active -> active",
"actor": null,
"details": {
"from": "active",
"to": "active",
"actions": [
{
"type": "move_sl",
"legs": [
0
],
"price": 2480
}
],
"tick": {
"symbol": "CT:TEST:RELIANCE",
"ltp": 2551
},
"reason": null,
"explain": null
}
},
{
"entry_id": "api:01M4QRFDBBMKYVDBYQ05TTGVX2",
"at": "2026-10-12T15:23:05.386+05:30",
"seq": 0,
"source": "api",
"kind": "created",
"summary": "created in the app (command 01M4QRFDBBMKYVDBYQ05TTGVX2)",
"actor": {
"user": "user-01M4QRFD9EACA9SER6K3EHY0BQ",
"credential": "app"
},
"details": {
"kind": "bracket",
"legs": [
{
"symbol": "CT:TEST:RELIANCE",
"side": "BUY",
"qty": 5,
"product": "MIS"
}
],
"spec": {
"entry_px": 2500,
"target": {
"when": "ltp >= entry_px * 1.02"
},
"stop_loss": {
"init_sl": "entry_px - 25"
}
},
"valid_until": null
}
}
],
"next_cursor": null
},
"error": null
}A v2 trigger's indicator values now #
/v2/triggers/{trigger_id}/indicators scope readEach indicator a trigger's conditions read, as the engine holds it now: the last closed candle's value, the forming candle's, its warm-up status, how far it is from the condition's level and a tick-mode hold in progress.
More about this
Read from the engine's memory (nothing is computed or stored for it); poll every 2 s while a page shows it. status unavailable when the engine did not answer within 500 ms. At most one request per second per trigger.
Parameters
- trigger_id path · string required
The trigger's id.
Errors
| HTTP | Code | When |
|---|---|---|
| 401 | unauthorized | No credential, or the credential is invalid, expired or revoked. |
| 403 | forbidden | The credential may not use this route (missing scope, app-session-only route, IP not allowed). |
| 404 | trigger_not_found | No trigger with this id for the caller. |
| 429 | rate_limited | Too many requests for this credential (or too many failed attempts); retry after the |
| 503 | service_unavailable | A dependency is temporarily unavailable, or the feature is off on this server; retry later. |
All fields (request and response schema)
Loading fields…
curl "https://api.cirrus.trade/v2/triggers/<trigger_id>/indicators" \
-H "Authorization: token $CIRRUS_API_KEY"import os
import requests
response = requests.get(
"https://api.cirrus.trade/v2/triggers/<trigger_id>/indicators",
headers={
"Authorization": f"token {os.environ['CIRRUS_API_KEY']}",
},
timeout=10,
)
print(response.status_code, response.json())const response = await fetch('https://api.cirrus.trade/v2/triggers/<trigger_id>/indicators', {
method: 'GET',
headers: {
Authorization: `token ${process.env.CIRRUS_API_KEY}`,
},
});
const body = await response.json();
console.log(response.status, body);