# Integration job verification

> Verify the one integration job your application test exercised, product events or feature flags, without needing a test error, trace, log or replay.

Canonical page: https://anectico.com/docs/reference/integration-job-verification/


After your agent wires Anectico into an application and runs a controlled test, it asks one
question about **the job it integrated**: did the selected events, or the selected flag exposures,
arrive as expected? Integration job verification answers that question and nothing else. A
product-event integration needs no test error, trace, log or session replay to verify, and a
verified job says nothing about those signals either.

It is a read: it sends nothing, changes nothing, stores no report, runs no model and is never
rerun for you. Your agent decides when to call it and when to retry.

| Surface | How |
| --- | --- |
| REST | `POST /api/v1/instrumentation/verify` with the JSON request as the body |
| MCP | `execute_read_action` with action `verify_integration` and the JSON request as arguments |
| CLI | `anectico doctor integration --file request.json` (plus the usual `--project` and `--json`) |

Project-bound keys may omit `project_id`; naming a different project is refused. The CLI refuses a
file whose `project_id` contradicts `--project`.

## Jobs

`job` selects exactly one job, and each job accepts only its own fields:

| `job` | Fields | Checked by |
| --- | --- | --- |
| `product_events` | `samples`, optional `tracking_plan_id` + `tracking_plan_revision` | [Integration event verification](/docs/reference/integration-verification) |
| `flags` | `exposures`, optional `compare_local_remote` | [Feature-flag exposure verification](/docs/reference/flag-integration-verification) |

Every request also carries `start_time` and `end_time`: the event-time window `[start, end)`,
millisecond precision, at most one hour. The selection fields have exactly the meaning, bounds and
presence rules of the read that checks them, so `session_id: ""` still checks for an empty session
and an omitted `session_id` skips the check.

These are refused with 400 before anything is read: a missing or unknown `job`; a field of the other
job (for example `exposures` on a `product_events` request, even as an empty list); an unknown
field; a field given twice; a body `org_id`; and, for the `flags` job, an exposure whose
`expected_evaluation_status` is not `evaluated` or whose `expected_is_fallback` is not `false`.
The `flags` job verifies successful application use; to verify that your application recorded a
fallback or an evaluation error, use feature-flag exposure verification directly.

`errors`, `traces_logs`, `replay`, `surveys` and `experiments` are not jobs here yet and are refused
by name. For an error or trace story, use `anectico doctor instrumentation` as described in
[verify your setup](/docs/start/verify-setup).

### Product events

```json
{
  "project_id": "2d3c9f8e-1a1b-4c5d-9e0f-123456789abc",
  "job": "product_events",
  "start_time": "2026-10-15T09:00:00Z",
  "end_time": "2026-10-15T10:00:00Z",
  "tracking_plan_id": "c0ffee00-1234-4abc-8def-0123456789ab",
  "tracking_plan_revision": "3",
  "samples": [
    {
      "event": "checkout_completed",
      "message_id": "test-run-42-checkout",
      "source_id": "7f1e2d3c-4b5a-4968-8776-5544332211aa",
      "distinct_id": "test-person-42",
      "session_id": "test-session-42"
    }
  ]
}
```

### Feature flags

```json
{
  "job": "flags",
  "start_time": "2026-10-15T09:00:00Z",
  "end_time": "2026-10-15T10:00:00Z",
  "compare_local_remote": false,
  "exposures": [
    {
      "message_id": "test-run-42-flag",
      "source_id": "7f1e2d3c-4b5a-4968-8776-5544332211aa",
      "flag_key": "checkout_v2",
      "expected_value": true,
      "expected_flag_version": "2026-10-15T09:30:12.123456Z",
      "evaluation_mode": "remote",
      "test_case_id": "case-42",
      "expected_evaluation_status": "evaluated",
      "expected_is_fallback": false,
      "expected_input_context": { "basis": "explicit_inputs", "required_inputs_present": true },
      "distinct_id": "test-person-42"
    }
  ]
}
```

`compare_local_remote` defaults to `false`: the answer then makes **no** claim that local and remote
evaluation agree, and carries no comparison. With `true`, each flag key and test case must have one
local and one remote exposure that are comparable and record the same value.

## The answer

