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:
| 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 asready.stricttreats only high confidence values asready, 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:
| 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.
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.