v1.0 · Beta · HTTP API

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.

1

Route a request

POST/api/route

Executes 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

FieldTypeRequiredDescription
tree_idintRequiredID of a published tree (Published = true in the Decision Tree Builder).
payloadobjectOptional (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_vectorbooleanOptional (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_fieldsarray<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.
Request body
{
  "tree_id": 1254585,
  "payload": {
    "query": "...",
    "context": "..."
  },
  "include_embedding_vector": false,
  "node_response_fields": ["choices.0.message.content", "usage.completion_tokens"]
}

Response

200 OK
{
  "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
  }
}
FieldTypeRequiredDescription
modelstring | nullAlways presentModel actually used by the winning decision node.
providerstring | nullAlways presentName of the private container that handled the call, e.g. "OpenAI".
footprint.energy_kwh / .water_l / .carbon_kgnumber | nullAlways presentComputed footprint for this one call. null if no footprint could be computed (e.g. unknown model size).
footprint.cif_usednumber | nullAlways presentGrid 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_usedstring | nullAlways presentCIF zone code the cif_used value applies to, e.g. "US-MIDA-PJM".
patharray<int>Always presentIDs of the nodes traversed, in order.
path_probabilitynumberAlways presentConfidence 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_idint | nullAlways presentUnique ID for this one result. This is the value you pass back into POST /api/response-quality to rate it.
embedding_idint | nullAlways presentID 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 / .outputint | nullAlways presentInput / output token counts for the call.
performance.ttft_msnumber | nullAlways presentTime-to-first-token, in milliseconds. null if not determinable (e.g. no streaming).
performance.tokens_per_secondnumber | nullAlways presentTokens/second during the generation phase only (excludes TTFT).
performance.provider_latency_msnumber | nullAlways presentPure runtime of the provider call.
performance.total_latency_msnumberAlways presentTotal time from request received to response sent, including embedding, routing, and logging.
performance.ecogovernance_overhead_msnumberAlways presenttotal_latency_ms minus provider_latency_ms -- the overhead EcoGovernance itself adds.
embedding_vectorarray<number> | nullOnly when include_embedding_vector: true was requestedRaw embedding vector for the query.
node_responseobject | nullOnly when node_response_fields was non-emptyThe requested paths as key/value pairs. A requested path that doesn't exist in the actual response comes back as null.
additional_resultsarray | absentOnly present for a force bundleSee "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.

200 OK -- text-node result
{
  "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.

200 OK -- force bundle (partial)
{
  "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

StatusWhenExample detail
401API key missing, malformed, or invalid/inactive."Invalid or inactive API key"
403API key is valid but belongs to a different owner than the tree."This API key does not belong to the owner of this tree."
404Tree does not exist, or is not published."Tree not found or not published."
400Tree execution failed for a content reason (e.g. a rule violation in the graph).
500Unexpected server error -- or, for a force bundle, every fired member failed and there was nothing to promote to the top level."Internal server error"
2

List reports

GET/api/reports

Read-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

FieldTypeRequiredDescription
sincedate (YYYY-MM-DD)OptionalLower bound, inclusive, matched by overlap (PeriodEnd >= since). Omit for no lower bound.
untildate (YYYY-MM-DD)OptionalUpper bound, inclusive, matched by overlap (PeriodStart <= until). Omit for no upper bound.

Response

200 OK
{
  "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 }
  ]
}
FieldTypeRequiredDescription
periodsarrayOne entry per period for which at least one of the four breakdown tables has data for your account.
periods[].archiveobject | nullstorage_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_breakdownarrayOne 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_breakdownarrayOne row per (node, tree version) you own in this period. Hardware/provider fields can be null if not resolvable.
periods[].external_call_breakdownarrayOne 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_savingsobject | nullOne 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_footprintarrayDaily totals, independent of the periods list -- can include days for which no period has been computed yet.

Errors

StatusWhenExample detail
401API key missing, malformed, or invalid/inactive."Invalid or inactive API key"
400since is after until, or either is malformed."'since' must not be after 'until'."
500Unexpected server error.

No 404 -- an account with no report data yet returns 200 with empty arrays.

3

Download a report PDF

GET/api/reports/pdf

Returns 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

FieldTypeRequiredDescription
period_startdate (YYYY-MM-DD)RequiredExact start of the report period. Together with the owner (from your API key), this identifies exactly one archived report.

Response

200 OK
{
  "url": "<signed-download-url>"
}
FieldTypeRequiredDescription
urlstringTime-limited signed URL for the PDF, valid for 1 hour. Request again once it expires.

Errors

StatusWhenExample detail
401API key missing, malformed, or invalid/inactive."Invalid or inactive API key"
404No 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."
500Unexpected server error.

No placeholder-PDF fallback -- a missing report always returns 404, never a generic demo file.

4

Node performance

GET/api/node-performance

Live 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

FieldTypeRequiredDescription
tree_idintRequiredID of the tree whose node performance you want.

Response

200 OK
{
  "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
  }
}
FieldTypeRequiredDescription
nodesarrayOne 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 / totalCarbonKgnumber | nullSum across every run of this node, not an average.
nodes[].totalCostUsdnumber | nullSum over runs with a known cost only (currently OpenRouter containers) -- may be a partial sum if you mix container types.
nodes[].hardwareClassesobjectCount of observed hardware classes (Nano/Micro/Small/Medium/Large) across this node's runs.
nodes[].avgRatingnumber | nullAverage 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[].ratingCountintHow many of this node's runs (out of runs total) have actually been rated -- tells you how much to trust avgRating.
nodes[].benchmarkobject | nullArtificial 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[].priceobject | nullOpenRouter 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_summaryobjectEither {"available": false, "reason": "..."} (no tree / no decision nodes / no real runs yet) or the full object shown above.
tree_summary.baselineobjectThe node with the highest footprint per output token across energy/carbon/water -- the comparison baseline for every *SavingsPct field.
tree_summary.*SavingsPctnumber | nullSavings 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

StatusWhenExample detail
401API key missing, malformed, or invalid/inactive."Invalid or inactive API key"
500Unexpected server error.
5a

Read tree structure

GET/api/tree

Returns 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

FieldTypeRequiredDescription
tree_idintRequiredID of the tree.

Response

200 OK
{
  "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" }
    ]
  }
}
FieldTypeRequiredDescription
treeId / name / published / createdAt / updatedAtBasic tree metadata.
graph.nodes[]arrayEvery 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[]arrayid / 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

