Free kit / Contract OCR API Pack
Contract OCR API: Clean JSON Out of Messy PDFs
Template based OCR breaks the moment a field moves an inch. You get the vision prompt, the enforced schemas and the validation layer I use to turn contracts and filings into strict JSON.
Why generalist models fail on dense legal PDFs, and the evaluation that settled it, is in Contract OCR API: Extract Structured Legal Data with Claude 4.7. Related: the Workflow Audit Kit finds where the documents are getting stuck before you build anything.
Before you start
This pack is the extraction layer I put in front of any document pipeline that has to produce structured records instead of a pile of text. The vision model reads the page in context. The schema decides what is allowed to leave. The validator decides what is allowed to write. You get the prompt, the schemas and the Python call that runs them. The thresholds and the retry wording sit beside it.
Template based OCR treats a page as a fixed map of coordinates, with every field pinned to one spot and every label to another. It works on one clean scan and falls apart on everything else, from a retainer with new letterhead to a lease with a stapled addendum to an ID photo taken at an angle. A vision-language model reads the page the way a person does, through layout, tables, handwriting and shifted blocks. The schema is what keeps that read honest.
Never let the model write directly into a system of record. Extraction produces a candidate payload. Validation decides if the payload is safe. Confidence decides if a human sees it. Only then does anything write. Keep the original document beside every extracted object so any field can be traced back to a page.
Section 1 · Why templates fail and context reads hold
Template OCR fails in specific, repeatable ways. Name the failure, and you know which layer to replace. Work the tables below against your current extractor before you change anything.
Table 1A · Template failure modes
| Failure mode | What it looks like | Why the template breaks | Context read behaviour |
|---|---|---|---|
| Field drift | Example: notice clause sits two lines lower after a letterhead change | Anchor box no longer overlaps the value | Model reads the clause from its heading |
| Layout variants | |||
| Multi-page tables | |||
| Handwritten annotations |
Table 1B · Document inventory (fill this in)
List every document type you extract. If a type appears monthly or more, it ships a schema. If it appears yearly, route it to the generic contract schema and review.
| Document type | Monthly volume | Fields downstream needs | Current extractor | Known break pattern |
|---|---|---|---|---|
| Example: engagement / retainer | 40 | parties, fee, term, notice days, jurisdiction | Zapier + fixed templates | fee block drifts on new letterhead |
What a context read does
| Layer | What it decides | Pass condition | Common failure |
|---|---|---|---|
| Page level | Document type, page count, whether the scan is legible at all | One type from your enum, every page accounted for, no unreadable flag | An exhibit pack arrives as one PDF and the model classifies by the cover sheet |
| Region level | Which blocks are parties, consideration, term, notice, governing law, signature | Each required region located or explicitly absent, with page number | Definitions mistaken for the operative clause |
| Field level | The typed value plus the source quote that proves it | Value, type, evidence quote and page on every required field | A number copied from a schedule instead of the operative clause |
A licence to accept whatever comes back. The model can still invent a date, misread a currency, or pick the wrong of two parties. That is why the schema, the validator and the escalation queue are not optional extras. They are the product.
Section 2 · The extraction prompt and its output rules
One prompt template, parameterised by document type and target schema. The rules are hard on purpose. A prompt that allows prose allows silent parse failures downstream.
Extraction prompt template
ROLE
You extract structured fields from legal documents. You never
summarize, never infer, never invent.
INPUT
document_type: {{engagement | nda | service_agreement | lease |
court_filing | identity | contract}}
matter_hint: {{case_type if known}}
target_schema: {{schema_name}}
page_count_hint: {{expected pages, or null}}
RULES
1. Return JSON only. No prose before or after.
2. Wrap output exactly as:
<response><json>{...}</json></response>
3. Read numbers as they appear in the document. Spell-out
numbers convert to digits: "thirty-six thousand six hundred
five" becomes 36605. If the digits and the words disagree,
set numeric_conflict: true and use neither value.
4. Dates become YYYY-MM-DD. If only a partial date exists,
set date_precision to year, month or day.
5. Jurisdiction is the stated one. If absent or ambiguous,
return null. Do not guess from the party names.
6. Every field in the required list must be present. Use null
for genuinely absent values, never an empty string.
7. Quote source text for each extracted value under
"evidence". If you cannot locate the source text, the
field is null.
8. Party fields capture the full legal name as written.
Roles are separate fields. Do not merge a company and its
signatory into one string.
9. Currency is ISO 4217. If the document states a symbol with
no currency code, infer the code only when the symbol is
unambiguous for the stated jurisdiction. Otherwise
currency: null and amount: null.
10. Never average, round or normalise a figure the document
states. Transcribe, then let code normalise.
11. If two sections disagree on the same fact, keep both under
"conflicts" and set the primary field to null.
12. If the document is unreadable, return
{"status":"unreadable","reason":"..."}.
OUTPUT
JSON object conforming to the target schema.What each rule prevents
| Rule | Failure it stops | Where it bites if you drop it |
|---|---|---|
| 1 JSON only | Markdown fences and commentary around the object | Every downstream parse step |
| 2 response / json wrapper | Model talking past a strict extractor | Retry logic, logging, schema registry |
| 3 digit vs word numbers | Invented numeric conversions | Fees, deposits, penalties, damages |
| 4 date precision | False day-level confidence on a month-only date | Notice windows, limitation periods |
| 5 jurisdiction as stated | Guessed governing law from a party address | Venue, conflict checks, court routing |
| 6 null not empty string | Empty writes that look like real values | CRM required fields, search indexes |
| 7 evidence per field | Fields nobody can trace back to a page | Reviewers, audits, corrections |
| 8 party roles separate | Company and signatory collapsed into one name | Dedupe, conflict check, service lists |
| 9 currency explicit | Silent USD assumption on a EUR document | Every money field |
| 10 transcribe first | Model rounding 36605 into 36600 | Fee schedules, escalators |
| 11 conflicts array | One of two disagreeing values chosen at random | Amendments, riders, prior drafts |
| 12 unreadable status | Confident junk from a blank or corrupt page | Batch jobs that would write garbage |
Retry wording (use as written)
When the first pass fails envelope parse or schema validation, retry once with a reduced prompt. Do not argue with the model. Narrow the job.
RETRY
The previous response did not validate against the target
schema. Return JSON only, wrapped as
<response><json>{...}</json></response>.
Extract ONLY these fields:
{{comma separated required field list}}
If a field is not on the page, return null.
If two sections disagree, set the field to null and list the
quotations under "conflicts".
Do not explain. Do not add fields.Two attempts is the ceiling. A third attempt is superstition. After two failures the document goes to the reviewer queue with both raw responses attached.
Section 3 · Schemas that reject anything the model invents
There is one base envelope and six type-specific property sets. The envelope carries status, confidence, evidence and conflict state. The property sets carry the fields that differ by document. Validate the envelope first, then the property set for the type you requested. Anything not in the schema is dropped before it reaches a record.
Base envelope schema
{
"$id": "ocr/envelope.v1.json",
"type": "object",
"properties": {
"document_type": {"enum": ["engagement","nda","service_agreement",
"lease","court_filing","identity","contract"]},
"target_schema": {"type": "string"},
"schema_version": {"type": "string"},
"page_count": {"type": "integer", "minimum": 1},
"status": {"enum": ["ok", "partial", "unreadable", "conflict"]},
"confidence": {"type": "number", "minimum": 0, "maximum": 1},
"meets_threshold": {"type": "boolean"},
"numeric_conflict": {"type": "boolean"},
"date_precision": {"enum": ["day", "month", "year", null]},
"jurisdiction": {"type": ["string", "null"]},
"evidence": {
"type": "object",
"additionalProperties": {
"type": "object",
"properties": {
"quote": {"type": "string"},
"page": {"type": "integer", "minimum": 1}},
"required": ["quote", "page"]}
},
"conflicts": {
"type": "array",
"items": {
"type": "object",
"properties": {
"field": {"type": "string"},
"values": {"type": "array", "items": {"type": "string"}},
"pages": {"type": "array", "items": {"type": "integer"}}},
"required": ["field", "values"]}
},
"fields": {"type": "object"}
},
"required": ["document_type","target_schema","schema_version","status",
"confidence","meets_threshold","numeric_conflict",
"evidence","fields"],
"additionalProperties": false
}Three points. additionalProperties: false is the rejection valve for invented keys. meets_threshold lets your orchestrator branch on a boolean instead of parsing free text. evidence is keyed by field name so a reviewer opens one object and sees both the claim and the page.
Type property sets
Each block below is the fields object for one document type, plus its required list. Load the base envelope, then the set for the requested type. Null means absent. Empty string is a validation failure.
// 1. ENGAGEMENT / RETAINER
"fields": {
"client_name": {"type": ["string","null"]},
"firm_name": {"type": ["string","null"]},
"responsible_attorney": {"type": ["string","null"]},
"practice_area": {"type": ["string","null"]},
"fee_type": {"enum": ["hourly","flat","contingency",
"retainer","hybrid",null]},
"fee_amount": {"type": ["number","null"], "minimum": 0},
"currency": {"type": ["string","null"], "maxLength": 3},
"billing_interval": {"enum": ["hour","month","matter","on_event",null]},
"retainer_amount": {"type": ["number","null"], "minimum": 0},
"effective_date": {"type": ["string","null"]},
"termination_notice_days": {"type": ["integer","null"], "minimum": 0},
"scope_summary": {"type": ["string","null"]},
"case_type": {"type": ["string","null"]},
"urgency": {"enum": ["low","normal","high","critical",null]}
}
"required_fields": ["client_name","firm_name","fee_type",
"effective_date","jurisdiction"]
// 2. NDA
"fields": {
"disclosing_party": {"type": ["string","null"]},
"receiving_party": {"type": ["string","null"]},
"mutual": {"type": ["boolean","null"]},
"effective_date": {"type": ["string","null"]},
"term_months": {"type": ["integer","null"], "minimum": 0},
"survival_months": {"type": ["integer","null"], "minimum": 0},
"confidential_info_scope": {"type": ["string","null"]},
"permitted_use": {"type": ["string","null"]},
"return_or_destroy_days": {"type": ["integer","null"], "minimum": 0},
"injunctive_relief": {"type": ["boolean","null"]},
"governing_law": {"type": ["string","null"]},
"non_solicit_months": {"type": ["integer","null"], "minimum": 0}
}
"required_fields": ["disclosing_party","receiving_party",
"effective_date","jurisdiction"]
// 3. SERVICE AGREEMENT / SOW
"fields": {
"provider_name": {"type": ["string","null"]},
"customer_name": {"type": ["string","null"]},
"service_description": {"type": ["string","null"]},
"start_date": {"type": ["string","null"]},
"end_date": {"type": ["string","null"]},
"auto_renew": {"type": ["boolean","null"]},
"renewal_notice_days": {"type": ["integer","null"], "minimum": 0},
"total_contract_value": {"type": ["number","null"], "minimum": 0},
"currency": {"type": ["string","null"], "maxLength": 3},
"payment_terms_days": {"type": ["integer","null"], "minimum": 0},
"sla_uptime_pct": {"type": ["number","null"], "maximum": 100},
"liability_cap_multiple": {"type": ["number","null"], "minimum": 0},
"termination_for_convenience_days": {"type": ["integer","null"], "minimum": 0}
}
"required_fields": ["provider_name","customer_name","start_date","jurisdiction"]
// 4. LEASE
"fields": {
"landlord_name": {"type": ["string","null"]},
"tenant_name": {"type": ["string","null"]},
"property_address": {"type": ["string","null"]},
"lease_type": {"enum": ["commercial","residential",
"sublease","ground",null]},
"commencement_date": {"type": ["string","null"]},
"expiration_date": {"type": ["string","null"]},
"base_rent_amount": {"type": ["number","null"], "minimum": 0},
"currency": {"type": ["string","null"], "maxLength": 3},
"rent_frequency": {"enum": ["month","quarter","year",null]},
"security_deposit": {"type": ["number","null"], "minimum": 0},
"escalation_pct": {"type": ["number","null"], "minimum": 0},
"renewal_options": {"type": ["integer","null"], "minimum": 0},
"use_clause": {"type": ["string","null"]},
"square_footage": {"type": ["number","null"], "minimum": 0}
}
"required_fields": ["landlord_name","tenant_name","property_address",
"commencement_date","jurisdiction"]
// 5. COURT FILING
"fields": {
"court_name": {"type": ["string","null"]},
"caption_plaintiff": {"type": ["string","null"]},
"caption_defendant": {"type": ["string","null"]},
"case_number": {"type": ["string","null"]},
"document_title": {"type": ["string","null"]},
"filing_date": {"type": ["string","null"]},
"filing_party": {"type": ["string","null"]},
"filing_type": {"enum": ["complaint","motion","brief","order",
"notice","exhibit","declaration",
"other",null]},
"hearing_date": {"type": ["string","null"]},
"response_deadline": {"type": ["string","null"]},
"certificate_of_service": {"type": ["boolean","null"]},
"exhibit_count": {"type": ["integer","null"], "minimum": 0}
}
"required_fields": ["court_name","case_number","document_title","jurisdiction"]
// 6. IDENTITY DOCUMENT
"fields": {
"doc_kind": {"enum": ["passport","drivers_license",
"national_id","residence_permit",
"other",null]},
"full_name": {"type": ["string","null"]},
"given_names": {"type": ["string","null"]},
"family_name": {"type": ["string","null"]},
"document_number": {"type": ["string","null"]},
"issuing_country": {"type": ["string","null"]},
"issuing_authority": {"type": ["string","null"]},
"date_of_birth": {"type": ["string","null"]},
"issue_date": {"type": ["string","null"]},
"expiry_date": {"type": ["string","null"]},
"address_line": {"type": ["string","null"]},
"mrz_present": {"type": ["boolean","null"]}
}
"required_fields": ["doc_kind","full_name","document_number","expiry_date"]Every payload carries schema_version. Adding a field bumps the minor version, and you accept both shapes for one month. Removing or retyping a field bumps the major version and stops the old shape. Store the version on the extracted object so a later reader knows which rules produced it.
Table 3A · Property set checklist (fill this in)
| Document type | Required fields (comma list) | Enum values your CRM accepts | Confidence floor | Schema version |
|---|---|---|---|---|
| Engagement / retainer | client_name, firm_name, fee_type, effective_date, jurisdiction | fee_type: hourly, flat, retainer | 0.88 | 1.0 |
| NDA | ||||
| Service agreement | ||||
| Lease | ||||
| Court filing | ||||
| Identity document |
Section 4 · Validation: numbers, dates, cross references
Validation runs in code. The prompt asks for honesty. The validator enforces it. Run every check below on every payload before anything writes.
Validation rules
- Envelope check: the
<response><json>wrapper parses, then the inner object parses as JSON. Reject and retry with the reduced prompt if not. - Type check: every field matches its declared type. Integer fields reject floats. Number fields reject strings with currency symbols still attached.
- Required check: every field in the type's required list is present. Null is allowed only where the schema allows it. A required field that is null forces escalation.
- Range check: amounts non-negative, notice days between 0 and your ceiling, confidence between 0 and 1, expiry dates in the past on identity documents.
- Enum check: fee type, lease type, filing type, urgency and jurisdiction limited to values your CRM accepts. Anything else is a mapping failure and gets rejected.
- Numeric consistency: if digits and words disagree, or the same figure appears twice with different values, set
numeric_conflictand stop. Language models hallucinate numeric conversions. This is the most expensive class of error in a legal document. - Date shape and order: values match their
date_precision. End date after start date. Expiry after issue. Hearing date not before filing date. Notice windows consistent with the termination clause. - Cross-reference parties: party names appear in the evidence map. Each party name is checked against your conflict list and existing contact records before any write.
- Evidence present: each required field has a source quote and a page. A field with no evidence is treated as absent.
- Unknown key check: any property outside the schema is dropped and logged. The model does not get to invent fields.
Cross-check matrix
| Check | Rule | On fail | Severity |
|---|---|---|---|
| Money: digits vs words | Both parse to the same integer amount | Set numeric_conflict, drop amount | Block write |
| Money: fee vs schedule | Operative clause figure equals schedule figure | Record both under conflicts | Block write |
| Money: currency present | ISO code on every amount field | Null amount and currency | Block write |
| Dates: precision | Year-only dates never written as day-level | Downgrade precision, flag | Warn |
| Dates: ordering | end after start, expiry after issue, hearing after filing | Null both ends, escalate | Block write |
| Dates: notice window | notice days match the termination clause | Record both under conflicts | Escalate |
| Parties: evidence | Every party name quotable on a page | Null the party | Escalate |
| Parties: role split | Company and signatory are separate values | Merge attempt rejected | Block write |
| Parties: conflict list | No match against known conflict records | Stop pipeline, conflict review | Block write |
| Jurisdiction: stated | Present in text, not inferred | Null, attorney review | Escalate |
| Page accounting | page_count equals received pages | Request missing pages | Block write |
Python: vision call, schema validation, routing
One module. Call the model with the document and the prompt. Parse the envelope. Validate against the base schema and the type property set. Run the cross-checks. Route on the result. This is the shape I ship.
import base64, json, re
import anthropic
from jsonschema import Draft7Validator
client = anthropic.Anthropic()
ENVELOPE = json.load(open("schemas/ocr/envelope.v1.json"))
SETS = json.load(open("schemas/ocr/property_sets.v1.json"))
FLOORS = {"engagement": 0.88, "nda": 0.86, "service_agreement": 0.86,
"lease": 0.87, "court_filing": 0.85, "identity": 0.92,
"contract": 0.85}
WRAPPER = re.compile(r"<response><json>(.*)</json></response>", re.S)
JURISDICTION_REQUIRED = {"engagement", "nda", "service_agreement",
"lease", "court_filing", "contract"}
def doc_block(path):
data = open(path, "rb").read()
b64 = base64.b64encode(data).decode()
if path.endswith(".pdf"):
return {"type": "document",
"source": {"type": "base64",
"media_type": "application/pdf", "data": b64}}
return {"type": "image",
"source": {"type": "base64", "media_type": "image/png",
"data": b64}}
def parse_envelope(text):
m = WRAPPER.search(text or "")
if not m:
raise ValueError("envelope_missing")
return json.loads(m.group(1))
def validate_payload(payload, doc_type):
errs = [f"envelope:{e.message}"
for e in Draft7Validator(ENVELOPE).iter_errors(payload)]
fields = payload.get("fields") or {}
props = SETS[doc_type]["fields"]
wrapped = {"type": "object", "properties": props,
"additionalProperties": False}
errs += [f"fields:{e.message}"
for e in Draft7Validator(wrapped).iter_errors(fields)]
for name in SETS[doc_type]["required_fields"]:
if name not in fields:
errs.append(f"missing_required:{name}")
elif fields.get(name) is None and name != "jurisdiction":
errs.append(f"null_required:{name}")
return errs
def cross_checks(payload):
errs = []
fields = payload.get("fields") or {}
if payload.get("numeric_conflict"):
errs.append("numeric_conflict")
pairs = [("effective_date", "end_date"), ("effective_date", "expiration_date"),
("start_date", "end_date"), ("commencement_date", "expiration_date"),
("issue_date", "expiry_date"), ("filing_date", "hearing_date")]
for a, b in pairs:
if fields.get(a) and fields.get(b) and str(fields[b]) <= str(fields[a]):
errs.append(f"date_order:{a}>={b}")
for name, cell in (payload.get("evidence") or {}).items():
if not cell.get("quote") or not cell.get("page"):
errs.append(f"evidence_incomplete:{name}")
dt = payload.get("document_type")
if dt in JURISDICTION_REQUIRED and not payload.get("jurisdiction"):
errs.append("jurisdiction_missing")
return errs
def retry_prompt(doc_type):
required = ", ".join(SETS[doc_type]["required_fields"])
return ("RETRY\nReturn JSON only, wrapped as "
"<response><json>{...}</json></response>.\n"
f"Extract ONLY these fields: {required}\n"
"If a field is not on the page, return null. "
"Do not explain. Do not add fields.")
def extract(path, doc_type, schema_name):
system = "You extract structured fields from legal documents."
base_prompt = open("prompts/extraction.v1.txt").read().format(
document_type=doc_type, matter_hint="null",
target_schema=schema_name, page_count_hint="null")
prompt, raw = base_prompt, ""
for attempt in (1, 2):
msg = client.messages.create(
model="claude-sonnet-4-5", max_tokens=4096, system=system,
messages=[{"role": "user", "content": [
doc_block(path), {"type": "text", "text": prompt}]}])
raw = msg.content[0].text
try:
payload = parse_envelope(raw)
except (ValueError, json.JSONDecodeError):
prompt = retry_prompt(doc_type)
continue
errs = validate_payload(payload, doc_type) + cross_checks(payload)
floor = FLOORS[doc_type]
ok = (payload.get("confidence", 0) >= floor
and not errs and not payload.get("numeric_conflict"))
payload["meets_threshold"] = ok
if ok:
return {"result": "accepted", "payload": payload,
"attempt": attempt}
prompt = retry_prompt(doc_type)
reason = "two_failed_attempts" if errs else "below_confidence_floor"
return {"result": "escalate", "payload": payload, "errors": errs,
"raw_responses": [raw], "reason": reason}A payload can be fluent, complete and wrong on the fee. The validator is the only layer that does not care how convincing the JSON is.
Table 4A · Validation worksheet (fill this in)
| Rule | Where you implement it | Sample fail you caught | Action wired |
|---|---|---|---|
| Numeric consistency | cross_checks() in extract.py | 36605 vs "thirty-six thousand six hundred" | numeric_conflict, escalate |
| Date ordering | |||
| Currency present | |||
| Party evidence | |||
| Conflict list match | |||
| Required field null | |||
| Unknown key drop |
Section 5 · Escalating to a human when confidence drops
A low confidence score is still information. The model is telling you it is unsure. The correct response is a reviewer with the payload and the source page.
Table 5A · Confidence thresholds and retry wording
| Document type | Floor | Attempts | Retry emphasis | Escalate to |
|---|---|---|---|---|
| Engagement / retainer | 0.88 | 2 | fee_type, fee_amount, notice days | Paralegal queue, SLA 2h |
| NDA | 0.86 | 2 | parties, term, survival | Paralegal queue, SLA 4h |
| Service agreement | 0.86 | 2 | TCV, payment terms, liability cap | Ops queue, SLA 4h |
| Lease | 0.87 | 2 | address, dates, rent and escalation | Paralegal queue, SLA 4h |
| Court filing | 0.85 | 2 | case number, dates, caption | Attorney review, SLA 1h |
| Identity document | 0.92 | 1 | no second attempt on identity data | Compliance queue, same day |
Floors are starting points. Raise a floor when a document type starts producing accepted payloads that reviewers keep correcting. Lower it only with evidence that reviewers are re-reading correct output.
Escalation triggers
| Trigger | Why it matters | Route to |
|---|---|---|
| Confidence below the type floor | The model is telling you it is unsure | Reviewer queue, SLA timer on |
| Required field returned null | CRM write would land incomplete | Reviewer queue |
numeric_conflict: true | Money figures are not a place to guess | Reviewer queue, block downstream |
| Conflicting dates across sections | Notice periods and limitations depend on them | Reviewer queue |
| Jurisdiction absent or ambiguous | Drives routing, conflicts and court | Attorney review, never auto-assign |
| Scanned image quality poor or pages missing | Extraction cannot be trusted at all | Re-request document |
| Party name matches a conflict record | Cannot proceed to matter or deal creation | Conflict review, pipeline stopped |
| Two failed extraction attempts | Retrying a third time is superstition | Reviewer queue |
Escalation queue record
{
"queue_item_id": "ocr_esc_01J2K8Q2",
"document_id": "doc_9f3a_retn_2026_04",
"document_type": "engagement",
"correlation_id": "corr_ocr_01J2K8Q2",
"idempotency_key": "ocr:engagement:doc_9f3a:attempt2",
"reason": "below_confidence_floor",
"confidence": 0.79, "floor": 0.88, "attempt_count": 2,
"errors": ["null_required:fee_amount", "evidence_incomplete:client_name"],
"payload": { "...": "candidate object as returned" },
"raw_responses": ["attempt1", "attempt2"],
"source_pages": [1, 3, 4],
"sla_minutes": 120,
"assigned_role": "paralegal",
"status": "open"
}Reviewer checklist
- Open the source page beside the candidate payload. Work from the page itself.
- Confirm every required field against the evidence quote before changing anything.
- Correct by writing a corrected object. Never overwrite the model output. Both versions stay.
- Record why the model was wrong: layout, illegible, ambiguous clause, bad page split.
- If the same reason appears three times in a week, change the prompt or the schema. Do not keep correcting by hand.
- Close the item with a decision: accept as corrected, reject and re-request, or block for attorney sign-off.
One role per document type. SLA on every open item, escalating when it ages. Rejection is a valid outcome and routes to a named person for a decision. Corrections append. Bulk close is disabled.
If you want the pipeline built rather than assembled, that is the document processing agents service.
Section 6 · FAQ
Does this replace my current OCR tool?
It replaces the extraction and validation layer. If a tool already stores and versions documents, keep it. Point the tool at this call instead of its built-in template mapper.
Which model does the Python call use?
A vision-capable model API with zero data retention configured on your account. The sample uses an Anthropic messages call with a document block. Swap the client, keep the schema and the validator.
Why not ask the model harder?
Because a prompt cannot enforce a type. It can only request one. The schema rejects invented keys, the validator rejects bad values, and the queue catches the rest. Three layers, each one mechanical.
What about handwritten signatures and stamps?
Treat them as evidence that execution happened. Capture presence, date if legible, and the page. Do not ask the model to invent a signatory name from a flourish.
Can I add a seventh document type?
Yes. Copy a property set, list the required fields, pick a confidence floor from the closest neighbour, and version the schema. Table 3A is the worksheet for it.
What if the break is downstream of extraction?
Then it is a wiring problem. The free workflow diagnostic maps that path. This pack stays useful as the extraction layer underneath it.
Run the free workflow diagnostic, or read the case study, then decide. The diagnostic reads your current document path and tells you which layer is failing. The D'Alessio Law Group case study shows the document and intake pipeline end to end. When the pack is not enough, the same systems ship as client engagements through document processing agents.
Get the fillable workbook
Everything above is yours to read. The workbook adds the empty worksheets, so you fill them in on your own stack with your own owners, fields and failures. It opens in any browser, on any device, and it saves as you type.
When you want my read on your own stack, the document processing questionnaire takes ten minutes, and I reply with the first fix I would make and what it costs.