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

# Extractions

> A document plus a JSON Schema becomes validated fields with checkable evidence.

An extraction takes a stored [document](/v2/documents) and a schema, and returns
fields that **validated against that schema**, each traceable back to the words
in the document it came from.

```bash theme={null}
curl "$IMPEL_API/extractions" \
  -H "Authorization: Bearer $IMPEL_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 8b31…" \
  -d '{
    "document_id": "doc_…",
    "instructions": "This is a supplier invoice.",
    "schema": {
      "type": "object",
      "properties": {
        "invoice_number": { "type": "string" },
        "issued_on":      { "type": "string", "format": "date" },
        "total":          { "type": "number", "minimum": 0 },
        "line_items": {
          "type": "array",
          "items": {
            "type": "object",
            "properties": {
              "description": { "type": "string" },
              "amount":      { "type": "number" }
            },
            "required": ["description", "amount"]
          }
        }
      },
      "required": ["invoice_number", "total"]
    }
  }'
```

Needs `extractions:create`. Long work, so it answers **202** with a `job_id` —
see [Jobs](/v2/jobs).

## The schema

The same closed subset as [structured outputs](/v2/structured-outputs#the-schema-subset),
enforced by the same validator. There is no second one: two validators would
agree today and disagree in a year, and the one that disagrees about a payment
amount is the one that matters.

Refused with a 400 naming the keyword: `$ref`, `pattern`, `oneOf`/`anyOf`/
`allOf`/`not`, `if`/`then`/`else`, `const`, `default`.

Bounds: 16 KB, 6 levels deep, 200 nodes, 100 properties per object, 200 enum
values.

## The result

```bash theme={null}
curl "$IMPEL_API/extractions/ext_…" -H "Authorization: Bearer $IMPEL_KEY"
```

```json theme={null}
{
  "object": "extraction",
  "id": "ext_…",
  "status": "completed",
  "document_id": "doc_…",
  "schema": { "…": "the schema you sent, verbatim" },
  "data": { "invoice_number": "INV-8841", "total": 1240.00, "…": "…" },
  "evidence": [
    {
      "path": "invoice_number",
      "quote": "INV-8841",
      "page": 1,
      "char_start": 812,
      "char_end": 820,
      "match": "exact"
    }
  ],
  "evidence_coverage": 0.87,
  "job_id": "job_…",
  "failure": null,
  "warnings": [],
  "metadata": {},
  "knowledge": { "published": false }
}
```

`data` is `null` until the extraction completes. The schema is echoed back
verbatim so a caller comparing two results can see which shape each was asked
for.

### Evidence is checkable, not decorative

`char_start` and `char_end` are offsets into **the same string**
`GET /documents/{id}/representation` returns. Slice it and see the words for
yourself. That is the point of publishing them: evidence a caller cannot check is
a claim.

`match` says how the quote was found. `normalized` means the words are the
document's but the whitespace or case is not — which you need to know if you are
rendering a highlight. `page` is `null` rather than `0` when the parser reported
no layout covering that offset, because `0` reads as "page zero".

`evidence_coverage` is how much of the answer is actually traceable, as a number
you can threshold. A caller who has to count an evidence list to find that out
will not.

### Pruning and warnings

`additionalProperties` defaults to false and **prunes**: keys the model
volunteered that you did not ask for are removed before validation, and the
removal is reported in `warnings`. A model volunteering an extra field is the
commonest form of plausible-looking noise, and neither silently returning it nor
failing the whole extraction over it is the right answer.

### Failure names paths, never values

```json theme={null}
{
  "failure": {
    "code": "…",
    "message": "…",
    "validation_errors": ["total: expected a number", "issued_on: not a date"]
  }
}
```

You get the paths and the reasons. You do not get what the fields contained.

<Note>
  A `required` field that came back `null` counts as **missing**, not present.
  `{"invoice_number": null}` is not an answer, it is the absence of one wearing a
  key. Write `"type": ["string", "null"]` if you genuinely want "present,
  possibly null".
</Note>

## Extracting does not publish

```json theme={null}
"knowledge": { "published": false }
```

That field is in the resource on purpose. Extracting a salary from a CV does not
make that salary something an agent will quote to the next person who asks, and
you should not have to infer that from the absence of a field.

Publication is a separate, deliberate act —
[knowledge ingestion](/v2/knowledge-ingestion).

## Regular and premium

Extraction has two tiers, and **the tier is decided from the document itself,
never from anything you send.** Not a field, not a header, not a filename — a
caller who picks their own tier picks their own price.

| Tier        | Documents                                                                                   |
| ----------- | ------------------------------------------------------------------------------------------- |
| **Regular** | Digital files with selectable text, normal layouts                                          |
| **Premium** | Scans, image-only PDFs, handwriting, low-quality or complex documents needing a visual pass |

The filename is used only to look up a format, and even that is overridden by the
file's own magic bytes: a scanned PDF uploaded as `notes.txt` is probed as a PDF,
because the alternative is a rename that changes the bill. `metadata` is never
read for this at all.

<AccordionGroup>
  <Accordion title="A document the classifier cannot place is not guessed at">
    "Unsure" is not a tier — it is an instruction to go and find out. The
    document goes through the regular path first and the outcome is the
    tiebreaker: readable text means regular, a failure to find readable text
    means premium.

    The platform never bills premium on suspicion, and never bills regular on
    hope.
  </Accordion>

  <Accordion title="The tier follows the work that was actually performed">
    Premium is recorded when a premium extraction actually ran. Where premium was
    warranted but could not run — no engine configured, a format the premium path
    cannot render — the document is recorded as **regular**, with a reason saying
    so. Charging the premium rate for work nobody did is the worse error of the
    two.
  </Accordion>

  <Accordion title="Every decision is recorded with its evidence">
    Each tier decision carries the measurements it was made from, the thresholds
    in force at the time, and a version. A disputed line is answerable with "here
    is what we measured on your file", and a later threshold change does not
    silently rewrite why an old document was charged what it was charged.

    Reason codes are coarse where the evidence is coarse. `no_text_layer` does not
    claim to know whether a scan is typed or handwritten, because at that point
    nothing has looked at the pixels.
  </Accordion>
</AccordionGroup>

Rates are shared during evaluation and are not published here.

## Listing

```bash theme={null}
curl "$IMPEL_API/extractions?limit=50" -H "Authorization: Bearer $IMPEL_KEY"
```

Needs `extractions:read`. Returns `next_cursor`, read back as **`cursor`**.

A listing carries **neither the echoed schema nor the evidence**. Both are
unbounded per row, and a fifty-row page carrying every quote from fifty documents
— or fifty copies of a 16 KB schema — is a response nobody wanted. The detail
endpoint is where the whole resource lives.

## Idempotency

`POST /extractions` sets **`Idempotent-Replay: true`** on a replay — not
`Idempotency-Replayed`, which is what the runtime endpoints use. See
[Idempotency](/v2/idempotency).
