GodEngine decision api

Private beta

API reference · v1 · engine ge-decisions/1.1.0

API reference

Two routes, one signed service token, strict validation. Everything here is taken from the service's code and its own documentation; limits are the values the code enforces.

Overview

Routes
RouteScopePurpose
POST /v1/decisionsdecisions:writeMake a decision, or replay the stored one for a reused idempotency key
GET /v1/decisions/{id}decisions:readRead a record back, scoped to your tenant
  • Base URL. Given with your invitation. Routes are written relative to it. By design the service answers only direct calls from allowed private networks; see access.
  • Format. Request and response bodies are JSON (Content-Type: application/json). Responses carry Cache-Control: no-store.
  • Versions. The route prefix is v1. Every record carries engine_version (today ge-decisions/1.1.0), so you can tell which scorer produced it.
  • Schema. The service has its own OpenAPI document, kept out of any public schema. Invited callers can ask for it.

Authentication

Every call carries a gst1 service token as a bearer credential. It proves which service is calling, which tenant it acts for and that the proof is still fresh.

Headerhttp
Authorization: Bearer gst1.<base64url claims>.<base64url signature>

The token is gst1., then the base64url-encoded JSON claims, then a base64url HMAC-SHA256 signature over "gst1." + claims. You do not build tokens yourself: a GodEngine operator issues them, and there is no self-serve token endpoint.

Token claims
ClaimMeaningFormat
svcThe calling service. Stored on every record it creates, for attribution.1–64 of A-Z a-z 0-9 _ . : -, starting with a letter or digit
tidThe tenant the token acts for, or * for a broker service that names the tenant on each request1–128 of A-Z a-z 0-9 : _ . @ -
scpScopes: decisions:write (POST), decisions:read (GET)a list of 1–8 names
iat, expIssued-at and expiryinteger unix seconds
jtiA random token id. It is logged when a token is accepted (the token itself never is) and is how one token is revoked.1–64 of A-Z a-z 0-9 _ -
  • Lifetime. 24 hours by default and 7 days at most, with 60 seconds of clock skew allowed. Fetch a fresh token well before expiry rather than holding a long-lived one.
  • Revocation. One token can be revoked by its jti before it expires; rotating the signing secret revokes every token at once. A revoked token is a 401.
  • Checks. The signature is compared in constant time, then every claim is checked against the formats above. A well-signed token with a malformed claim is still a 401.
  • Order. The token is checked before the body is read, so an unauthenticated caller learns nothing about the schema.

SecretA gst1 token is a bearer credential until it expires. Keep it out of source control, logs, URLs, error reports and any code that runs in a browser.

Tenants

A tenant id is an organisation id where one exists, otherwise u: followed by a user id. Allowed characters are A-Z a-z 0-9 : _ . @ -, up to 128.

How the tenant is decided
TokenPOSTGET
Concrete (tid = org_a)Tenant comes from the token. A body tenant_id must match it or be left out; a mismatch is 403.Tenant comes from the token. An X-Tenant-Id header, if sent, must match; a mismatch is 403.
Broker (tid = *)Send tenant_id in the body. Missing is 400.Send X-Tenant-Id. Missing is 400.

A malformed tenant id is 422. Reads always filter on the id and the tenant: an unknown id and another tenant's id get the same 404.

POST /v1/decisions

POST/v1/decisionsdecisions:write

Makes a decision and stores it, or returns the stored record when the idempotency key was used before with the same inputs.

Request body

Top-level fields (any other field is 422)
FieldTypeRequiredRules
questionstringyes1–2,000 characters. A horizon in it (“over the next 2 years”) is read as the cost horizon.
optionsarray of Optionyes2–20 options with unique ids
idempotency_keystringyes1–200 printable ASCII characters, no spaces
evidencearray of EvidencenoAt most 50 items. Their text and values together are at most 40,000 characters.
constraintsobjectnoOnly the keys under constraints; at most 30 keys and about 8,000 characters, nested at most 8 levels
task_idstringno1–128 of A-Z a-z 0-9 _ . : -. Your task this decision serves; part of the inputs digest.
tenant_idstringbroker tokensUp to 128 characters; see tenants
Evidence object (needs text or value)
FieldTypeRules
sourcestring, required1–200 characters. Sources the engine read are listed back in evidence_refs.
textstringUp to 4,000 characters. Scanned for budgets, deadlines and quotes.
valuenumber or stringA string value is at most 200 characters
unitstringUp to 40 characters, for example lakh

