Skip to main content
The examples on this page use Northpine, a fictional outdoor-gear shop whose refund-exception decision grants, escalates or denies refund requests outside its standard return policy. How Moonbounce works introduces it.
From answers to an outcome showed how answers become a score and an outcome. This page is the call that does it: one evaluation is one run of one decision on one item, through POST /v1/evaluate. You build the request up a piece at a time, send Northpine’s ticket C, and read the response field by field.

The smallest request

Four fields are required: the decision, its version, the item’s id and the content.
decision_id is a name you choose: lowercase letters, digits, ., _ and -, starting with a letter. This request names the version by its hash, which works once the spec is registered under the decision; the next section shows how the first call does that. Every field the rest of this page adds is optional.

Which version runs

The version is the spec’s features plus the numbers that combine their answers. You name it with two fields. The structure. Send spec, the spec itself, or structure, its hash. Except on a dry run, an inline spec is registered the first time the API sees it (creating the decision if the id is new), so the first call can carry the file. A structure hash must already be registered for this decision, or the call is 404 VERSION_UNKNOWN. You may send both: the API hashes the spec, and if the result differs from your structure, the call is 409 VERSION_MISMATCH and nothing runs. That is a cheap way to assert that the file you deploy is the version you previewed. The weights. weights is "default" (the starting numbers every spec gets) or a weights hash you pinned. Leave it out and it means "default". Versions and hashes covers both hashes, registering, and pins.

The item

item_id is required: your own id for the thing being judged, up to 256 characters, such as a ticket or order number. It does two jobs. It groups every evaluation of the same ticket, so a re-sent or edited ticket shows up as one item with several evaluations. And feedback can name an item instead of an evaluation ({"decision_id", "item_id"}); it then binds to that item’s latest evaluation in your key’s environment. Use an id that stays the same for the same ticket and differs between tickets.

Content: subject and context

Content is a list of parts. subject (1 to 64 parts) is the thing being judged. context (up to 64 parts) is background the engine always reads alongside it. Put per-request background in context, not in the spec. The spec’s guidance is the same for every call and is part of the structure hash; if you pasted each customer’s order into it, every call would mint a new version. Keep the spec static and send what changes per call as content.

Text and json parts

A part is text or json:
text holds a non-empty string. json holds an object or an array in data. Image, audio, video, document and conversation-turn parts are recognized but not accepted yet: a request with one is 400 CAPABILITY_UNSUPPORTED, with param pointing at the part (subject.1.type), before anything is registered or run.

Naming parts and inputs

name is optional: a lowercase slug such as customer_message. It lets a feature read only some of the subject. A feature with "inputs": ["customer_message"] reads that subject part and skips the others; a feature without inputs reads every subject part. Context parts are read by every feature either way. The rule is strict. Every name in any feature’s inputs must match the name of a subject part in this call, or the call is 400 CONTENT_INVALID with param at the unknown name (nodes.defective.inputs.0). A context part with that name does not count. So a typo in inputs is refused rather than silently reading nothing. If some calls omit a part a feature names, drop that feature’s inputs, or send the part on every call. Avoid naming a subject part context when the request also sends context. "sensitive": true on a part is display policy only: reviewers see it blurred or behind a link. The engine still reads it, and it is stored like any other part.

Outcomes, metadata and summary

  • outcomes: the cut points that turn the score into an outcome, {"below", "cuts", "abstain"?}, as on From answers to an outcome. They travel with every call and are never hashed, so you can change them in code without minting a version. Without them you get a score and no outcome.
  • metadata: your own key-values, stored with the evaluation and never shown to the model. Keys are up to 64 characters; values are strings (up to 512), numbers, booleans or null. Keys starting mb. are reserved for where a call came from: mb.commit, mb.branch, mb.service and mb.release. You set them yourself.
  • summary: one line (up to 140 characters) shown on review cards, so a reviewer knows what an item is before opening it. Never shown to the model.

Ticket C, in full

Ticket C is the late tent: it arrived two weeks late because of the carrier, was used once, cost $189, and the customer wrote on day 39. Northpine’s app sends the customer’s message as the subject and the order as a json context part. Look at the order fields against the features. outside_window asks whether the request is “more than 30 days after delivery”. A delivery date alone would leave the engine to guess today’s date, so the app sends days_since_delivery, which answers the prompt directly. order_total_usd answers high_value (“above $250”) and refunds_last_12_months answers repeat_refunder. The tracking line is there too, but no feature captures carrier delays, so the engine reads it and nothing in the score can use it. That gap is what the loop finds later.
The app reads the decision file, so whatever is merged is what runs. Describe a decision builds the spec it sends.

