Cookbook: ID cards
Cookbook: IDs & passports
Section titled “Cookbook: IDs & passports”The job: turn a photo or scan of an identity document into a typed record. IDs have a hard constraint that invoices do not — the data is PII, so the redaction story matters from the first line.
The recipe
Section titled “The recipe”from scriva import Orchestrator, Workerfrom scriva.prompts import Prompt
worker = ( Worker.openai("gpt-4o") .prompt(Prompt.id_card) .score(method="rendering") .retry(times=3))
recipe = ( Orchestrator() .orient() .deskew() .crop(margins="auto") .split.boxes(detector="id-card") # photo, fields, MRZ band .classify() .recognize.by_kind({ "text": worker, "number": Worker.number(), "date": Worker.date(), "address": Worker.address(), "blank": Worker.skip(), }) .reconstruct.kv() .export.json("card.json"))
card = recipe("licence.jpg").to_dict()["fields"]print(card["full_name"], card["date_of_birth"], card["expiry_date"])For passports, swap the splitter and validate the MRZ in reconstruct:
passport_recipe = ( Orchestrator() .orient() .deskew() .split.boxes(detector="passport") # photo + visual zone + MRZ .recognize(worker) .reconstruct.passport(validate_mrz=True) # raises on MRZ disagreement .export.json("passport.json")).reconstruct.passport(validate_mrz=True) runs the standard ICAO
check-digit validation and raises RecognitionError rather than
silently returning a corrupt record.
MRZ-only path
Section titled “MRZ-only path”When you only need MRZ (faster, more deterministic, no VLM cost):
from scriva.schemas import Passport
passport = Passport.from_mrz("""P<USAMARTIN<<JONATHAN<<<<<<<<<<<<<<<<<<<<<<<<1234567894USA8501231M3001011<<<<<<<<<<<<<<04""")Passport.from_mrz runs the standard ICAO validation and raises
MRZParseError on malformed input. No network required.
For a hybrid path — VLM reads the MRZ from the image, parser validates it:
mrz_only = ( Orchestrator() .split.boxes(detector="mrz-band") # bottom 2 lines only .recognize(Worker.openai("gpt-4o").prompt(Prompt.mrz)) .reconstruct.document())text = mrz_only("passport.jpg").render()passport = Passport.from_mrz(text)split.boxes(detector="mrz-band") knows where the MRZ sits on a TD3
passport; the Worker just transcribes those two lines.
Redaction before the Worker call
Section titled “Redaction before the Worker call”ID images carry PII (face, signature, sometimes biometric markers). Strip what you do not need before shipping the crop to a third-party Worker:
from scriva.reliability import with_redaction, RedactionPolicy
worker = with_redaction( Worker.openai("gpt-4o").prompt(Prompt.id_card), policy=RedactionPolicy( mask_regions=["face", "signature"], # built-in region detectors patterns=[r"\b\d{9}\b"], # raw 9-digit IDs in hint text replace_with="<REDACTED>", scan_image=True, ), audit_log=Path("audit/id-redactions.jsonl"),)with_redaction(...) wraps a Worker; redaction runs before the
crop is sent upstream, so the third party never sees the face.
The audit log entry includes the source path, the redacted region IDs, and an ISO timestamp — enough to demonstrate compliance after the fact without retaining the redacted content itself.
Address normalisation
Section titled “Address normalisation”Worker.address() returns a structured address; the reconstruct step
slots its fields into the typed record:
card["address"]["line_1"] # "123 Main St"card["address"]["locality"] # "Springfield"card["address"]["region"] # "IL"card["address"]["postal_code"] # "62704"card["address"]["country"] # "US"To bypass normalisation (e.g. when legal-name parity is required),
substitute Worker.text() for the address kind:
recipe = recipe.recognize.by_kind({"address": Worker.text(), ...})Expiry / age gates
Section titled “Expiry / age gates”For age-verification flows, you usually do not need the whole document back — just a boolean and a confidence score:
from datetime import date, timedelta
result = recipe("licence.jpg")card = result.to_dict()["fields"]is_18_plus = card["date_of_birth"] <= date.today() - timedelta(days=365 * 18)confidence = result.confidence_for("date_of_birth")The pattern: extract, then derive. The recipe does not encode the gate itself, because the gate is your business logic, not the document’s.
Production notes
Section titled “Production notes”Don’t log raw PII
Section titled “Don’t log raw PII”import scrivascriva.config.set_log_redactor(scriva.reliability.RedactionPolicy.identity())Once set, every log record (including event payloads) runs through
the redactor. The Worker still sees the unredacted crop — this only
sanitises observability output. Pair with with_redaction(...) when
you also want to keep the VLM provider from seeing the data.
Cache cautiously
Section titled “Cache cautiously”worker = Worker.openai("gpt-4o").prompt(Prompt.id_card) # no .cache(...)For ID flows, disable caching unless you have reasoned about it explicitly. A semantic cache hit on a photo of a different person’s licence that looks similar enough is the classic PII leak — much better to pay the VLM cost than to risk crossing identities.
If you must cache (e.g. retry-safety inside a single submission), scope the cache by tenant + session:
from scriva.cache import Cacheworker = worker.cache(Cache.fs(f".cache/{tenant}/{session_id}", ttl_s=60 * 10))Retention
Section titled “Retention”ID extraction is the textbook case for “extract, validate, then delete”:
result = recipe(source)publish_to_kyc(result.to_dict()["fields"])source.unlink() # delete the source imageSchedule a daily sweep of any sidecar .json outputs that hold raw
PII.
Refuse on low confidence
Section titled “Refuse on low confidence”A wrong DOB on a KYC flow is much worse than a re-prompt to the user:
result = recipe("licence.jpg")fields = ("full_name", "date_of_birth", "id_number")if min(result.confidence_for(f) or 0 for f in fields) < 0.9: request_better_photo()else: accept(result.to_dict()["fields"])For the same gate inside the recipe, use .review.hitl(...):
recipe = recipe.review.hitl( when=lambda r: r.region.attrs.get("field") in {"full_name", "date_of_birth"} and (r.confidence or 0) < 0.9, sidecar="kyc-review.json",)What to read next
Section titled “What to read next”- Worker — engines, decorators, composition.
- Reliability › PII redaction — the canonical redaction surface.
- Evaluation — measuring DOB / number accuracy specifically.