---
title: "Poll a detection run"
method: GET
path: "/documents/{documentId}/detect/{runId}"
tags: ["Detection"]
---

# Poll a detection run

`GET /documents/{documentId}/detect/{runId}`

## Request

- Base URL: `https://inkseal.net/api/v1`
- URL: `https://inkseal.net/api/v1/documents/{documentId}/detect/{runId}`
- Auth: HTTP bearer

The run's current state. `status` is `processing`, `completed`, or `failed`.

`completed` carries `fields`, each with its type, page index, normalized bounds, label, confidence, and a one-line reason for the placement, plus the agent's `formatting` assessment of the document. When more fields were detected than an envelope may hold, the highest-confidence ones are kept and `warning` says so.

`failed` carries `error`. Detection failing is a distinct signal from an envelope failing: the envelope is untouched, and the caller can force another run or place fields by hand.

Every status carries the run's own metadata: whether it was forced, the agent version, how far it got, its timings, what it cost and where that number came from, and which models answered. `reservedCostUsd` is what is being held against the daily cap while the run is in flight; `costUsd` is what it actually spent and stays null until it finishes. A failed run can still have spent real money, so its cost is reported too.

Field coordinates are normalized ratios in the range 0 to 1 against the page box, with the origin at the top-left corner: `xRatio` and `yRatio` locate the top-left corner of the field, `widthRatio` and `heightRatio` size it. `pageIndex` is 0-based, matching the page image endpoints. Nothing here is in pixels or points, so the same numbers hold at any render scale.

## Response `200`

The run, in whichever state it is currently in.

- object
  - `data` DetectionRun, required: A detection run in full. `fields`, `formatting`, and `warning` appear only on a completed run; `error` only on a failed one.
    - `detectionRunId` integer, required
    - `status` 'processing' | 'completed' | 'failed', required: A detection run's lifecycle. `processing` while it is queued or running, then `completed` or `failed`.
    - `forced` boolean, required: Whether this run was explicitly asked for instead of a cached one.
    - `agentVersion` string, nullable, required
    - `pagesProcessed` integer, nullable, required: How many pages the run has finished. Progress while it is running.
    - `totalPages` integer, nullable, required: How many pages the run was reserved for.
    - `modelRequested` string, required: The model the deployment asked for.
    - `modelUsed` string, nullable, required: The model recorded for the run overall.
    - `primaryModelUsed` string, nullable, required: The model that answered the primary classify and detect pass.
    - `fallbackModelsTried` string[], required: Models the cascade fell through to. Empty when the primary handled the whole run.
    - `tokenUsage` DetectionTokenUsage, required: Model token usage for the run. Each value is null until the provider reports it.
      - `promptTokens` integer, nullable, required
      - `completionTokens` integer, nullable, required
      - `totalTokens` integer, nullable, required
    - `reservedCostUsd` number, required: What is being held against the workspace's daily cap for this run, in USD. Reported alongside `costUsd` rather than merged into it, because one is a reservation and the other is spend.
    - `costUsd` number, nullable, required: What the run actually spent, in USD. Null until it finishes. A failed run can still have spent money, so this is reported there too.
    - `costSource` 'actual' | 'estimated' | 'mixed' | 'null', nullable, required: Where `costUsd` came from: `actual` when the provider reported it, `estimated` when a conservative estimate was required, `mixed` when the run used both. Null until the run finishes.
    - `estimatedCostUsd` number, nullable, required: The estimate made before the run, in USD.
    - `createdAt` string, date-time, required
    - `startedAt` string, date-time, nullable, required
    - `completedAt` string, date-time, nullable, required
    - `fields` DetectedField[]: The detected fields. Present only when `status` is `completed`.
      - `type` 'signature' | 'initials' | 'text' | 'date' | 'checkbox', required: The kind of field detected. The same vocabulary envelope fields use.
      - `pageIndex` integer, required: 0-based page index, matching the page image endpoints.
      - `bounds` DetectedFieldBounds, required: Where the field sits on its page, as ratios of the page box in the range 0 to 1 with the origin at the top-left corner. `xRatio` and `yRatio` locate the field's top-left corner; `widthRatio` and `heightRatio` size it. No pixels and no points, so the numbers hold at any render scale.
        - `xRatio` number, required
        - `yRatio` number, required
        - `widthRatio` number, required
        - `heightRatio` number, required
      - `label` string, required: The label derived from the surrounding text, or a default for the field type.
      - `detectionConfidence` number, nullable, required: Confidence in this placement, 0 to 1. Null when the run recorded none.
      - `detectionReason` string, nullable, required: A one-line, human-readable reason for the placement.
      - `detectionSource` 'agent' | 'manual' | 'agent-then-manual', required: Who placed the field. Detection output is `agent`; the other values appear once a person has edited a placement.
      - `party` object, nullable, required
        - `key` string, required
        - `label` string, required
        - `confidence` number, required
      - `fillHint` 'signer' | 'sender', required
      - `fillHintConfidence` number, required
      - `suggestedName` string, required
    - `formatting` object, nullable: The agent's own assessment of the document, as the visual editor sees it. Its shape differs by pipeline, so it is passed through rather than narrowed: the agent path records strategy and budget outcome, the single-shot path records document type and placement quality. Present only when `status` is `completed`.
    - `warning` string, nullable: Set when more fields were detected than an envelope may hold and the lowest-confidence ones were dropped. Present only when `status` is `completed`.
    - `error` string: Why detection failed. Present only when `status` is `failed`. The envelope is untouched: the caller can force another run or place fields by hand.

## Other responses

- `400`: The request is malformed or the resource is in the wrong state for a synchronous check. Codes: `validation_error`, `invalid_body`, `invalid_parameter`, `invalid_idempotency_key`, `invalid_event_id`, `invalid_file`, `invalid_url`, `invalid_status`, `key_mismatch`, `no_signers`, `not_completed`, `envelope_exists`, `document_not_ready`, `upload_failed`.
- `401`: The credential is missing, malformed, unknown, expired, revoked, or past its rotation grace window. Code: `unauthorized`. Every authentication failure is deliberately indistinguishable.
- `403`: The credential is valid but may not do this. Codes: `insufficient_scope` (the route's scopes are not all held; `details` names `requiredScopes` and `missingScopes`), `forbidden` (the capability check on the resolved resource failed), `scope_escalation` (a grant wider than the caller's own was requested).
- `404`: The resource does not exist, was deleted, or belongs to another workspace. Code: `not_found`. The three cases are answered identically on purpose, so a 404 never confirms that an identifier exists elsewhere.
- `429`: A limit was reached. Codes: `rate_limited` (the credential's per-bucket HTTP limit, or the per-workspace detection throttle), `workspace_limit_exceeded` (a workspace content or send limit; `details.limit` names it), `daily_budget_exceeded` (the workspace's daily detection budget). The four rate-limit headers are sent on this response and on no successful one, so a client cannot read its remaining budget without being refused. Retrying after `Retry-After` is always safe: a rate-limit refusal happens before the handler runs.
- `503`: A dependency the request needs is unreachable and nothing was changed. Codes: `queue_unavailable` (a background job could not be enqueued; retry the call), `billing_unavailable` (Stripe would not open a checkout session; nothing was charged, retry the call), `service_unavailable` (the rate-limit store is down, so the request was refused before running).

---

[API](https://skmtc.dev/inkseal/apis/inkseal-api.md) · [All operations](https://skmtc.dev/inkseal/apis/inkseal-api/llms.txt) · [OpenAPI document](https://skmtc.dev/inkseal/apis/inkseal-api/revisions/c440c981d578?raw)