| Field | Meaning |
| --- | --- |
| `verdict` | `verified_sample` when every required check is `observed_match`; `failed` when a required check is `mismatch` or `not_observed`; `undetermined` otherwise. |
| `checks` | The required checks in the job's order, each with `check`, `required`, `state`, `reason` and a `selector` (`index` into your samples or exposures, and the `event`, `message_id`, `source_id`, `flag_key`, `test_case_id` and property `key` it is about). |
| `population` | Counts of your selected samples or exposures: `requested`, `observed`, `not_observed`, `ambiguous`, `undetermined`, `verified`, `failed`. `basis` is `explicitly_selected_application_samples`. |
| `start_time`, `end_time` | The window as requested. |
| `effective_start_time`, `effective_end_time` | The window the read actually covered after readable history; empty when nothing was read. |
| `checked_at` | When the answer was released. |
| `sources` | The read behind the job (`product_events` or `flag_exposures`) with its `read_cutoff`, start and finish clocks, `limit_reached` and `limit_reason`. |
| `upstream_completeness` | Always `unknown`. |
| `limitations` | Plain statements of what this answer does not establish. |
| `links` | Always empty: there is no proof page for this read. |
| `evidence` | The full answer of the read that checked the job, under `product_events` or `flags`, exactly as that read returns it. |

The check names are:

- `product_events`: `tracking_plan` (when a plan is pinned), then per sample `event`,
  `distinct_id`, `session_id` (when requested), `groups` (the account, when requested), `identity`, `plan_declaration` (with a plan) and one
  `property` per declared or requested key, then `read_budget`.
- `flags`: per exposure `event`, `distinct_id`, `session_id` (when requested), `identity`,
  `flag_key`, `value`, `definition_version`, `evaluation_mode`, `test_case_id`,
  `evaluation_status`, `is_fallback`, `input_basis`, `required_inputs_present`, `input_binding`;
  with `compare_local_remote`, one `local_remote_comparison` per flag key and test case; then
  `read_budget`.

States keep the meaning of the read that produced them: `observed_match`, `mismatch`,
`not_observed`, `withheld`, `unavailable`, `ambiguous`, and `not_comparable` for a comparison
that could not be established (its `reason` is the first reason the flag read gave, such as
`missing_inputs`). `read_budget` is `unavailable` with a `limit_…` reason when the read reached a
limit. A withheld, unavailable, ambiguous or not-comparable required check never verifies, and an
observed event survives as its own fact next to it.

## Scopes

| Scope | What it needs it for |
| --- | --- |
| `query:read` | Required for every job. (MCP also needs `mcp:read`.) |
| `analytics:read` | Required for both jobs. |
| `flags:read` | Required for the `flags` job. |
| `persons:read` | The `identity` check. Without it identity is `withheld`. |
| `tracking_plans:read` | The `tracking_plan` and `plan_declaration` checks. Without them they are `withheld`. |
| `agents:content:read` | `property` checks and the recorded flag fields. Without it they are `withheld`. |

A job's required scope that is missing refuses the request (403) before anything is read. If a
scope, the project's content policy or the selected data changes while the read runs, no answer is
released: the call fails and you start a fresh read.

## Limits

The window is at most one hour, with at most 20 samples or exposures and at most 20 property
checks per sample. Every other limit is the limit of the read behind the job and is returned under
`evidence`. The whole call stops after 20 seconds. Normal rate limits and agent budgets apply;
there is no separate fee.

Two limits of the shared product-event read are listed in every answer's `limitations`:

- The read is not narrowed by date: it reads every retained event of the selected names in the
  project, so a project with millions of them can reach a read limit and report `unavailable` for
  an event that exists. Select fewer distinct event names per call in that case.
- Two sources that send the same event name, timestamp, distinct ID and message ID keep one stored
  event, so verifying one source can report `not_observed` because the other source's copy was
  kept. Use a unique `message_id` per source in tests.

## Retrying

A call that fails with 429 (another verification for the project is running), 409 or 503 released
no answer; retry the **same** request shortly. A `not_observed` or `unavailable` check right after
the test usually means the event has not been read yet: wait briefly and verify again with the same
request. Do not resend events to make them appear.

## What a verified sample does not prove

- That other events, flags, people or application paths are instrumented: only the selected
  samples of this one job were checked.
- That errors, traces, logs or replay work: they were neither required nor checked.
- That everything the application emitted arrived: SDK configuration, sampling and client-side loss
  are not observed, and an accepted intake request is not readable evidence.
- That a person saw anything, or that the application authenticated its own inputs.

The stages between your code and stored evidence, how long to wait before trusting a negative
answer, and what has and has not been run are on [What integration verification proves and what it
does not](/docs/reference/integration-verification-limits).
