API Reference
Call your published decision trees over HTTP, pull footprint and performance data, and manage trees remotely -- from any language that can make an HTTP request. Every endpoint below is scoped to your own account by your API key; nothing here can see or touch another owner's trees.
Not affiliated with any consulting or coaching service of the same name.
Getting started
Every request below is relative to your own EcoGovernance deployment's base URL -- e.g. https://your-ecogovernance-domain. Replace it with wherever your instance is actually hosted.
Create an API key from your dashboard's Your API-Keys tab. You can create more than one key and revoke any of them independently -- useful if a key ends up somewhere it shouldn't, like a public repository. A key only ever sees the trees and data belonging to the account that created it.
Authentication
Every endpoint requires your API key as a bearer token in the Authorization header:
Authorization: Bearer <your-api-key>A missing, malformed, or invalid/inactive key gets a 401. Every non-2xx response returns a small JSON body, {"detail": "..."}, describing what went wrong -- except automatic request validation errors (malformed dates, an out-of-range rating, and similar), which come back as 422in FastAPI's standard validation-error shape instead.
Data & privacy
EcoGovernance does not store the raw text of your queries server-side. What gets logged per call is metadata for routing, footprint, and performance purposes -- model, provider, token counts, latency, footprint figures -- not the query content itself. If you need to look back at what you asked, keep your own record of it; see Rate a response below for where this matters most.
Rate limits
The Free tier currently allows 1,000 queries per month against POST /api/route, 1 API key, and 1 publishable decision tree.
The tree limit is a publish slot, not a build limit -- you can design as many trees as you like in the Decision Tree Builder, but only one can be reachable via /api/route at a time. Publishing a different tree simply swaps which one holds that slot; you can rotate freely.
Route a request
/api/routeExecutes one of your published decision trees. Send a query, EcoGovernance embeds it, routes it to the best-matching decision node, makes the underlying provider call for you, and hands the result back in a single unified response shape -- regardless of which provider (OpenAI, Anthropic, a local model, ...) the winning node actually called.
Body
| Field | Type | Required | Description |
|---|---|---|---|
| tree_id | int | Required | ID of a published tree (Published = true in the Decision Tree Builder). |
| payload | object | Optional (default {}) | Free-form -- field names must match whatever the tree's root node expects. There is no fixed schema; it depends on how you built the tree. |
| include_embedding_vector | boolean | Optional (default false) | When true, the raw embedding vector for the query is included in the response. Off by default since it's a few hundred numbers. |
| node_response_fields | array<string> | Optional (default []) | Dot-paths into the raw provider response you want extracted, e.g. "choices.0.message.content". Each path is resolved independently against whichever node actually wins this run; a path resolves to null if it doesn't exist there. Because a tree can route to nodes with different provider shapes (OpenAI vs. Anthropic, say), you can list paths for several possible shapes at once -- only the ones matching the winning provider's response come back non-null. |
{
"tree_id": 1254585,
"payload": {
"query": "...",
"context": "..."
},
"include_embedding_vector": false,
"node_response_fields": ["choices.0.message.content", "usage.completion_tokens"]
}Response
{
"model": "gpt-4.1-mini",
"provider": "OpenAI",
"footprint": {
"energy_kwh": 0.0021,
"water_l": 0.004,
"carbon_kg": 0.0009,
"cif_used": 412.5,
"cif_zone_used": "US-MIDA-PJM"
},
"path": [12, 47, 103],
"path_probability": 0.821543,
"response_id": 88231,
"embedding_id": 5821,
"tokens": { "input": 128, "output": 342 },
"performance": {
"ttft_ms": 412.0,
"tokens_per_second": 87.3,
"provider_latency_ms": 1683.2,
"total_latency_ms": 1779.6,
"ecogovernance_overhead_ms": 96.4
},
"embedding_vector": null,
"node_response": {
"choices.0.message.content": "...",
"usage.completion_tokens": 342
}
}| Field | Type | Required | Description |
|---|---|---|---|
| model | string | null | Always present | Model actually used by the winning decision node. |
| provider | string | null | Always present | Name of the private container that handled the call, e.g. "OpenAI". |
| footprint.energy_kwh / .water_l / .carbon_kg | number | null | Always present | Computed footprint for this one call. null if no footprint could be computed (e.g. unknown model size). |
| footprint.cif_used | number | null | Always present | Grid carbon intensity (g CO₂/kWh, monthly national average from Ember) at the time of the call -- lets you reproduce the carbon calculation independently. |
| footprint.cif_zone_used | string | null | Always present | CIF zone code the cif_used value applies to, e.g. "US-MIDA-PJM". |
| path | array<int> | Always present | IDs of the nodes traversed, in order. |
| path_probability | number | Always present | Confidence of the winning path only (product of edge weights / cosine similarities along that path) -- how unambiguous the routing decision was. Does not reveal probabilities of paths that didn't win, to keep your tree's internal structure private. |
| response_id | int | null | Always present | Unique ID for this one result. This is the value you pass back into POST /api/response-quality to rate it. |
| embedding_id | int | null | Always present | ID of the embedding computed for this request. Unlike response_id, this is shared across every result produced by the same request (relevant for force bundles, see below). |
| tokens.input / .output | int | null | Always present | Input / output token counts for the call. |
| performance.ttft_ms | number | null | Always present | Time-to-first-token, in milliseconds. null if not determinable (e.g. no streaming). |
| performance.tokens_per_second | number | null | Always present | Tokens/second during the generation phase only (excludes TTFT). |
| performance.provider_latency_ms | number | null | Always present | Pure runtime of the provider call. |
| performance.total_latency_ms | number | Always present | Total time from request received to response sent, including embedding, routing, and logging. |
| performance.ecogovernance_overhead_ms | number | Always present | total_latency_ms minus provider_latency_ms -- the overhead EcoGovernance itself adds. |
| embedding_vector | array<number> | null | Only when include_embedding_vector: true was requested | Raw embedding vector for the query. |
| node_response | object | null | Only when node_response_fields was non-empty | The requested paths as key/value pairs. A requested path that doesn't exist in the actual response comes back as null. |
| additional_results | array | absent | Only present for a force bundle | See "Force bundles" below. Absent entirely (not an empty array, not null) in the normal case. |
Free tier limit
On the Free tier, this endpoint is capped at 1,000 calls per month, and only 1 API key and 1 published tree exist per account -- see Rate limits above for the full picture.
Text-node results
A path can end at a text node instead of a real provider call -- the tree answers with fixed text instead. You can tell this happened because model and provider come back null (along with every other field that presupposes a real call). node_response is always populated with {"text": "..."} in this case, regardless of what you requested in node_response_fields.
{
"model": null,
"provider": null,
"footprint": {
"energy_kwh": null, "water_l": null, "carbon_kg": null,
"cif_used": null, "cif_zone_used": null
},
"path": [12, 47],
"path_probability": 0.6,
"response_id": 88231,
"embedding_id": 5821,
"tokens": { "input": null, "output": null },
"performance": {
"ttft_ms": null, "tokens_per_second": null, "provider_latency_ms": null,
"total_latency_ms": 42.1, "ecogovernance_overhead_ms": 42.1
},
"embedding_vector": null,
"node_response": { "text": "The node's fixed reply text." }
}Force bundles
A Force node can cause several decision nodes to fire simultaneously (see the tree spec for how Force nodes work). When that happens, the top-level fields in the response are the first result that succeeded -- not necessarily the first one fired, since a failed first attempt shouldn't sink the whole request when other bundle members succeeded. Every other member of the bundle appears in additional_results[], in the same shape as the top-level fields, plus a succeeded boolean. embedding_id and embedding_vector are not repeated per member (one embedding is shared by the whole request); response_id, path, and path_probability are set independently for every member. performance.total_latency_ms is identical across every result in the bundle, since it covers the entire request -- only ecogovernance_overhead_ms differs per member, because provider_latency_ms varies.
{
"model": "gpt-4.1-mini",
"provider": "OpenAI",
"...": "... (same fields as above)",
"additional_results": [
{
"model": "claude-3-5-haiku",
"provider": "Anthropic",
"footprint": { "energy_kwh": 0.0018, "water_l": 0.0035, "carbon_kg": 0.0008, "cif_used": 412.5, "cif_zone_used": "US-MIDA-PJM" },
"path": [12, 47, 108],
"path_probability": 0.821543,
"response_id": 88232,
"tokens": { "input": 128, "output": 301 },
"performance": { "ttft_ms": 380.0, "tokens_per_second": 91.2, "provider_latency_ms": 1502.0, "total_latency_ms": 1779.6, "ecogovernance_overhead_ms": 277.6 },
"node_response": { "content.0.text": "..." },
"succeeded": true
}
]
}Errors
| Status | When | Example detail |
|---|---|---|
| 401 | API key missing, malformed, or invalid/inactive. | "Invalid or inactive API key" |
| 403 | API key is valid but belongs to a different owner than the tree. | "This API key does not belong to the owner of this tree." |
| 404 | Tree does not exist, or is not published. | "Tree not found or not published." |
| 400 | Tree execution failed for a content reason (e.g. a rule violation in the graph). | — |
| 500 | Unexpected server error -- or, for a force bundle, every fired member failed and there was nothing to promote to the top level. | "Internal server error" |
List reports
/api/reportsRead-only access to the report figures already computed for your account: the four breakdown tables (tree / node / external-call / routing-savings) plus daily footprint rollups, optionally filtered to a date range. Nothing is computed on the fly -- periods you haven't generated a report for simply won't appear.
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
| since | date (YYYY-MM-DD) | Optional | Lower bound, inclusive, matched by overlap (PeriodEnd >= since). Omit for no lower bound. |
| until | date (YYYY-MM-DD) | Optional | Upper bound, inclusive, matched by overlap (PeriodStart <= until). Omit for no upper bound. |
Response
{
"periods": [
{
"period_start": "2026-06-01",
"period_end": "2026-06-30",
"archive": {
"storage_path": "reports/owner123/2026-06.pdf",
"created_at": "2026-07-02T08:14:00Z"
},
"tree_breakdown": [
{
"tree_id": 1254585, "version_id": 8831,
"paths": 14, "height": 4, "width": 6,
"vector_nodes": 5, "decision_nodes": 3,
"troubleshoot": 12, "queries": 4310,
"tok_in": 512300, "tok_out": 198420
}
],
"node_breakdown": [
{
"node_id": "decision-3", "tree_id": 1254585, "version_id": 8831,
"provider": "OpenAI", "hardware_chip": "NVIDIA H100",
"hardware_multiplier": 1.0, "host": "openai-us-east",
"location_code": "US-MIDA-PJM", "avg_latency_ms": 812.4,
"queries": 2100, "tok_in": 240000, "tok_out": 91000
}
],
"external_call_breakdown": [
{
"tree_id": 1254585, "node_id": "decision-3", "node_name": "GPT-4.1 mini Node",
"provider": "OpenAI", "model": "gpt-4.1-mini",
"total_latency_ms": 1706040.0, "queries": 2100,
"tok_in": 240000, "tok_out": 91000, "size_class": "Small",
"price_in_per_m": 0.4, "price_out_per_m": 1.6,
"aa_intelligence": 41.2, "aa_coding": 38.5, "aa_agentic": 29.7
}
],
"routing_savings": {
"baseline_provider": "OpenAI", "baseline_model": "gpt-4.1",
"energy_ratio": 0.62, "water_ratio": 0.58, "carbon_ratio": 0.61, "latency_ratio": 0.94
}
}
],
"daily_footprint": [
{ "date": "2026-06-01", "energy_kwh": 0.412, "water_l": 0.81, "carbon_kg": 0.174 }
]
}| Field | Type | Required | Description |
|---|---|---|---|
| periods | array | — | One entry per period for which at least one of the four breakdown tables has data for your account. |
| periods[].archive | object | null | — | storage_path / created_at if a PDF was archived for this exact period. null if figures were computed but no PDF was ever generated -- the two processes run independently. |
| periods[].tree_breakdown | array | — | One row per tree version you own in this period. Structural columns (paths / height / width / vector_nodes / decision_nodes) describe that version's graph snapshot; volume columns (troubleshoot / queries / tok_in / tok_out) are usage aggregates. |
| periods[].node_breakdown | array | — | One row per (node, tree version) you own in this period. Hardware/provider fields can be null if not resolvable. |
| periods[].external_call_breakdown | array | — | One row per (tree, node, provider, model) combination. total_latency_ms is a sum across all queries in that combination, not an average. Pricing / Artificial Analysis fields are null when the model isn't in either external catalog. |
| periods[].routing_savings | object | null | — | One row per (owner, period) -- not broken down by tree/node. Ratios below 1.0 mean savings vs. the baseline; above 1.0 means more usage than the baseline. null if not computed for this period. |
| daily_footprint | array | — | Daily totals, independent of the periods list -- can include days for which no period has been computed yet. |
Errors
| Status | When | Example detail |
|---|---|---|
| 401 | API key missing, malformed, or invalid/inactive. | "Invalid or inactive API key" |
| 400 | since is after until, or either is malformed. | "'since' must not be after 'until'." |
| 500 | Unexpected server error. | — |
No 404 -- an account with no report data yet returns 200 with empty arrays.
Download a report PDF
/api/reports/pdfReturns a temporary, signed download URL for the archived PDF of one report period. Complements List reports, which returns archive.storage_path for a period -- that path points into a private storage bucket and isn't downloadable directly; this endpoint turns it into a usable link.
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
| period_start | date (YYYY-MM-DD) | Required | Exact start of the report period. Together with the owner (from your API key), this identifies exactly one archived report. |
Response
{
"url": "<signed-download-url>"
}| Field | Type | Required | Description |
|---|---|---|---|
| url | string | — | Time-limited signed URL for the PDF, valid for 1 hour. Request again once it expires. |
Errors
| Status | When | Example detail |
|---|---|---|
| 401 | API key missing, malformed, or invalid/inactive. | "Invalid or inactive API key" |
| 404 | No archived report for that period (wrong period, someone else's period, or figures were computed but no PDF was ever archived). | "No report available for this period." |
| 500 | Unexpected server error. | — |
No placeholder-PDF fallback -- a missing report always returns 404, never a generic demo file.
Node performance
/api/node-performanceLive counterpart to List reports, scoped to one tree: continuously up-to-date per-node performance numbers, plus quality ratings and market pricing/benchmark data for the models your tree's nodes are actually using. Built for your own monitoring or alerting (e.g. cost alerts over time, or checking whether a cheaper host now exists for a model you already use) -- it only covers models this tree's nodes use, not the whole market.
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
| tree_id | int | Required | ID of the tree whose node performance you want. |
Response
{
"nodes": [
{
"nodeId": "decision-3",
"runs": 2100, "successes": 2078, "successRate": 0.9895,
"avgTokens": 1580.2, "totalTokens": 3318420,
"avgOutputTokens": 433.3, "totalOutputTokens": 909930,
"avgEnergyKwh": 0.0021, "totalEnergyKwh": 4.41,
"avgWaterL": 0.004, "totalWaterL": 8.4,
"avgCarbonKg": 0.0009, "totalCarbonKg": 1.89,
"avgLatencyMs": 1683.2, "avgTtftMs": 412.0, "avgTokensPerSecond": 87.3,
"totalCostUsd": 214.5,
"hardwareClasses": { "Small": 2100 },
"lastRunAt": "2026-08-15T09:12:00Z",
"provider": "OpenAI", "model": "gpt-4.1-mini",
"queryPathFailures": 4,
"avgRating": 4.2, "ratingCount": 37,
"benchmark": { "intelligenceIndex": 41.2, "codingIndex": 38.5, "agenticIndex": 29.7 },
"price": {
"pricePerMInput": 0.4, "pricePerMOutput": 1.6,
"providers": [ { "name": "Groq", "pricePerMInput": 0.35, "pricePerMOutput": 1.4 } ]
}
}
],
"tree_summary": {
"available": true, "totalRuns": 4310,
"baseline": { "nodeId": "decision-7", "model": "llama-3.1-70b", "provider": "Together", "sizeB": 70.0 },
"energySavingsPct": 0.42, "waterSavingsPct": 0.38, "carbonSavingsPct": 0.41,
"costSavingsPct": 0.29, "latencySavingsPct": 0.12,
"energyPerOutputToken": 0.0000048, "waterPerOutputToken": 0.0000091, "carbonPerOutputToken": 0.0000021
}
}| Field | Type | Required | Description |
|---|---|---|---|
| nodes | array | — | One entry per decision node with at least one run or piece of feedback. A node with no entry has had no runs yet. |
| nodes[].totalEnergyKwh / totalWaterL / totalCarbonKg | number | null | — | Sum across every run of this node, not an average. |
| nodes[].totalCostUsd | number | null | — | Sum over runs with a known cost only (currently OpenRouter containers) -- may be a partial sum if you mix container types. |
| nodes[].hardwareClasses | object | — | Count of observed hardware classes (Nano/Micro/Small/Medium/Large) across this node's runs. |
| nodes[].avgRating | number | null | — | Average of all ratings submitted via Rate a response for runs of this node. null until at least one rating exists -- this is the field that answers "which node gives the better answer for my use case". |
| nodes[].ratingCount | int | — | How many of this node's runs (out of runs total) have actually been rated -- tells you how much to trust avgRating. |
| nodes[].benchmark | object | null | — | Artificial Analysis quality scores for nodes[].model, looked up live. null if the model isn't found there; individual sub-fields can also be missing. |
| nodes[].price | object | null | — | OpenRouter pricing for nodes[].model, looked up live. pricePerM* is the aggregate price; providers[] lists price per hosting provider, cheapest first. null if the model isn't found there. |
| tree_summary | object | — | Either {"available": false, "reason": "..."} (no tree / no decision nodes / no real runs yet) or the full object shown above. |
| tree_summary.baseline | object | — | The node with the highest footprint per output token across energy/carbon/water -- the comparison baseline for every *SavingsPct field. |
| tree_summary.*SavingsPct | number | null | — | Savings vs. the baseline, compared per output token (not per query, since different paths can produce very different answer lengths). Individual fields can be null even when available is true (e.g. missing cost data). |
Unrecognized tree_id
Unlike Route a request, this endpoint does not distinguish a foreign or nonexistent tree_id with 403/404 -- it simply returns 200 with an empty nodes[] array and tree_summary.available: false. No data about another owner is ever exposed; it's just less granular than the error handling on the routing endpoint.
Errors
| Status | When | Example detail |
|---|---|---|
| 401 | API key missing, malformed, or invalid/inactive. | "Invalid or inactive API key" |
| 500 | Unexpected server error. | — |
Read tree structure
/api/treeReturns the full graph of one of your trees -- structure, node names, and the current on/off state of every rule (block/force) node. Meant to be read before calling Toggle a rule node below, so you know which node_ids exist and what state they're currently in. Unlike Route a request, this endpoint deliberately exposes the full tree structure (including jsonBody / containerId / endpoint per node) -- it's for managing your own tree, not for describing a single execution.
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
| tree_id | int | Required | ID of the tree. |
Response
{
"treeId": 1254585,
"name": "Customer Support Router",
"published": true,
"createdAt": "2026-06-01T10:00:00Z",
"updatedAt": "2026-08-10T14:22:00Z",
"graph": {
"nodes": [
{ "id": "embedding_a3f9c1d2", "type": "embedding", "data": { "nodeName": null } },
{ "id": "vektor_7b1e", "type": "vektor", "data": { "nodeName": "Nutrition" } },
{ "id": "force-1785227618917", "type": "force", "data": { "nodeName": null, "active": true } },
{
"id": "decision_5f2a",
"type": "decision",
"data": {
"nodeName": "Groq-prod",
"endpoint": "Groq-prod",
"containerId": 42,
"jsonBody": "{\"model\": \"llama-3.3-70b-versatile\", ...}"
}
},
{ "id": "block-1785232255837", "type": "block", "data": { "nodeName": null, "active": false } }
],
"edges": [
{ "id": "e1", "source": "embedding_a3f9c1d2", "target": "vektor_7b1e" },
{ "id": "e2", "source": "vektor_7b1e", "target": "force-1785227618917" },
{ "id": "e3", "source": "force-1785227618917", "target": "decision_5f2a" }
]
}
}| Field | Type | Required | Description |
|---|---|---|---|
| treeId / name / published / createdAt / updatedAt | — | — | Basic tree metadata. |
| graph.nodes[] | array | — | Every node with id, type (embedding / vektor / decision / block / force / text), and data -- only the semantically relevant data fields (nodeName; active for rule nodes; endpoint / containerId / jsonBody for decision nodes). Pure canvas layout fields (position, size) are filtered out. |
| graph.edges[] | array | — | id / source / target only -- styling fields filtered out. |
The active flag
active is absent on nodes that aren't rule nodes (not applicable to them). For a block/force node with no explicit flag set, the tree spec's default is true -- this endpoint always includes it explicitly rather than leaving you to guess what a missing value means.
Errors
| Status | When | Example detail |
|---|---|---|
| 401 | API key missing, malformed, or invalid/inactive. | "Invalid or inactive API key" |
| 404 | Tree does not exist or belongs to another owner. | "Tree not found." |
| 500 | Unexpected server error. | — |
Toggle a rule node
/api/tree/rule-nodeTurns a single block or force node on or off. One boolean covers both directions: active: true blocks/forces, active: false lets traffic pass through again. Intended as an emergency or automation lever -- pair it with Node performance to, say, have your own script auto-block a node when its cost or error rate crosses a threshold.
Body
| Field | Type | Required | Description |
|---|---|---|---|
| tree_id | int | Required | ID of the tree containing the node. |
| node_id | string | Required | ID of the block- or force-type rule node. |
| active | boolean | Required | New state -- true to block/force, false to release it. |
{
"tree_id": 1254585,
"node_id": "block-1785232255837",
"active": true
}Response
{
"treeId": 1254585,
"nodeId": "block-1785232255837",
"type": "block",
"active": true
}| Field | Type | Required | Description |
|---|---|---|---|
| treeId / nodeId / type / active | — | — | Echoes the newly set state. Doesn't return the full graph -- call Read tree structure again if you need that. |
How the change takes effect
This patches the tree's graph in place -- it's the same write path as saving in the Decision Tree Builder. A new tree version isn't created immediately; one appears automatically the next time Route a request actually runs against the tree. There is currently no built-in guard against blocking every viable path at once -- if you toggle yourself into a dead end, Route a request will simply start failing until you toggle something back on.
Errors
| Status | When | Example detail |
|---|---|---|
| 401 | API key missing, malformed, or invalid/inactive. | "Invalid or inactive API key" |
| 404 | Tree doesn't exist, belongs to another owner, or node_id doesn't exist in this tree. | "Tree or node not found." |
| 400 | node_id exists but isn't a rule node (block/force) -- active has no meaning for other node types. | "'active' only applies to block/force nodes." |
| 500 | Unexpected server error. | — |
Account monitoring
/api/monitoringAccount-wide, live energy/water/carbon summary across every tree you own -- fills the gap between List reports (periodic, but account-wide) and Node performance (live, but one tree at a time). Backed by pre-aggregated rollup tables rather than a live scan of every run, so it's cheap to poll from a status widget.
Query parameters
| Field | Type | Required | Description |
|---|---|---|---|
| range | string | Optional (default "today") | One of today / month / year. A calendar boundary ("since the start of today/this month/this year"), not a rolling window. |
Response
{
"range": "today",
"energy": {
"total": 4.21,
"series": [
{ "bucket": "2026-08-15T08:00:00+00:00", "value": 0.62 },
{ "bucket": "2026-08-15T09:00:00+00:00", "value": 0.88 }
]
},
"water": { "total": 8.4, "series": [ { "bucket": "2026-08-15T08:00:00+00:00", "value": 1.2 } ] },
"carbon": { "total": 1.9, "series": [ { "bucket": "2026-08-15T08:00:00+00:00", "value": 0.28 } ] }
}| Field | Type | Required | Description |
|---|---|---|---|
| range | string | — | The range value actually used, echoing the request. |
| energy / water / carbon | object | — | For each metric: total across the whole range, and series, a time series. |
| *.series[].bucket | string (ISO) | — | Hourly bucket for range=today, daily bucket for month/year. |
| *.series[].value | number | — | Sum of that metric within this one bucket. |
Errors
| Status | When | Example detail |
|---|---|---|
| 401 | API key missing, malformed, or invalid/inactive. | "Invalid or inactive API key" |
| 400 | range is not today, month, or year. | "'range' must be today, month, or year." |
| 500 | Unexpected server error. | — |
No 404 -- an account with no runs yet returns 200 with empty series and total: 0.
Rate a response
/api/response-qualityAttaches a 1-5 star rating to one specific result you got back from Route a request, referenced by its response_id. This updates that run's own log entry rather than creating a new record -- no query text is stored anywhere by this endpoint. The payoff is reading it back: ratings roll up into avgRating / ratingCount per node via Node performance, which is how you find out which node or model actually performs best for your use case.
Body
| Field | Type | Required | Description |
|---|---|---|---|
| response_id | int | Required | The response_id from the original /api/route result you're rating -- unique per result, including individual additional_results[] members of a force bundle. |
| rating | int (1-5) | Required | Star rating. |
{
"response_id": 88231,
"rating": 4
}Response
{
"response_id": 88231,
"rating": 4,
"ratedAt": "2026-08-15T16:04:00Z"
}| Field | Type | Required | Description |
|---|---|---|---|
| ratedAt | string (ISO) | — | When this rating call happened -- distinct from the run's original timestamp, since rating can happen any time after the fact. |
Re-rating
Calling this again with the same response_id simply overwrites the previous rating -- no error, no restriction on changing your mind later.
What's deliberately not accepted
tree_id, query_text, path, model, and node_id are not part of the request body -- everything needed is already on the NodeRunLog row identified by response_id. If you need to look up your own original query text, you'll need to keep your own record of it; EcoGovernance does not store raw query text server-side.
Errors
| Status | When | Example detail |
|---|---|---|
| 401 | API key missing, malformed, or invalid/inactive. | "Invalid or inactive API key" |
| 404 | response_id doesn't exist, or belongs to a run from another owner. | "No matching request found." |
| 422 | rating is outside 1-5 (standard FastAPI validation error). | — |
| 500 | Unexpected server error. | — |