# Prepare, commit and verify

[Filling from context docs](/docs/fill-from-context) is one call: you send a form
and some documents, and you get a filled PDF back. That is the right shape when
you trust the result. When you want to see what Emboss decided **before** it
writes anything into the PDF, split the same work into two calls: **prepare**
proposes the answers, and **commit** writes them.

Prepare and commit run the same engine as a one-call context fill. The
difference is only where you stand: prepare stops at the proposal, so an agent
or a person can read every proposed answer, see which document it came from,
answer whatever is still open, and only then commit.

Both paths end in a **verification report**, a deterministic check of the
rendered document against the form's contract: arithmetic and geometry, not
inference. Before that, a second AI model looks over each written value against
its box's label and the boxes around it; its doubts, if any, land in the
receipt's `checks` block, never in the PDF.

## The two-call flow

### 1. Prepare

`POST /forms/{form_id}/prepare` takes the same multipart body as
`POST /forms/{form_id}/with-context`: one or more `context` file parts and an
optional `policy`. Nothing is written into the PDF.

```bash
curl -X POST https://api.getemboss.ai/forms/6c47f7f5-f921-4698-910f-95dd7d81310b/prepare \
  -H "Authorization: Bearer sk_..." \
  -F "context=@./w9-prior-year.pdf" \
  -F "context=@./company-profile.txt" \
  -F "policy=safe"
```

```json
{ "job_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "status": "processing" }
```

If you do not have a form yet, `POST /forms/prepare` takes the flat PDF as
`file` alongside the context, detects the fields, and prepares in one call.
Because it both extracts and prepares, it bills one form creation plus one
context fill.

### 2. Poll the job

Prepare answers on the same poll as a context fill,
`GET /forms/with-context/{job_id}`. Once it is `ready` the job carries a
`proposal_id`.

```bash
curl https://api.getemboss.ai/forms/with-context/a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d \
  -H "Authorization: Bearer sk_..."
```

```json
{
  "job_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
  "status": "ready",
  "proposal_id": "3e714663-ff2c-45ac-ae11-3c7015facfef",
  "session_id": "f28119f5-0f99-4226-b596-7df0cd7cb197",
  "error": null
}
```

### 3. Read the proposal

`GET /proposals/{proposal_id}` returns the proposal as its holder reads it: every
field with its state, its candidate values and the evidence behind each one, plus
the questions still open and what the form needs beyond values.

```bash
curl https://api.getemboss.ai/proposals/3e714663-ff2c-45ac-ae11-3c7015facfef \
  -H "Authorization: Bearer sk_..."
```

```json
{
  "proposal_id": "3e714663-ff2c-45ac-ae11-3c7015facfef",
  "form_id": "6c47f7f5-f921-4698-910f-95dd7d81310b",
  "status": "proposed",
  "policy": "safe",
  "mode": "two_call",
  "expires_at": "2026-10-06T18:31:49.316308+00:00",
  "fields": [
    {
      "field_id": 3,
      "label": "Name:",
      "state": "ready",
      "value": "Acme Holdings LLC",
      "confidence": "high",
      "origin": "document",
      "evidence": [{ "document_id": "d1", "document_name": "w9-prior-year.pdf", "page": 1, "method": "quote_check", "location": "Acme Holdings LLC" }],
      "candidates": [],
      "required": true
    },
    {
      "field_id": 14,
      "label": "EIN:",
      "state": "conflict",
      "value": null,
      "confidence": null,
      "origin": null,
      "evidence": [],
      "candidates": [
        { "value": "12-3456789", "evidence": [{ "document_id": "d1", "document_name": "w9-prior-year.pdf", "page": 1, "method": "quote_check", "location": "EIN 12-3456789" }] },
        { "value": "98-7654321", "evidence": [{ "document_id": "d2", "document_name": "company-profile.txt", "page": 1, "method": "quote_check", "location": "EIN: 98-7654321" }] }
      ],
      "required": true
    }
  ],
  "questions": [
    {
      "topic": "tax_identity",
      "field_ids": [14],
      "ask": "Two documents give different EINs. Which one is current?",
      "priority": 1
    }
  ],
  "confirmations": [
    {
      "field_id": 22,
      "label": "Date business started",
      "value": "03/01/2021",
      "confidence": "low",
      "evidence": [
        { "document_name": "company-profile.txt", "page": 1, "method": "quote_check", "location": "started trading in early 2021" }
      ]
    }
  ],
  "requirements": { "signatures": [], "attachments": [] },
  "summary": {
    "ready": 21,
    "missing": 1,
    "needs_confirmation": 1,
    "conflict": 1,
    "invalid": 0,
    "required_unresolved": 3
  }
}
```

