---
title: "Start a field-detection run"
method: POST
path: "/documents/{documentId}/detect"
tags: ["Detection"]
---

# Start a field-detection run

`POST /documents/{documentId}/detect`

## Request

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

Asks the detection agent to find the signature, initials, text, date, and checkbox fields in a document, and returns the run's id straight away.

Detection is a run, not a synchronous call. Nothing in this response carries fields. Two ways to get them:

- Poll `GET /documents/{documentId}/detect/{runId}` until `status` is `completed` or `failed`.
- Subscribe to the `detection.completed` and `detection.failed` webhook events and wait for one.

The status code says what happened. `202` means a new run was reserved and queued. `200` means no new work was started: either a completed run for the same document and agent version was reused (`cached: true`, `status: "completed"`), or a run for this document was already in flight and the caller was joined to it (`status: "processing"`).

Send `{"force": true}` to skip the completed-run cache and start a fresh run. Forcing never returns, mutates, or deletes an earlier run: history is kept for audit and comparison and is listed by `GET /documents/{documentId}/detect`. What forcing does not skip is the in-flight guard, since a run happening right now is already producing fresh output, so a forced request joins it rather than paying twice for the same answer. Unknown body keys are rejected rather than ignored, so a misspelled `force` fails loudly instead of quietly handing back the cached run it was meant to bypass.

The document must be `ready`; anything else answers 409 `document_not_ready`. A document over the 200-page limit answers 422. Detection is throttled at five runs per workspace per minute, answering 429 `rate_limited`, and against a daily spend cap, answering 429 `daily_budget_exceeded`; forcing buys neither a larger budget nor a larger allowance. Spend is charged to the workspace that owns the document, never to the user who issued the credential. On a deployment with no model provider configured, this endpoint answers 501.

Detection failure is a distinct signal from envelope failure: the envelope is untouched and fields can still be placed by hand.

Accepts an `Idempotency-Key`: a retry with the same key replays the original response instead of starting a second run.

## Headers

- `Idempotency-Key` string

## Request body

- DetectionRunRequest: An absent or empty body means `force: false`. Unknown keys are rejected rather than ignored.
  - `force` boolean: Skip the completed-run cache and start a fresh run. Earlier runs are left untouched and stay listed in the history. A run already in flight is joined rather than duplicated.

## Response `200`

No new run was started. Either a cached completed run answered the request, or an in-flight run was joined.

- object
  - `data` DetectionRunAccepted, required: What `POST /documents/{documentId}/detect` returns. `status` here describes the run this request resolved to, not the outcome of detection.
    - `detectionRunId` integer, required: The run to poll on `GET /documents/{documentId}/detect/{runId}`.
    - `status` 'queued' | 'processing' | 'completed', required: `queued` for a new run (202), `processing` when an in-flight run was joined (200), `completed` when a cached run answered (200).
    - `cached` boolean, required: True only when an already completed run answered the request and no new work was started.
    - `forced` boolean: Whether the run this request resolved to was forced. Absent on a cached answer.

## Other responses

- `202`: A new run was reserved and queued.
- `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.
- `409`: The resource's state forbids this transition, or another request owns the work. Codes: `invalid_state`, `invalid_status`, `credential_state`, `limit_exceeded`, `envelope_missing`, `not_retryable`, `owner_unavailable`, `workspace_unavailable`, `detection_not_completed`, `document_in_use`, `version_conflict`, `document_processing`, `document_processing_failed`, `normalized_pdf_unavailable`, `pages_not_rendered`, `document_not_ready`, `idempotency_in_progress` (which also carries `Retry-After`), `payment_replayed` (a USDC payment signature presented twice), `payment_pending` (a USDC settlement whose outcome is not yet known).
- `422`: The request parsed but its content is not acceptable. Codes: `validation_error` (envelope configuration, with an `issues` array in `details`), `invalid_envelope`, `envelope_not_ready`, `document_page_limit`, `idempotency_key_reuse`.
- `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.
- `500`: An unexpected server failure. Codes: `internal_error`, `queue_failed`, `upload_failed`, `validation_failed`. Safe to retry, and safe to retry with the same `Idempotency-Key`: a 5xx is never pinned to a key.
- `501`: AI field detection is not configured on this deployment (`feature_disabled`).
- `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)
