Before you start
BetaThe Decision API is a private beta and is not switched on for outside callers yet. By design it answers only direct calls from allowed private networks, so these steps work only for an invited service whose access has been set up. How access works.
From your invitation you need two things:
- A base URL. Routes on this site are written relative to it, for example
POST /v1/decisions. - A gst1 service token with the scope
decisions:write(to make decisions) anddecisions:read(to read them back). It names your service and, usually, your tenant. Tokens last 24 hours by default and at most 7 days. See authentication.
Keep the token out of source code, logs, URLs and anything that runs in a browser. Put both values in your shell:
# Both values come with your invitation
export GE_BASE="<base URL from your invitation>"
export GE_TOKEN="<gst1 service token>"
Write the request
Save this as decision.json. It is plain JSON: a question, the evidence you have, two to twenty options with the facts you know about each, an optional constraint and an idempotency key you choose.
{
"question": "Should we buy a CNC machine, lease one, or outsource machining over the next 2 years?",
"evidence": [
{"source": "finance", "text": "We have a 10 lakh budget for this."},
{"source": "vendor", "text": "Lease quote is ₹30,000 per month."},
{"source": "cfo", "value": 10, "unit": "lakh"}
],
"options": [
{"id": "buy", "label": "Buy outright",
"params": {"cost": "45 lakh", "upside": 0.9, "risk": "low", "time_months": 2}},
{"id": "lease", "label": "Lease",
"params": {"cost": "1.5 lakh", "monthly_cost": "30k", "upside": 0.6, "risk": "medium", "time_months": 1}},
{"id": "outsource", "label": "Outsource",
"params": {"monthly_cost": "25k", "upside": 0.35, "risk": "medium", "time_months": 0.5}}
],
"constraints": {"budget": "10 lakh"},
"task_id": "task-7f3a",
"idempotency_key": "order-42"
}
- Amounts can be written the way people write them:
"45 lakh","30k","₹1.2 cr","$2.5M"or{"value": 10, "unit": "lakh"}. Lakh and crore mean rupees. - The horizon (over the next 2 years) is read from the question and becomes 24 months. You can set it yourself with
constraints.horizon_months. task_idis optional: the id of your own task that this decision serves.- With a token that names your tenant, leave
tenant_idout. A broker token (one that acts for many tenants) must send"tenant_id"in the body.
Send it
curl -sS -i -X POST "$GE_BASE/v1/decisions" \
-H "Authorization: Bearer $GE_TOKEN" \
-H "Content-Type: application/json" \
--data-binary @decision.json
A new record answers 201. Two headers matter:
HTTP/1.1 201 Created
content-type: application/json
idempotent-replay: false
cache-control: no-store
Read the record
The body is a DecisionRecord. Start with these fields; the DecisionRecord page covers all of them.
{
"id": "6f1c…",
"tenant_id": "org_a",
"task_id": "task-7f3a",
"created_at": "2026-10-04T09:12:44.120Z",
"recommended_option": {"option_id": "lease", "label": "Lease", "score": 0.5756, "feasible": true,
"reasons": ["Costs ₹8.70 lakh over 24 months, within the ₹10 lakh budget (87% of it).", "…"]},
"alternatives": [
{"option_id": "outsource", "score": 0.5408, "feasible": true, "reasons": ["…"]},
{"option_id": "buy", "score": 0.3074, "feasible": false,
"reasons": ["Costs ₹45 lakh over 24 months, over the ₹10 lakh budget by ₹35 lakh.", "…"]}
],
"assumptions": ["Horizon of 24 months read from the question (\"2 year\").", "…"],
"feasibility": {"status": "feasible", "recommended_option_feasible": true,
"feasible_option_ids": ["lease", "outsource"],
"infeasible_options": {"buy": ["over_budget"]}},
"authorization": {"status": "not_authorized", "executable": false, "reason": "…"},
"reversal_conditions": [
{"kind": "budget_at_least", "threshold": 4500000, "currency": "INR", "switches_to": "buy",
"condition": "Budget rises to at least ₹45 lakh", "then": "'Buy outright' becomes the recommendation."}
],
"inputs_digest": "…",
"engine_version": "ge-decisions/1.1.0"
}
recommended_optionis the best feasible option. Buying is ranked last because it breaks the budget: an infeasible option is ranked below every feasible one, and its score is halved.- A
scoreranks options under the inputs you gave. It is not a probability. reversal_conditionstell you how settled the choice is: how far the budget, the horizon or one option's figures would have to move for the answer to change.- Store the
id. It is the durable handle for this decision.
Fetch it by id
curl -sS "$GE_BASE/v1/decisions/$DECISION_ID" \
-H "Authorization: Bearer $GE_TOKEN"
# With a broker token, name the tenant on reads:
# -H "X-Tenant-Id: org_a"
You get the record exactly as it was issued. An id that does not exist and an id that belongs to another tenant both answer 404, because whether a record exists is itself tenant data.
Retry safely
Networks fail. Send the same request again with the same idempotency_key:
HTTP/1.1 200 OK
content-type: application/json
idempotent-replay: true
cache-control: no-store
The body is the stored record, byte for byte; the engine does not run again. Change the inputs but keep the key, and the service refuses rather than overwrite:
{
"detail": {
"error": "idempotency_key_conflict",
"message": "This idempotency key was already used with different inputs.",
"decision_id": "6f1c…"
}
}
The key goes in the JSON body as idempotency_key; there is no Idempotency-Key header. Keys belong to your service within your tenant, so another service that picks the same key never collides with you. Use one key per real-world decision, for example your order or task id. Details: idempotency.
When something fails
| Status | Usually means | Do this |
|---|---|---|
| 400 | A broker token did not name the tenant | Send tenant_id in the body (POST) or X-Tenant-Id (GET) |
| 401 | Token missing, malformed, forged, expired or revoked | Get a fresh token; check the Bearer prefix |
| 403 | Token lacks the scope, or names a different tenant | Ask for the scope you need; drop or fix tenant_id |
| 404 | The service is not reachable from where you called, or the record is not in your tenant | Check the base URL and the network route from your invitation; check the id |
| 409 | Idempotency key reused with different inputs | Use a new key, or GET the record named in decision_id |
| 413 | Body larger than 1 MiB | Trim the evidence |
| 422 | Invalid JSON, unknown field, a value that cannot be read, or a limit exceeded | Read detail[0].loc: it names the field |
| 429 | Over your write limit | Wait for Retry-After seconds, then retry with the same key |
| 503 | The rate limiter is unavailable | Retry after the Retry-After header (5 seconds) |
A 422 looks like this. The message never echoes your value back:
{
"detail": [
{"type": "value_error", "loc": ["body", "constraints", "budgte"], "msg": "…"}
]
}
The same in Python
There is no SDK, and none is needed: it is plain HTTP and JSON. This sketch uses the requests package.
import json
import os
import requests # pip install requests
base = os.environ["GE_BASE"]
headers = {"Authorization": "Bearer " + os.environ["GE_TOKEN"]}
with open("decision.json", encoding="utf-8") as f:
body = json.load(f)
# The optional review step can take up to about 25 s, so allow time.
r = requests.post(base + "/v1/decisions", json=body, headers=headers, timeout=60)
if r.status_code in (200, 201):
record = r.json()
print("recommended:", record["recommended_option"]["option_id"])
print("replay:", r.headers.get("Idempotent-Replay"))
for rc in record["reversal_conditions"]:
print("-", rc["condition"], "->", rc["then"])
elif r.status_code in (429, 503):
print("retry after", r.headers.get("Retry-After"), "seconds, with the same key")
else:
print(r.status_code, r.json().get("detail"))
Next: the full API reference, or every field of the record.