From evidence text the engine reads budgets (“budget”, “can spend”, “at most” or “approved” near an amount), deadlines (“within 3 months”) and quotes that name exactly one option. A quote fills a cost the option is missing, and the record notes it. When evidence gives conflicting budgets, the tightest one is used. At most 20 amounts and 20 deadlines are read from any one text, and the record says when a text had more. Evidence is free-form: a value that cannot be read is left out of the model and named in the assumptions.

Option object
FieldTypeRules
idstring, required1–64 of A-Z a-z 0-9 _ . : -; unique in the request
labelstring, required1–200 characters
paramsobjectAt most 40 params. Names up to 64 characters; string values up to 200. Values are scalars or a one-level object such as {"value": 10, "unit": "lakh"}. See option params.

Responses

  • 201 Created, header Idempotent-Replay: false: a new DecisionRecord.
  • 200 OK, header Idempotent-Replay: true: the stored record for a reused key with the same inputs.
  • 409 Conflict: the key was used with different inputs. See idempotency.
  • Errors: see status codes. The body shape is described on the DecisionRecord page.

Constraints

These are the only keys. Any other key, a typo such as budgte included, is 422 at ["body", "constraints", "<key>"].

Constraint keys
KeyMeaning
budgetA number, a string such as "10 lakh", or {value, unit}. Overrides any budget found in the evidence. A recurring budget ("40k per month") is modelled as its total over the horizon, and the record says so.
budget_basistotal_over_horizon (default) compares the budget with upfront plus recurring spend over the horizon. upfront compares it with the one-off cost only (a recurring budget with upfront is 422).
horizon_months or horizonOverrides the horizon read from the question. Send one, not both.
deadline_months or deadlineA hard limit on time-to-value. Send one, not both.
max_riskA hard limit on the risk level
risk_tolerancelow, medium or high. Tilts the risk and upside weights.
weights{dimension: weight}, then normalised. Dimensions: affordability, upside, risk, speed, reversibility, fit, compliance. Each weight is a number from 0 to 1000; at least one dimension the options are actually scored on must be above zero.
exclude_optionsUp to 20 option ids from this request that you rule out
currencyThe decision's currency. Default: the first currency any input names, else INR. A budget in another currency than an explicit currency is 422.

A malformed weights or exclude_options is 422 too: a non-object, an unknown dimension, a negative, non-numeric, boolean or non-finite weight, all weights zero, a non-list, or an id that names no option.

Option params

The model reads these params, under any of the listed names. Any other param is reported in modelled_constraints.options.<id>.ignored_params and in the assumptions; it is never dropped silently. A known param whose value cannot be read is 422.

Params the model reads
Param namesModelled as
cost, upfront, price, capex, investmentone-off spend
monthly_cost, opex, rent, emi, subscription; annual_costrecurring spend
upside, benefit, impact, value, roiupside: 0 to 1, a percent, or low / medium / high
expected_value, expected_revenueupside, through return against total cost (money)
risk, downsiderisk (higher is worse)
time_months, time, lead_time, time_to_valuespeed; "6 weeks" works
reversible, reversibility, lock_inreversibility
strategic_fit, fit, alignmentfit
compliance, compliance_riskcompliance

A dimension that no option gives is left out of the score. A dimension some options give and others do not is filled with a neutral 0.5 for the others, counted in coverage.defaulted_cells.

Amounts

Amounts are read the way people write them. Lakh and crore imply INR. Nothing is converted between currencies: an option priced in another currency is not compared with the budget, and the record says so.

How amounts are read
InputValue
10 lakh1,000,000
45L4,500,000
₹90L9,000,000
Rs. 90,00,0009,000,000
₹1.2 cr12,000,000
0.1 crore1,000,000
50k50,000
$2.5M2,500,000 USD
30k per month30,000 a month
8.4 lakh/yr840,000 a year
Rs. 10,00,000/-1,000,000

In free text, a number counts as money only when it carries a currency or a magnitude word, so “over the next 2 years” is read as a horizon. In evidence, a spaced L, m or b needs a currency marker to count as a magnitude, because {"value": 500, "unit": "L"} from a tank gauge is litres.

Money fields are strict

