Prepare, commit and verify

Filling from context docs 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.

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"
{ "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.

curl https://api.getemboss.ai/forms/with-context/a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d \
  -H "Authorization: Bearer sk_..."
{
  "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.

curl https://api.getemboss.ai/proposals/3e714663-ff2c-45ac-ae11-3c7015facfef \
  -H "Authorization: Bearer sk_..."
{
  "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.

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"
      }'
{ "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.

{
  "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.

The three results

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

ResultMeaning
completeNo issues. Every required field is filled and everything written fits its box.
review_requiredIssues were found, none of them blocking. The document is usable, but a person should look at it.
incompleteAt 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.

StateMeaning
readyA value was found and is confident enough under the current policy.
missingRequired, and no candidate value was found in the documents.
needs_confirmationA value was found, but under this policy it is not confident enough to write. Listed under confirmations.
conflictTwo or more documents give different values for the same field.
invalidA 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.

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:

KindMeaningBlocking
required_unfilledA required field was left empty.Always
option_invalidThe value is not one of the field's allowed options.Always
group_inconsistentTwo or more options are set in a group where only one may be.Always
checkbox_not_renderedA checkbox was meant to be ticked but the rendered document does not show it.Always
text_overflowThe text did not fit its box even at the smallest font size.Only if the field is required
text_clippedThe text needs more lines than the box holds, or the widget falls outside the page.Only if the field is required
signature_missingA signature field is unsigned. A person always has to sign, so this alone never makes a form incomplete.Never
unresolved_candidateA value was written, or two or more candidates remain, without an explicit confirmation.Never
attachment_requiredThe form asks for a document to be attached alongside it.Never
not_renderedAn XFA form's value was written into its data, but the rendered page does not show it.Always
comb_shortA 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.

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 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 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.

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 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 and Pay per job with 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 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, and prepare, commit and verify as A2A skills.