API
One endpoint to send a file, one to get the result. Everything is JSON. Base URL: https://titleread.com
Authentication
Every request carries your API key: Authorization: Bearer ts_live_…. Keys are created in the dashboard and can be revoked there.
Send a document
curl -X POST https://titleread.com/v1/documents \
-H "Authorization: Bearer $TITLE_API_KEY" \
-F "file=@packet.pdf" \
-F 'options={"context":{"lot_number":"45123456"},"metadata":{"order":"A-1001"},"webhook_url":"https://your-app/hooks"}'
Or send the bytes directly with Content-Type: application/pdf and an X-File-Name header. PDF, JPEG, PNG, WebP; up to 50 MB; any number of pages and documents in one file.
Response 202: {"id":"…","status":"queued"}. One credit is charged per vehicle — a group of pages sharing a VIN that has a title or a bill of sale — when the read completes. Pages without a title or bill of sale are free.
options, all optional: context.lot_number (an auction lot number printed on a sticker is then never mistaken for the title number), context.vin, context.state; metadata (anything, returned with the result and in webhooks); webhook_url (overrides the account setting for this document).
Get the result
curl https://titleread.com/v1/documents/ID -H "Authorization: Bearer $TITLE_API_KEY"
status moves queued → processing → completed or failed. A completed result has:
result.documents[]— one per logical document found:pages,type(title, bill_of_sale, other…),state,subtype(clean, salvage, junk, rebuilt, bos_only; copart, iaai),fields(the raw read: every value withscoreConfidenceandgeometry— its box on the page, in a 1000×1000 frame), andnormalized(the cleaned, typed form: title number, brands, VIN, owner, lienholders, transfers, reassignments, odometer with its brand).result.groups[]— documents grouped by vehicle:vin,merged(title fields plus bill-of-sale fields in one record),conflicts(where the two disagree),ownership(last U.S. owner, seller candidate, how it was decided).result.usage.durationMs— how long the read took.
A document that could not be read has failure.reason: CORRUPTED_DOCUMENT (rescan it), NO_PAGES_DETECTED, PROVIDER_ERROR. A page the model answered badly has extraction.failure.reason such as TRUNCATED or BLOCKED, and rawText to inspect.
Webhooks
Set a URL in the dashboard (or per document). We POST {"id":"evt_…","event":"document.completed","data":{"id":"…","status":"completed","documents":2,"metadata":{…}}} and retry with backoff for up to eighteen hours. The URL must be public and use HTTPS. The X-Title-Signature header is t=<unix>,v1=<hex> where v1 = HMAC-SHA256(secret, t + "." + body); reject anything older than five minutes.
Other calls
GET /v1/documents?limit=50&before=… | your documents, newest first, without results |
GET /v1/documents/ID/pages/N.png | a rendered page, for your own review screens |
POST /v1/documents/ID/corrections | {"documentIndex":0,"fieldPath":"titleNumber","oldValue":"…","newValue":"…"} — what a person changed; it improves the service |
POST /v1/documents/ID/reprocess | read it again (charged again) |
DELETE /v1/documents/ID | remove the result and the file now, ahead of retention |
GET /v1/usage, GET /v1/me | uploads, vehicles read, credits |
Limits and errors
120 requests per minute per key (429). 401 bad key, 402 no credits, 413 too large, 415 wrong type, 404 not yours or gone. Errors are {"error":"code","message":"…"}.