`confirmations` lists the values the proposal already holds but has not
written under this policy: a low-confidence value, or a medium one under
`strict`. Names, dates, addresses and every other kind of answer are written
like any other once they meet the policy. Confirm them by field id on commit, or send a
value to override one. `questions` covers only fields with no usable value.

Reading a proposal is free.

A proposal you made with your own API key can be committed for **30 days**; after that a commit answers `409` "proposal has expired", though the proposal stays readable. A
proposal paid for anonymously through the pay-per-call API is shorter lived: its commit
token lasts 45 minutes, so commit it in the same sitting. An anonymous proposal
that is never committed is finished automatically once it expires, under the
policy it was prepared with, so a payer is never left with nothing; that commit
is marked `auto` in the receipt.

### 4. Commit

`POST /proposals/{proposal_id}/commit` writes the answers into the PDF, renders
it, verifies it, and returns a receipt. The body is JSON: an optional `confirm`
list of field ids to accept as proposed, any `values` you decided for fields
that were still open, an optional `policy` overriding the proposal's own, and
an optional `idempotency_key`.

```bash
curl -X POST https://api.getemboss.ai/proposals/3e714663-ff2c-45ac-ae11-3c7015facfef/commit \
  -H "Authorization: Bearer sk_..." \
  -H "Content-Type: application/json" \
  -d '{
        "confirm": [22],
        "values": [
          { "field_id": 14, "value": "12-3456789", "note": "confirmed with the client" }
        ],
        "policy": "safe"
      }'
```

```json
{ "job_id": "8c9d0e1f-2a3b-4c5d-9e6f-a1b2c3d4e5f6", "status": "processing" }
```

Commit carries values and confirmations only. Sending a multipart body returns
`400`: new documents go to prepare, not to commit. A value for a field the
contract does not know, or one that fails the field's own validation, returns
`400` and does **not** consume a commit. Neither does `400 option_conflict`,
raised when two options of one exclusive group are set true; it names them in
`conflicting_field_ids`. Nor does `400 confirm_conflict`, raised when a field
is both confirmed and given a value; it names them in `field_ids`.

Confirming a field that has no single value to accept (it is `missing`, or in
`conflict`) returns `400 not_confirmable`, naming those fields in
`invalid_field_ids`: send a value for them instead. It does not consume a
commit either.

You can commit the same proposal up to **10 times**, so a review loop can commit,
look at the PDF, and commit again with a correction. The eleventh returns `409`.
Re-commits are free: the work was already billed at prepare.

### 5. Collect the result

Poll `GET /forms/with-context/{job_id}` again. A finished commit job carries
`result`, `receipt_url` and `pdf_url` alongside the `report`.

```json
{
  "job_id": "8c9d0e1f-2a3b-4c5d-9e6f-a1b2c3d4e5f6",
  "status": "ready",
  "result": "complete",
  "receipt_url": "https://api.getemboss.ai/proposals/3e714663-ff2c-45ac-ae11-3c7015facfef/receipt",
  "pdf_url": "https://api.getemboss.ai/proposals/3e714663-ff2c-45ac-ae11-3c7015facfef/pdf",
  "report": {
    "result": "complete",
    "checked_at": "2026-09-06T18:33:02.114921+00:00",
    "issues": [],
    "adjustments": [{ "field_id": 7, "kind": "font_reduced", "from": 10.0, "to": 8.5 }],
    "counts": {
      "fields": 24,
      "written": 24,
      "required_unfilled": 0,
      "issues_blocking": 0,
      "issues_review": 0
    }
  }
}
```

Download the PDF from `GET /proposals/{proposal_id}/pdf` and the receipt from
`GET /proposals/{proposal_id}/receipt`.

When a submission needs more than the filled PDF itself, attach the
documents the form asks for and let commit hand over one package: see
[The submission package](/docs/submission-package).

## The three results

Every commit and every verification ends in one of three results:

| Result | Meaning |
| --- | --- |
| `complete` | No issues. Every required field is filled and everything written fits its box. |
| `review_required` | Issues were found, none of them blocking. The document is usable, but a person should look at it. |
| `incomplete` | At least one blocking issue. A required field is empty, or a value is not one of the field's allowed options. |

The rule is mechanical: any blocking issue makes it `incomplete`, any issue at
all makes it `review_required`, and nothing makes it `complete`.

A commit that breaks partway is not a result: its job's `status` is `failed`,
with error code `commit_failed`, and it carries no `result`.

## The five field states

Every field in a proposal carries one of five states. `ready` fields are written
at commit; the other four are the ones worth a question.

| State | Meaning |
| --- | --- |
| `ready` | A value was found and is confident enough under the current policy. |
| `missing` | Required, and no candidate value was found in the documents. |
| `needs_confirmation` | A value was found, but under this policy it is not confident enough to write. Listed under `confirmations`. |
| `conflict` | Two or more documents give different values for the same field. |
| `invalid` | A value was found, but it is not one of the field's allowed options. |

Each row carries `field_id`, `label`, `state`, `required`, and, for a `ready`
field, its `value`, `confidence`, `origin` and `evidence`. A `conflict`,
`needs_confirmation` or `invalid` field carries its `candidates` instead. An
optional field sitting at `missing` with no candidates is left out as noise;
a required one is always listed.

When documents disagree, an AI check ranks the readings: a `conflict` row's
`candidates` come in its order, and the row's `tiebreak` field carries the
check's record, with its `reason`. Readings it judges to be the same fact are
merged into one value.

## Policy

`policy` draws the line between `ready` and `needs_confirmation`. It takes two
values on prepare, on commit, and on a one-call context fill:

- `safe` (the default) treats high and medium confidence values as `ready`.
- `strict` treats only high confidence values as `ready`, leaving every medium
  confidence value for you to confirm.

Commit takes its own `policy`, so you can prepare under `strict` to see
everything the engine was unsure about, then commit under `safe` once you have
looked.

## Verify a PDF you already hold

`POST /forms/{form_id}/verify` runs the same check against a filled PDF you
supply, however it was filled. Emboss does not need to have rendered it. Send the
document as multipart `file`.

```bash
curl -X POST https://api.getemboss.ai/forms/6c47f7f5-f921-4698-910f-95dd7d81310b/verify \
  -H "Authorization: Bearer sk_..." \
  -F "file=@./filled-by-hand.pdf"
```

The response is the same report shape a commit returns: `result`, `checked_at`,
`issues`, `adjustments` and `counts`. Each issue names a `field_id`, a `page`, a
`kind`, a `detail` and whether it is `blocking`. There are eleven kinds:

| Kind | Meaning | Blocking |
| --- | --- | --- |
| `required_unfilled` | A required field was left empty. | Always |
| `option_invalid` | The value is not one of the field's allowed options. | Always |
| `group_inconsistent` | Two or more options are set in a group where only one may be. | Always |
| `checkbox_not_rendered` | A checkbox was meant to be ticked but the rendered document does not show it. | Always |
| `text_overflow` | The text did not fit its box even at the smallest font size. | Only if the field is required |
| `text_clipped` | The text needs more lines than the box holds, or the widget falls outside the page. | Only if the field is required |
| `signature_missing` | A signature field is unsigned. A person always has to sign, so this alone never makes a form incomplete. | Never |
| `unresolved_candidate` | A value was written, or two or more candidates remain, without an explicit confirmation. | Never |
| `attachment_required` | The form asks for a document to be attached alongside it. | Never |
| `not_rendered` | An XFA form's value was written into its data, but the rendered page does not show it. | Always |
| `comb_short` | A row of single-character boxes got fewer characters than it has boxes. | Never |

Values are matched to contract fields by widget name, so a PDF whose widgets do
not match this form returns `400`, as does a file that is not a PDF. Verify needs
an API key: there is no proposal here to authorise the call on its own.

Verify is billed as one `verify` at 1¢, which is
flat to 5 pages, then 1¢ per page. The first 5 each month are
free.

Once you have a filled PDF in hand, whether verify approved it or not, you
can read every value back out of it labelled: see
[Read a filled form back](/docs/read-filled-form).

## The receipt