The response

The call answers 200 with the evaluation. Ticket C’s, with the engine answers from the running example:
  • id: the evaluation’s id, evl_ followed by a time-ordered UUID. Keep it: feedback and reads name it. A dry run returns a dry_ id that names nothing stored.
  • decision_id: the decision you called.
  • version: exactly what ran. structure and weights are the two hashes; weights_source says whether the weights were "default" or "pinned"; weights_fitted is true only when the weights were fit on feedback. binding is the revision of the decision’s engine settings in this environment, and engine_profile lists which engine, model and adapter answered each feature that grant_score or grant reads. Versions and hashes and Engines, keys and environments cover these.
  • status: scored (a score, cut into an outcome), no_outcomes (a score, but you sent no outcomes) or abstained (the output had no usable answer, so there is no score; outcome is then outcomes.abstain if you set it). Branch on status to detect an abstention, since abstain may reuse an outcome name like escalate.
  • score and outcome: the output node’s value, and where it falls among your cut points. Ticket C’s 0.54 is between 0.38 and 0.60, so escalate.
  • nodes: every node’s value by name, in spec order. A feature’s value is the engine’s probability that the answer is yes. A combine’s contributions are weight × answer per term (the bias is not among them, so value = 0.8 − 0.19 − 0.07 + 0.01 − 0.004 − 0.006). calibrated: false means the calibrate node passed the score through. A node with no answer keeps its type and has abstained: true or failed: true with a reason instead of a value.
  • engines: one entry per feature. model is what the engine settings asked for, resolved_model the exact versioned model that answered, adapter the adapter and its version. cached is whether the answer was reused from an earlier identical call instead of a fresh one; on evaluate it is false today.
  • outcomes: the cut points you sent, echoed so your logs and Moonbounce’s records agree.
  • usage: the tokens the engine calls used.
  • cost: recorded spend, including on a dry run: engine_calls, cache_hits, input_tokens and usd_micros (millionths of a US dollar). Calls, input tokens and money come from the same usage row as getUsage; usd_micros here is reported cost only, while getUsage adds the estimate for calls that reported no usage and breaks it out. engine_calls counts upstream engine calls on evaluate and seeds; one call can answer several features (Jev compiles them). On previews it counts features sent to engines instead. Only previews use the feature cache today; evaluate and seeds report cache_hits: 0. The read-back endpoint returns the same cost when a usage row exists; otherwise it omits cost. The example cost above is illustrative.
  • item_id, summary, metadata: echoed from the request.
  • warnings: present only when there is one. FEATURE_FAILED means a feature the score doesn’t read failed (the call still succeeds); WEIGHTS_PARTIAL and WEIGHTS_STALE_TERM mean a pinned weights hash and the spec disagree (Versions and hashes).
Numbers come back at full precision, so ticket C’s item_used contribution arrives as -0.06999999999999999. This page rounds; your code should compare the raw numbers, which is what the outcome was cut from.
Fresh responses always include cost. An idempotent replay of a response stored before costs were reported may omit it until 24 hours after deployment, when those snapshots expire. A replay returns the original cost when present; it is not new spend. Suggestion generation’s own cost isn’t in per-request cost; the usage report shows it under other_spend.

Retries and Idempotency-Key

Send an Idempotency-Key header (1 to 255 printable ASCII characters) on every POST, and keep it with the work item until you have an answer. If the call times out or fails with a 503, send the same request with the same key:
  • If the first attempt was stored, you get its response back, byte for byte, with Idempotent-Replayed: true, and nothing runs twice. Two requests in flight with the same key also produce one evaluation.
  • If it failed before it was stored, the retry runs it.
  • The same key with a different request is 422 IDEMPOTENCY_KEY_REUSED. “Different” means the parsed body: spelling out "weights": "default" is the same request as leaving it out, but a changed metadata value is not.
  • If the first result has since been deleted, the key answers 410 and can’t be replayed. Use a fresh key.
A key is remembered for 24 hours and belongs to your organization and the key’s environment, shared across every operation, so use a fresh one per distinct request. A test key and a production key never share keys. On evaluate, a retry through a rotated API key in the same environment still replays. Feedback and review retries don’t, because who sent them is part of the request (Feedback).