constraints.budget and an option's money params (cost, monthly_cost, annual_cost, expected_value and their other names) must be exactly one amount: a JSON number, a string like those above, or {"value": 10, "unit": "lakh"}. A spaced single letter counts there ("10 L" is ₹10 lakh). Anything else is 422 with the field's location in detail[0].loc, and nothing is computed or stored.

Money values that are refused
ValueWhy
"-10 lakh", -1000000negative
"1e6", "10.5.3 lakh"not one number
"ten lakh", "approx 45 lakh"not only an amount
["10 lakh"], truenot a number or a string
"2000000000 crore", 1e16above the ceiling of 1015

The same rule holds for the other known fields: horizon, deadline, max_risk, risk_tolerance, budget_basis, currency, an option's time and its level params such as risk and upside.

GET /v1/decisions/{id}

GET/v1/decisions/{id}decisions:read
  • id: the record's UUID, as returned by the POST.
  • Authorization: a token with decisions:read.
  • X-Tenant-Id: required with a broker token; with a concrete token it may be left out.

200 returns the record as it was issued. 404 means the record is not found for this tenant; the answer is the same whether the id does not exist or belongs to someone else.

Idempotency

The key travels in the body as idempotency_key. There is no Idempotency-Key header. A key is scoped to the calling service within a tenant: (tenant_id, svc, idempotency_key). Two services in one tenant that both pick order-1 stay independent; neither replays the other's record or gets a 409 because of it. Records are still tenant data, so either service can GET the other's record by id.

Reusing a key
CaseResult
Same key, same inputs200 with Idempotent-Replay: true and the stored record, byte for byte. The engine does not run again.
Same key, different inputs409 {"detail": {"error": "idempotency_key_conflict", "message": "…", "decision_id": "…"}}. The stored record is not changed.

“Same inputs” means the same inputs_digest: a SHA-256 over the canonical question (whitespace collapsed), evidence, options, constraints and task_id when given. The check happens before anything is computed. If two retries race, a unique index settles it and the loser is answered from the winning record. Replays count towards the rate limit.

Validation and limits

Limits the code enforces
WhatLimitOver it
Body size1 MiB, chunked or not413
JSON nesting32 levels (8 inside constraints)422
NumbersFinite only: no NaN or Infinity; integers up to 1015 anywhere in the body422
StringsNo NUL character and no lone UTF-16 surrogate in any key or value (a valid escaped emoji pair is fine)422
Options2 to 20, unique ids, at most 40 params each422
EvidenceAt most 50 items; 40,000 characters of text and values in total422
AmountsAt most 1015; never negative in a money field422
DurationsHorizon, deadline and option time at most 1,200 months422
Unknown fieldsNot allowed at the top level, in evidence or option objects, or in constraints. Unknown option params are allowed and reported.422

The ceilings keep every derived number, such as total spend over the horizon or budget use, finite. As a last check, the store refuses any record that is not strict JSON, so nothing non-finite is ever stored and the idempotency key stays free for a corrected request. Reading a request costs time linear in its size; a worst-case body at the evidence cap is answered in tens of milliseconds.

Rate limits

Writes are limited per tenant and calling service. The default limits are 120 a minute and 3,000 an hour; a deployment can set others. Replays count.

  • Over the limit: 429 with Retry-After in seconds. Retry with the same idempotency key.
  • When the rate limiter is required and unavailable: 503 with Retry-After: 5.

Status codes

Every status the routes return
StatusWhen
200GET found the record; or POST replayed a stored record (Idempotent-Replay: true)
201POST made a new record
400A broker token did not name the tenant
401Missing, malformed, forged, expired or revoked token, or a claim outside the formats
403The token lacks the scope, or names a different tenant
404The service is not configured, the call did not come from an allowed network, or the record is not found for this tenant
409Idempotency key reused with different inputs
413Body larger than 1 MiB
422Body is not valid JSON, nests too deeply, has an unstorable string, fails the schema, or a known field cannot be read
429Over the per-tenant, per-service write limit
503The rate limiter is required and unavailable

Error bodies

Errors use a detail field. Validation errors are a list with a loc path to the field; messages never echo the value you sent.

422 on a money paramjson
{
  "detail": [
    {"type": "value_error", "loc": ["body", "options", 0, "params", "cost"], "msg": "…"}
  ]
}
422 on the body itselfjson
{
  "detail": [
    {"type": "json_invalid", "loc": ["body"],
     "msg": "Body must be JSON with finite numbers only, and integers no larger than 10^15"}
  ]
}