`GET /proposals/{proposal_id}/receipt` returns a self-contained record of what
was decided, by whom, on what evidence, and what the document still needs. An
agent can hand it on without the recipient having to call Emboss.

It carries `receipt_version`, `emboss_version`, the `proposal_id` and `form_id`,
the `contract`'s `schema_version` and `detector_version`, the `policy` and `mode`,
`timestamps` (`proposed_at`, `committed_at`, `verified_at`), a `fields` row per
field (`field_id`, `label`, `value`, `state`, `origin`, `confidence`, `evidence`,
`note`), the `unresolved` block `{fields, attachments}`, holding the fields
still open with their candidates and the attachment requirements nothing was
uploaded for, the full `verification` report, `requirements` (signatures still
needed and attachments the form asks for), `files` with the filled PDF's size
and sha256 and, when that commit built one, the `package` entry with its own
size and sha256, a `commits` history (`n`,
`at`, `auto`, `result`, `values_supplied`), the `retention` policy, and
`checks`, the second model's review of the written values: a `summary` (`ran`,
`checked`, `doubted`, `cost`, `ms`) and one entry in `fields` (`field_id`,
`label`, `value`, `note`) per value it doubted. When that review could not run,
`check_note` says so.
Prepare takes the same optional `retention` field as a create call: see
[Ephemeral processing](/docs/ephemeral-processing) for the two values and
what a 410 means once a form is swept.

Each commit writes its own receipt, so the history grows with the review loop.

## Paying per call

Both [pay-per-call rails](/docs/pay-per-call) support the two-call flow on the
`fill-with-context` operation. Send `mode=two_call` alongside the file and the
pay-per-call API stops at the proposal instead of returning a finished PDF,
handing back three extra keys on the `202`: `proposal_id`, `commit_token`, and a
`proposal_url` with the token already in it.

Send the token back on the commit as `X-Proposal-Token`. It proves you own the
proposal you just paid for, and it lives 45 minutes.

```bash
curl -X POST https://api.getemboss.ai/proposals/3e714663-ff2c-45ac-ae11-3c7015facfef/commit \
  -H "X-Proposal-Token: <commit_token>" \
  -H "Content-Type: application/json" \
  -d '{"values": [{"field_id": 14, "value": "12-3456789"}]}'
```

A commit on the pay-per-call API has no payment step of its own, so a
[submission package](/docs/submission-package) has to be bought at prepare:
declare `package=true` on the prepare call and its quote prices the package
in. A commit there asking for a package the quote did not carry is refused
with `400` "package was not quoted; prepare again with package=true".

See [Pay per call with MPP](/docs/pay-per-call/mpp) and
[Pay per job with x402](/docs/pay-per-call/x402) for the payment step itself.

## Pricing

Prepare bills one **context fill**, the same as a one-call fill, at
3¢, which is flat to 5 pages, then 2¢ per page,
counting the form and its context documents together, with the first
5 each month free. Committing that proposal costs nothing,
including every re-commit up to the cap of 10. Reading the proposal, the receipt
and the PDF is free. See [Pricing](/pricing) for the full schedule.

## Also available as tools

The same three steps are exposed to agents directly: `prepare_form`,
`get_proposal`, `commit_proposal` and `verify_form` as
[MCP tools](/docs/mcp-tools), and `prepare`, `commit` and `verify` as
[A2A skills](/docs/a2a).

## See also

- [Pre-fill forms from context documents](https://getemboss.ai/use-cases/prefill-from-context)
- [Fillable PDF](https://getemboss.ai/glossary/fillable-pdf)
- [Ephemeral processing](https://getemboss.ai/docs/ephemeral-processing)
- [Fill from context docs](https://getemboss.ai/docs/fill-from-context)
- [MCP tools reference](https://getemboss.ai/docs/mcp-tools)
- [Use Emboss from agent frameworks (A2A)](https://getemboss.ai/docs/a2a)
- [Pay per call](https://getemboss.ai/docs/pay-per-call)
- [Read a filled form back](https://getemboss.ai/docs/read-filled-form)
- [The submission package](https://getemboss.ai/docs/submission-package)
- [Fill with context](https://getemboss.ai/docs/reference/fill-with-context)
- [Prepare and verify](https://getemboss.ai/docs/reference/prepare-and-verify)