StatusWhenExample detail
401API key missing, malformed, or invalid/inactive."Invalid or inactive API key"
404Tree does not exist or belongs to another owner."Tree not found."
500Unexpected server error.
5b

Toggle a rule node

POST/api/tree/rule-node

Turns 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

FieldTypeRequiredDescription
tree_idintRequiredID of the tree containing the node.
node_idstringRequiredID of the block- or force-type rule node.
activebooleanRequiredNew state -- true to block/force, false to release it.
Request body
{
  "tree_id": 1254585,
  "node_id": "block-1785232255837",
  "active": true
}

Response

200 OK
{
  "treeId": 1254585,
  "nodeId": "block-1785232255837",
  "type": "block",
  "active": true
}
FieldTypeRequiredDescription
treeId / nodeId / type / activeEchoes 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

StatusWhenExample detail
401API key missing, malformed, or invalid/inactive."Invalid or inactive API key"
404Tree doesn't exist, belongs to another owner, or node_id doesn't exist in this tree."Tree or node not found."
400node_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."
500Unexpected server error.
6

Account monitoring

GET/api/monitoring

Account-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

FieldTypeRequiredDescription
rangestringOptional (default "today")One of today / month / year. A calendar boundary ("since the start of today/this month/this year"), not a rolling window.

Response

200 OK
{
  "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 } ] }
}
FieldTypeRequiredDescription
rangestringThe range value actually used, echoing the request.
energy / water / carbonobjectFor each metric: total across the whole range, and series, a time series.
*.series[].bucketstring (ISO)Hourly bucket for range=today, daily bucket for month/year.
*.series[].valuenumberSum of that metric within this one bucket.

Errors

StatusWhenExample detail
401API key missing, malformed, or invalid/inactive."Invalid or inactive API key"
400range is not today, month, or year."'range' must be today, month, or year."
500Unexpected server error.

No 404 -- an account with no runs yet returns 200 with empty series and total: 0.

7

Rate a response

POST/api/response-quality

Attaches 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

FieldTypeRequiredDescription
response_idintRequiredThe response_id from the original /api/route result you're rating -- unique per result, including individual additional_results[] members of a force bundle.
ratingint (1-5)RequiredStar rating.
Request body
{
  "response_id": 88231,
  "rating": 4
}

Response

200 OK
{
  "response_id": 88231,
  "rating": 4,
  "ratedAt": "2026-08-15T16:04:00Z"
}
FieldTypeRequiredDescription
ratedAtstring (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

StatusWhenExample detail
401API key missing, malformed, or invalid/inactive."Invalid or inactive API key"
404response_id doesn't exist, or belongs to a run from another owner."No matching request found."
422rating is outside 1-5 (standard FastAPI validation error).
500Unexpected server error.