Time

An evaluation answers within 10 seconds, counted from when the request arrives, or fails with an error you can act on. Prefer: wait=<seconds> (whole seconds, 1 or more) shortens that budget; it never lengthens it. For a normal call, the response follows the evaluation commit, so feedback can already bind to it. A dry run answers after its usage is stored and has no feedback target.

When it fails

  • 502 ENGINE_FAILED: the engine couldn’t answer a feature the score depends on, so there is no score. details.features names what failed. A retry may not help: route the item to a person. A failed call isn’t stored, so a retry with the same key runs it again.
  • 503 (ENGINE_UNAVAILABLE, STORE_UNAVAILABLE, AUTH_UNAVAILABLE): temporary. Retry with the same key after Retry-After seconds. STORE_UNAVAILABLE means the API couldn’t confirm the evaluation was saved, and the keyed retry settles it.
  • 429 RATE_LIMITED: wait Retry-After seconds and retry with the same key.
  • 429 QUOTA_EXCEEDED: your team’s daily work budget is used up until details.reset_at. Route the item to a person, or wait for the reset (Rate limits).
  • Any other 4xx: the request needs fixing; retrying it unchanged gives the same answer.
The full list of statuses, reasons and limits is in the quickstart.

Reading an evaluation back

GET /v1/evaluations/{evaluation_id} returns the evaluation result plus assignment_mode (primary, seed, shadow or backtest) and the submitted subject when stored. subject is absent when content was not stored (store_content: false). It is scoped to the key’s environment: an evaluation made with a production key is 404 to a test key, so test code never reads production traffic. Both calls need the decisions:evaluate scope.

Finding decisions and past runs

All three lists return environment, items, next_cursor and has_more. limit defaults to 50 and accepts 1–100. Pass next_cursor as cursor while has_more is true.
  • GET /v1/decisions lists decisions with a binding in your key’s environment, newest decision first. Each item has decision_id and the shared registry’s structures count. Either decisions:evaluate or decisions:develop is sufficient.
  • GET /v1/decisions/{decision_id}/structures lists registered hashes newest first, with created_at, nodes, default_weights and meta when present. The decision must exist in your environment through a binding; its registry is shared across your organization’s environments. Spec bodies come from getStructure (StructureRead); registerStructure returns StructureRecord without spec, including on an idempotent replay.
  • GET /v1/decisions/{decision_id}/evaluations lists undeleted evaluations newest first, without content. Filter by item_id, assignment_mode, since (inclusive) or until (exclusive). Each item has id, item_id, assignment_mode, outcome, score, created_at and version with structure and weights. The evaluation list requires decisions:evaluate; structure reads and lists accept decisions:evaluate or decisions:develop.

Trying a decision without storing an evaluation

Add "dry_run": true to either evaluate request form. It still spends engine calls and counts against normal team admission limits. The response has the usual result fields with dry_run: true and a dry_ id that names nothing stored. Normal responses have an evl_ id and omit dry_run. Reading or submitting feedback about a dry_ id is refused; MCP may reject its pattern before calling the API. A dry run stores only the usage row. It creates no evaluation, feedback target, structure, binding, content key or feature-cache entry. An inline spec works without registration; when the decision has no binding in this environment, the deployment default is used in memory. A request by hash still requires a registered structure. Omit Idempotency-Key: a dry run with that header is a 400, because no replay is stored. Retrying a dry run can spend again. Usage totals include it under unknown engine profile and assignment mode; the per-engine breakdown requires a stored evaluation trace and therefore excludes dry runs.

One decision per call

Each call runs one decision. If an item goes through two decisions, make two calls and chain them in your code, for example only asking a second decision when the first says escalate. A feature that both specs share is answered again in the second call.

What is stored, and why

A normal evaluation stores, when content storage is enabled, the content you sent (subject and context), every node’s value, which engine answered each feature, the version and engine settings that ran, and the cut points you sent, along with item_id, summary and metadata. That is what lets the rest of the loop work without calling your app again: feedback binds to an exact evaluation, reviewers see the content that was judged, and a preview replays a changed spec or new cut points over past cases.

Next

Versions and hashes explains the two hashes in version: what changes each one, how to register and pin them, and how the decision file keeps them in step with your code.