> ## Documentation Index
> Fetch the complete documentation index at: https://docs.usenexio.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Outcomes and annotations

> Report what happened after a run and attach human judgment to it, safely and without double counting.

A run's answer is only half the story. The other half is what your team did with it: whether someone looked at it, took the recommendation, chose something else and why, and what finally happened. Nexio records that as two kinds of signal against a run.

* An **outcome** is an event in the run's life after delivery: viewed, accepted, overridden, or a final result. Outcomes are append-only and idempotent.
* An **annotation** is a person's judgment of the run: a rating, a required comment, and an optional pointer at the part of the output it is about.

Both feed the engine's quality measures and its improvement proposals. See [How the signal is used](#how-the-signal-is-used).

A run's output is typed by the engine's declared response schema (see [Declared schemas](/engines/overview#declared-schemas)). Outcome events are not: the event types and their payloads are fixed by the platform and are the same for every engine.

## Outcomes

`POST /api/v1/engines/{engine_slug}/runs/{run_id}/outcomes`

| Field        | Required | Meaning                                                                                                                                                                                    |
| ------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `event_id`   | Yes      | Your ID for this event: a UUID version 4 in its standard 36-character hyphenated form, generated by you. It is the idempotency key. Send it in lowercase and repeat it exactly on a retry. |
| `event_type` | Yes      | One of the four event types below.                                                                                                                                                         |
| `payload`    | Yes      | The fields for that event type. Unknown fields are refused.                                                                                                                                |

### Event types

| `event_type`        | Meaning                                                                                | Required timestamp |
| ------------------- | -------------------------------------------------------------------------------------- | ------------------ |
| `viewed`            | A person looked at the result.                                                         | `viewed_at`        |
| `accepted`          | The result's recommendation was taken. The payload names the accepted alternative.     | `accepted_at`      |
| `overridden`        | Something else was chosen. The payload names the chosen alternative and a reason code. | `overridden_at`    |
| `placement_outcome` | The final business result, with a `status`.                                            | `outcome_at`       |

Timestamps are RFC 3339 with an offset of at most 23 hours. Nexio stores identifiers as sent and does not check them against the run. The identifier fields (`accepted_carrier_id`, `chosen_carrier_id`, `carrier_id`, `broker_id`), the `placement_outcome` type with its `status` values, and the `reason_code` list are a fixed set; the full field list is in the [endpoint reference](/api-reference/engines/submit-outcome).

`reason_code` on `overridden` must come from the override reason taxonomy, version `2026-08-31-unified`. Send that version in `reason_taxonomy_version`, or leave it out (or send an empty string) and Nexio stamps it. Any other version is refused. `reason_text` is at most 1,000 characters, counted as Unicode characters, not bytes. It is required, and cannot be only whitespace, when `reason_code` is `other`.

### Record an outcome

<CodeGroup>
  ```bash curl theme={null}
  curl -s -X POST https://api.usenexio.com/api/v1/engines/vendor-review/runs/3f6c2a8e-5b1d-4c7e-9a2f-8d4b6e1c0a57/outcomes \
    -H "Authorization: Bearer $NEXIO_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "event_id": "a2c4e6f8-1b3d-4f5a-8c7e-9d0b2a4c6e81",
      "event_type": "viewed",
      "payload": {
        "viewed_at": "2026-09-24T10:02:00Z"
      }
    }'
  ```

  ```python Python theme={null}
  import os

  import requests

  resp = requests.post(
      "https://api.usenexio.com/api/v1/engines/vendor-review/runs/3f6c2a8e-5b1d-4c7e-9a2f-8d4b6e1c0a57/outcomes",
      headers={"Authorization": "Bearer " + os.environ["NEXIO_API_KEY"]},
      json={
          "event_id": "a2c4e6f8-1b3d-4f5a-8c7e-9d0b2a4c6e81",
          "event_type": "viewed",
          "payload": {"viewed_at": "2026-09-24T10:02:00Z"},
      },
      timeout=30,
  )
  print(resp.status_code, resp.json())
  ```

  ```typescript TypeScript theme={null}
  const resp = await fetch(
    "https://api.usenexio.com/api/v1/engines/vendor-review/runs/3f6c2a8e-5b1d-4c7e-9a2f-8d4b6e1c0a57/outcomes",
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.NEXIO_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        event_id: "a2c4e6f8-1b3d-4f5a-8c7e-9d0b2a4c6e81",
        event_type: "viewed",
        payload: { viewed_at: "2026-09-24T10:02:00Z" },
      }),
    },
  );
  console.log(resp.status, await resp.json());
  ```
</CodeGroup>

Response `201` on the first write, and `200` with the same body on an identical retry:

```json theme={null}
{
  "event_id": "a2c4e6f8-1b3d-4f5a-8c7e-9d0b2a4c6e81",
  "status": "recorded"
}
```

### Idempotency

* An `event_id` is unique within your organization and the key's environment bucket: live keys share one bucket, and test keys, including every named sandbox, share the other. The same ID can be used once in live and once in test. Two sandboxes do not get separate namespaces: an ID already used from one sandbox never records a second outcome from another.
* Sending the same `event_id` again with the same run, engine, event type and payload returns `200` and records nothing new. Payloads are compared as JSON, so key order and whitespace do not matter.
* Sending it with anything different returns `409 event_id_reused`.
* A retry is recognized even after the run itself has been removed by retention, so a late retry never turns into a `404`.
* The run must belong to your organization, your key's environment, and the engine in the path. Otherwise `404 run_not_found`.

### Outcome errors

| Status | Code                                        | Cause                                                                                                                                                 |
| ------ | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400    | `invalid_request`                           | Body is not JSON, has unknown top-level fields, or is larger than 1 MiB.                                                                              |
| 400    | `missing_event_id`, `invalid_event_id`      | `event_id` absent, or not a 36-character hyphenated UUID v4.                                                                                          |
| 400    | `invalid_event_type`                        | Not one of the four types.                                                                                                                            |
| 400    | `invalid_payload`                           | Missing payload, missing or unknown payload fields, a bad timestamp, an unknown `reason_code`, an unknown `status`, or `other` without `reason_text`. |
| 400    | `reason_text_too_long`                      | `reason_text` over 1,000 characters.                                                                                                                  |
| 400    | `reason_taxonomy_version_unknown`           | A non-empty version other than `2026-08-31-unified`.                                                                                                  |
| 400    | `invalid_run_id`                            | Path run ID is not a UUID in the standard 36-character hyphenated form.                                                                               |
| 401    | `unauthorized`                              | Missing or invalid key.                                                                                                                               |
| 403    | `insufficient_capability`                   | Scoped key without `runs:write`.                                                                                                                      |
| 403    | `engine_binding_forbidden`                  | Scoped key not bound to the run's engine.                                                                                                             |
| 404    | `run_not_found`                             | No such run for this organization, environment and engine.                                                                                            |
| 409    | `event_id_reused`                           | The `event_id` was used for a different run, engine, type or payload.                                                                                 |
| 500    | `outcome_write_failed`, `run_lookup_failed` | Retry with the same `event_id`.                                                                                                                       |

## Annotations

`POST /api/v1/engines/{engine_slug}/runs/{run_id}/annotations`

| Field                  | Required | Meaning                                                                                                                                                        |
| ---------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `rating`               | Yes      | `good`, `bad` or `neutral`.                                                                                                                                    |
| `comment`              | Yes      | Free text. Cannot be empty or only whitespace.                                                                                                                 |
| `target`               | No       | A JSON object, or `null`, pointing at the part of the output the note is about. Any shape your team can read back, except that `kind` cannot be `opportunity`. |
| `submitter_id`         | No       | Your identifier for the person.                                                                                                                                |
| `time_on_task_seconds` | No       | How long the person spent reviewing, from 0 to 86,399.                                                                                                         |

Annotations sent over the API are stamped `source: "api"`, so they are distinguishable from annotations made in the portal's run review. Annotations are not idempotent: each call creates a new annotation. Unknown body fields are ignored. The run is found by its ID in your organization and environment; unlike outcomes, the engine slug in the path is not checked against the run.

<CodeGroup>
  ```bash curl theme={null}
  curl -s -X POST https://api.usenexio.com/api/v1/engines/vendor-review/runs/3f6c2a8e-5b1d-4c7e-9a2f-8d4b6e1c0a57/annotations \
    -H "Authorization: Bearer $NEXIO_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "rating": "bad",
      "comment": "The vendor sent a SOC 2 Type II report last week, so this gap is out of date.",
      "target": { "gap_id": "gap_001", "field": "recommendation" },
      "submitter_id": "dana.ortiz",
      "time_on_task_seconds": 240
    }'
  ```

  ```python Python theme={null}
  import os

  import requests

  resp = requests.post(
      "https://api.usenexio.com/api/v1/engines/vendor-review/runs/3f6c2a8e-5b1d-4c7e-9a2f-8d4b6e1c0a57/annotations",
      headers={"Authorization": "Bearer " + os.environ["NEXIO_API_KEY"]},
      json={
          "rating": "bad",
          "comment": "The vendor sent a SOC 2 Type II report last week, so this gap is out of date.",
          "target": {"gap_id": "gap_001", "field": "recommendation"},
          "submitter_id": "dana.ortiz",
          "time_on_task_seconds": 240,
      },
      timeout=30,
  )
  print(resp.status_code, resp.json())
  ```

  ```typescript TypeScript theme={null}
  const resp = await fetch(
    "https://api.usenexio.com/api/v1/engines/vendor-review/runs/3f6c2a8e-5b1d-4c7e-9a2f-8d4b6e1c0a57/annotations",
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.NEXIO_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        rating: "bad",
        comment: "The vendor sent a SOC 2 Type II report last week, so this gap is out of date.",
        target: { gap_id: "gap_001", field: "recommendation" },
        submitter_id: "dana.ortiz",
        time_on_task_seconds: 240,
      }),
    },
  );
  console.log(resp.status, await resp.json());
  ```
</CodeGroup>

Response `201`:

```json theme={null}
{
  "id": "d41e7a90-3c2b-4f6e-8a15-9b7c0e2d4f68",
  "org_id": "org_01HZX4K8Q2M6N9P3R5T7V1W3Y5",
  "run_id": "3f6c2a8e-5b1d-4c7e-9a2f-8d4b6e1c0a57",
  "target": { "gap_id": "gap_001", "field": "recommendation" },
  "rating": "bad",
  "comment": "The vendor sent a SOC 2 Type II report last week, so this gap is out of date.",
  "submitter_id": "dana.ortiz",
  "source": "api",
  "time_on_task_seconds": 240,
  "created_at": "2026-09-24T10:05:12.483920Z",
  "updated_at": "2026-09-24T10:05:12.483920Z"
}
```

Annotation timestamps carry microseconds. `submitter_user_id` appears only on annotations made by a signed-in portal user.

### Annotation errors

| Status | Code                                  | Cause                                                                          |
| ------ | ------------------------------------- | ------------------------------------------------------------------------------ |
| 400    | `invalid_request`                     | Body is not JSON, or a field has the wrong type.                               |
| 400    | `invalid_run_id`                      | Path run ID is not a UUID.                                                     |
| 400    | `invalid_rating`                      | Not `good`, `bad` or `neutral`.                                                |
| 400    | `missing_comment`                     | Empty or whitespace-only `comment`.                                            |
| 400    | `invalid_target`                      | `target` is neither a JSON object nor `null`.                                  |
| 400    | `invalid_time_on_task`                | Outside 0 to 86,399.                                                           |
| 401    | `unauthorized`                        | Missing or invalid key.                                                        |
| 403    | `insufficient_capability`             | Scoped key without `runs:write`.                                               |
| 403    | `engine_binding_forbidden`            | Scoped key not bound to the run's engine.                                      |
| 404    | `run_not_found`                       | No such run in your organization and environment.                              |
| 409    | `scoped_annotation_required`          | `target.kind` is `opportunity`. This route does not annotate opportunity rows. |
| 500    | `internal_error`, `run_lookup_failed` | Retry.                                                                         |

## How the signal is used

Outcomes and annotations are inputs to the engine's evaluation work, which Nexio operates in the portal:

* The **Judge** view compares automated quality verdicts with human annotations, to show how far the automated judge can be trusted.
* For engines of the `comparison`, `entity_analysis` and `opportunity` types, the improvement **pass** on the **Proposals** view reads recent annotations, outcome counts, judge verdicts and evaluation results, and drafts a configuration change as a proposal. A person approves or rejects it. Approval releases a new version behind the evaluation gate: a minor version by default, or a major version on an [`exact_required`](/engines/versions) engine when the change breaks its request or response schema.

Recording an outcome or an annotation never changes a run's output. See [Evaluation](/engines/evaluation).

## Credentials

Both routes accept the organization key, or a scoped key with `runs:write` bound to the run's engine. See [Authentication and access](/authentication).

<CardGroup cols={2}>
  <Card title="Evaluation" href="/engines/evaluation">
    Evaluation sets, the judge, and improvement proposals.
  </Card>

  <Card title="Submit run outcome" href="/api-reference/engines/submit-outcome">
    Endpoint reference.
  </Card>
</CardGroup>
