Extract
Pull structured fields out of documents — invoices, receipts, and your own schemas.
Extract takes a document and returns typed fields rather than an answer. It is a separate service with its own error envelope and its own key requirements: integration keys only, workspace-scoped.
The flow
POST /v1/extract/upload-urlsreserves a job and returns a presigned S3 POST.- You POST the file straight to S3, bypassing the API's body limit.
POST /v1/extract/jobs/{jobId}/confirmverifies the object and queues the job.GET /v1/extract/jobs/{jobId}polls until the status is terminal.
Endpoints
/extract/health Extract service liveness. Returns checks.extract ok when EXTRACT_* env vars are set. Separate from GET /health on the main API Lambda.
Auth Public — no credential
Path parameters
None.
Query parameters
None.Serves a fixed document and takes no input at all.
Body parameters
None.GET requests carry no body.
Response
200 Success
{
"status": "ok",
"version": "0.1.0",
"checks": { "extract": "ok" }
} Status codes
| Status | Meaning |
|---|---|
| 200 | Success. |
| 429 | Rate limited. See rate limits. |
curl
curl -X GET 'https://api.dev.oprag.ai/extract/health' \
-H 'X-Oprag-Key: sk_live_...' /v1/extract/upload-urls Reserve an extraction job and get a presigned S3 POST for the file.
Auth Integration key
Before you call it
- Uploading straight to S3 is what keeps large files clear of API Gateway's request body limit.
- Supported formats: PDF, PNG, JPEG, GIF, and WebP (max 10 MB per file). PDFs may be up to 100 MB; the ceiling is enforced by
content-length-rangeon the presigned POST and re-checked on confirm. - POST the file to
uploadUrlasmultipart/form-data— every entry ofuploadFieldsfirst, then the file — and then callPOST /v1/extract/jobs/{jobId}/confirm. The URL is valid forexpiresInseconds. - The response is a
CreateExtractUploadUrlResponse:jobId,status: "upload_pending",uploadUrl,uploadFields,expiresIn, andpollUrl. - Integration keys only, scoped to the workspace. Each call counts toward
EXTRACT_DAILY_JOB_LIMIT. - Available in dev only until
enable_extractis turned on in staging and production.
Path parameters
None.
Query parameters
None.Everything it needs is in the request body.
Body parameters
| Name | Type | Required | Description |
|---|---|---|---|
filename | string | Required | Original file name. |
contentType | string | Optional | MIME type. Defaults to application/octet-stream; the extension is used when it disagrees. |
schemaId | string | Optional | A built-in id, a custom_ id, or "auto" to classify the document. |
allowedSchemas | string[] | Optional | Restricts what "auto" may classify the document as. |
clientRef | string | Optional | Your own reference, echoed back on the job. |
Request
{
"filename": "annual-contract.pdf",
"contentType": "application/pdf",
"schemaId": "contract",
"clientRef": "erp-sync-8842"
} Response
200 Success
{
"jobId": "ext_a1b2c3d4e5f6789012345678abcdef01",
"status": "upload_pending",
"uploadUrl": "https://extract-inbox.s3.amazonaws.com/",
"uploadFields": {
"key": "inbox/co_abc/ext_a1b2…/annual-contract.pdf",
"Policy": "…",
"X-Amz-Signature": "…",
"Content-Type": "application/pdf"
},
"expiresIn": 900,
"pollUrl": "/v1/extract/jobs/ext_a1b2c3d4e5f6789012345678abcdef01"
} Status codes
curl
curl -X POST 'https://api.dev.oprag.ai/v1/extract/upload-urls' \
-H 'X-Oprag-Key: sk_live_...' \
-H 'Content-Type: application/json' \
-d '{"filename": "annual-contract.pdf","contentType": "application/pdf","schemaId": "contract","clientRef": "erp-sync-8842"}' /v1/extract/jobs Deprecated for file submission — always returns 400 and points at the presigned upload flow.
Auth Integration key
Before you call it
- Use
POST /v1/extract/upload-urls, POST the file to S3 withuploadFields, thenPOST /v1/extract/jobs/{jobId}/confirm.GET /v1/extract/jobs/{jobId}polls status from there. - Integration keys only, scoped to the workspace. Available in dev only until
enable_extractis turned on in staging and production.
Path parameters
None.
Query parameters
None.Everything it needs is in the request body.
Body parameters
Defined by the caller.Deprecated for file submission — always returns 400 pointing at the upload-url flow.
Request
# Direct multipart/fileUrl submit removed — use presigned upload:
curl -X POST "$API_URL/v1/extract/upload-urls" \
-H "X-Oprag-Key: sk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"filename":"invoice.pdf","contentType":"application/pdf","schemaId":"invoice"}' Response
200 Success
{
"error": "Use POST /v1/extract/upload-urls, POST the file to S3 with uploadFields, then POST /v1/extract/jobs/{jobId}/confirm",
"code": "VALIDATION_ERROR"
} Status codes
curl
curl -X POST 'https://api.dev.oprag.ai/v1/extract/jobs' \
-H 'X-Oprag-Key: sk_live_...' \
-H 'Content-Type: application/json' \
-d '# Direct multipart/fileUrl submit removed — use presigned upload:curl -X POST "$API_URL/v1/extract/upload-urls" \-H "X-Oprag-Key: sk_live_YOUR_KEY" \-H "Content-Type: application/json" \-d '{"filename":"invoice.pdf","contentType":"application/pdf","schemaId":"invoice"}'' /v1/extract/jobs/{jobId}/confirm Confirm the uploaded object and queue the job for extraction.
Auth Integration key
Before you call it
- Call it after the file has been POSTed to
uploadUrl. The object is checked for existence, non-emptiness, size (100 MB for PDFs, 10 MB for images), and a supported format by both MIME type and magic bytes. - On success the job moves from
upload_pendingtoqueuedand{ jobId, companyId, schemaId }is enqueued for the worker. - The response is a
ConfirmExtractJobResponse:jobId,status: "queued", andpollUrl. - Confirming a job that is already
queuedreturns the same 202 again, so retrying is safe.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
jobId | string | Required | Extract job id. |
Query parameters
None.Acts on the resource named in the path; there is nothing to choose.
Body parameters
None.Confirms the object already uploaded for this job id.
Request
# No body — jobId in path only
curl -X POST "$API_URL/v1/extract/jobs/ext_a1b2c3d4e5f6789012345678abcdef01/confirm" \
-H "X-Oprag-Key: sk_live_YOUR_KEY" Response
202 Success
{
"jobId": "ext_a1b2c3d4e5f6789012345678abcdef01",
"status": "queued",
"pollUrl": "/v1/extract/jobs/ext_a1b2c3d4e5f6789012345678abcdef01"
} Status codes
| Status | Meaning |
|---|---|
| 202 | Success. |
| 400 | The object is missing, empty, or oversize; unsupported_format when its MIME type or magic bytes are wrong. |
| 401 | Missing, revoked, or wrong-environment key. |
| 403 | Origin or IP not allowed for this project. |
| 404 | No such job in this workspace. A malformed job id is also a 404, from the router. |
| 409 | The job is already processing or terminal — but not merely queued. |
| 429 | Rate limited. See rate limits. |
| 503 | The enqueue failed. The body still carries jobId and pollUrl, with status: "failed". |
curl
curl -X POST 'https://api.dev.oprag.ai/v1/extract/jobs/{jobId}/confirm' \
-H 'X-Oprag-Key: sk_live_...' \
-H 'Content-Type: application/json' \
-d '# No body — jobId in path onlycurl -X POST "$API_URL/v1/extract/jobs/ext_a1b2c3d4e5f6789012345678abcdef01/confirm" \-H "X-Oprag-Key: sk_live_YOUR_KEY"' /v1/extract/jobs/{jobId} Poll an extraction job until it reaches completed or failed.
Auth Integration key
Before you call it
statusrunsupload_pending→queued→processing→completedorfailed. A failed job carrieserror.code.- The shape of
datafollowsschemaId(ordetectedSchemaId): one ofInvoiceExtractData,ReceiptExtractData,PurchaseOrderExtractData,CreditNoteExtractData,DeliveryNoteExtractData,BankStatementExtractData,PayslipExtractData,UtilityBillExtractData,TicketExtractData,TaxFormExtractData,ContractExtractData,GstInvoiceExtractData, orIdDocumentExtractData— alongsideconfidence,provenance, andbilling. - Auto-detected jobs set
schemaIdto the detected type and addclassificationMode,requestedSchemaId,detectedSchemaId, andclassification { schemaId, confidence, reason? }, wherereasonappears only when the model supplies one. - Scanned documents that go through OCR add
processingTier: "ocr",documentConfidence,validationIssues,provenance.bboxfrom Textract geometry, and sometimesocrQualityWarning. - Large PDFs — 21 pages or more, over 10 MB, or a bank statement of 4 pages or more — run a chunked pipeline in 20-page chunks. While
processingthey addjobTier: "large",phase(preprocess,extracting, ormerging),totalPages, andprogress { chunksCompleted, chunksTotal }. provenancemay carry anexcerpt, for any schema.- Typical latency on dev is 5–15 s for an explicit schema and 10–30 s for auto-detect. Chunked PDFs take minutes — poll and watch
progress.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
jobId | string | Required | Extract job id. |
Query parameters
None.Reads the resource named in the path; there is nothing else to select.
Body parameters
None.GET requests carry no body.
Responses
200 Success
{
"jobId": "ext_a1b2c3d4e5f6789012345678abcdef01",
"status": "completed",
"schemaId": "invoice",
"extractedAt": "2026-07-11T21:20:46.573Z",
"data": {
"vendorName": "Contoso Ltd.",
"invoiceNumber": "INV-100",
"invoiceDate": "11/15/2019",
"dueDate": "12/15/2019",
"subtotal": 100,
"taxAmount": 10,
"totalAmount": 110,
"lineItems": [{ "description": "Consulting Services", "quantity": 2, "unitPrice": 30, "amount": 60 }]
},
"confidence": { "invoiceNumber": 1, "totalAmount": 1 },
"documentConfidence": 0.95,
"processingTier": "digital",
"billing": { "pagesProcessed": 1, "billable": true }
}
// Large PDF while processing (optional progress fields):
// "status": "processing", "jobTier": "large", "phase": "extracting",
// "totalPages": 80, "progress": { "chunksCompleted": 2, "chunksTotal": 4 }
// Auto-detect completed (requestedSchemaId was "auto"):
// "schemaId": "invoice", "classificationMode": "auto", "requestedSchemaId": "auto",
// "detectedSchemaId": "invoice", "classification": { "schemaId": "invoice", "confidence": 0.91 } 200 Another example
{
"jobId": "ext_656c03b65e61454e5bc562567a0f98b3",
"status": "completed",
"schemaId": "receipt",
"clientRef": "invoicehome-receipt",
"extractedAt": "2026-07-11T22:31:16.976Z",
"data": {
"merchantName": "East Repair Inc.",
"merchantAddress": "1912 Harvest Lane New York, NY 12210",
"receiptNumber": "US-001",
"receiptDate": "11/02/2019",
"subtotal": 145,
"taxAmount": 9.06,
"totalAmount": 154.06,
"currency": "$",
"lineItems": [
{ "description": "Front and rear brake cables", "quantity": 1, "unitPrice": 100, "amount": 100 },
{ "description": "New set of pedal arms", "quantity": 2, "unitPrice": 15, "amount": 30 },
{ "description": "Labor 3hrs", "quantity": 3, "unitPrice": 5, "amount": 15 }
]
},
"confidence": { "merchantName": 1, "receiptNumber": 1, "totalAmount": 1, "lineItems": 1 },
"provenance": [{ "field": "merchantName", "page": 1, "excerpt": "East Repair Inc." }],
"billing": { "pagesProcessed": 1, "billable": true }
} Status codes
curl
curl -X GET 'https://api.dev.oprag.ai/v1/extract/jobs/{jobId}' \
-H 'X-Oprag-Key: sk_live_...' /v1/extract/schemas List the schemas this workspace can extract with.
Auth Integration key
Before you call it
- Built-in schemas come first, then the workspace's custom ones sorted by
schemaId.
Path parameters
None.
Query parameters
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
includeBuiltin | boolean | Optional | true | Send the literal string false to list custom schemas only. Every other value keeps the built-in catalog. |
status | "active" | "archived" | "all" | Optional | active | Which custom schemas to include. An unrecognised value falls back to active rather than erroring. |
Body parameters
None.GET requests carry no body.
Response
200 Success
{
"schemas": [
{ "schemaId": "invoice", "displayName": "Invoice", "source": "builtin", "status": "active" },
{ "schemaId": "receipt", "displayName": "Receipt", "source": "builtin", "status": "active" },
{
"schemaId": "custom_purchase_order",
"displayName": "Purchase order",
"source": "custom",
"status": "active",
"version": 2,
"fieldCount": 7,
"createdAt": "2026-06-01T10:00:00.000Z",
"updatedAt": "2026-06-14T09:12:00.000Z"
}
]
} Status codes
| Status | Meaning |
|---|---|
| 200 | Success. |
| 401 | Missing, revoked, or wrong-environment key. |
| 403 | Origin or IP not allowed for this project. |
| 429 | Rate limited. See rate limits. |
curl
curl -X GET 'https://api.dev.oprag.ai/v1/extract/schemas' \
-H 'X-Oprag-Key: sk_live_...' /v1/extract/schemas Define a custom extraction schema for your own document type.
Auth Integration key
Path parameters
None.
Query parameters
None.Everything it needs is in the request body.
Body parameters
| Name | Type | Required | Description |
|---|---|---|---|
schemaId | string | Required | Must be custom_-prefixed. Built-in ids are rejected with 409. |
displayName | string | Required | Shown in the schema list. Up to 128 characters. |
documentDescription | string | Required | What this kind of document is. Up to 512 characters. |
extractionHints | string | Optional | Document quirks worth telling the extractor. Up to 512 characters. |
fields | any[] | Required | Field definitions: key, type, and description each. |
Request
{
"schemaId": "custom_purchase_order",
"displayName": "Purchase order",
"documentDescription": "A supplier purchase order with line items and totals.",
"extractionHints": "Totals are on the last page. Currency is always the document currency.",
"fields": [
{ "key": "poNumber", "type": "string", "description": "Purchase order number" },
{ "key": "orderDate", "type": "date", "description": "Date the order was raised" },
{ "key": "total", "type": "number", "description": "Order total including tax" }
]
} Response
200 Success
{
"schemaId": "custom_purchase_order",
"displayName": "Purchase order",
"source": "custom",
"status": "active",
"version": 1,
"documentDescription": "A supplier purchase order with line items and totals.",
"fields": [
{ "key": "poNumber", "type": "string", "description": "Purchase order number" },
{ "key": "orderDate", "type": "date", "description": "Date the order was raised" },
{ "key": "total", "type": "number", "description": "Order total including tax" }
],
"createdAt": "2026-06-01T10:00:00.000Z",
"updatedAt": "2026-06-01T10:00:00.000Z"
} Status codes
| Status | Meaning |
|---|---|
| 201 | Created. |
| 400 | The request body failed validation. |
| 401 | Missing, revoked, or wrong-environment key. |
| 403 | Origin or IP not allowed for this project. |
| 409 | The schemaId is a built-in name, or already exists. |
| 429 | The workspace is at its active custom-schema limit, or is creating schemas too quickly. |
curl
curl -X POST 'https://api.dev.oprag.ai/v1/extract/schemas' \
-H 'X-Oprag-Key: sk_live_...' \
-H 'Content-Type: application/json' \
-d '{"schemaId": "custom_purchase_order","displayName": "Purchase order","documentDescription": "A supplier purchase order with line items and totals.","extractionHints": "Totals are on the last page. Currency is always the document currency.","fields": [{ "key": "poNumber", "type": "string", "description": "Purchase order number" },{ "key": "orderDate", "type": "date", "description": "Date the order was raised" },{ "key": "total", "type": "number", "description": "Order total including tax" }]}' /v1/extract/schemas/{schemaId} Fetch one extraction schema, built-in or custom.
Auth Integration key
Before you call it
- A built-in id returns a short descriptor with a
documentationUrl; a custom id returns the full field definitions.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
schemaId | string | Required | Extraction schema id — a built-in name or a custom_ id. |
Query parameters
None.Reads the resource named in the path; there is nothing else to select.
Body parameters
None.GET requests carry no body.
Response
200 Success
{
"schemaId": "invoice",
"displayName": "Invoice",
"source": "builtin",
"status": "active",
"documentationUrl": "/docs/api/extract/schemas/"
} Status codes
curl
curl -X GET 'https://api.dev.oprag.ai/v1/extract/schemas/{schemaId}' \
-H 'X-Oprag-Key: sk_live_...' /v1/extract/schemas/{schemaId} Update a custom schema's name, description, hints, or fields.
Auth Integration key
Before you call it
- Every accepted change bumps
version.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
schemaId | string | Required | Extraction schema id — a built-in name or a custom_ id. |
Query parameters
None.Everything it needs is in the request body.
Body parameters
| Name | Type | Required | Description |
|---|---|---|---|
displayName | string | Optional | See the request example below. |
documentDescription | string | Optional | See the request example below. |
extractionHints | string | Optional | See the request example below. |
fields | any[] | Optional | Replaces the whole field list and bumps version. |
Request
{
"displayName": "Supplier purchase order",
"extractionHints": "Totals are on the last page; ignore the delivery note."
} Response
200 Success
{
"schemaId": "custom_purchase_order",
"displayName": "Supplier purchase order",
"source": "custom",
"status": "active",
"version": 2,
"updatedAt": "2026-06-14T09:12:00.000Z"
} Status codes
| Status | Meaning |
|---|---|
| 200 | Success. |
| 400 | The request body failed validation. |
| 401 | Missing, revoked, or wrong-environment key. |
| 403 | Origin or IP not allowed for this project. |
| 404 | No such resource in this workspace. |
| 409 | The id is built-in, or the schema is archived. Create a new schemaId instead. |
| 429 | Rate limited. See rate limits. |
curl
curl -X PATCH 'https://api.dev.oprag.ai/v1/extract/schemas/{schemaId}' \
-H 'X-Oprag-Key: sk_live_...' \
-H 'Content-Type: application/json' \
-d '{"displayName": "Supplier purchase order","extractionHints": "Totals are on the last page; ignore the delivery note."}' /v1/extract/schemas/{schemaId} Archive a custom schema so it can no longer be used for new jobs.
Auth Integration key
Before you call it
- Archiving is not a hard delete: jobs already extracted with the schema keep their results, and the
schemaIdcan never be reused.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
schemaId | string | Required | Extraction schema id — a built-in name or a custom_ id. |
Query parameters
None.Acts on the resource named in the path; there is nothing to choose.
Body parameters
None.DELETE requests carry no body.
Response
200 Success
{
"schemaId": "custom_purchase_order",
"status": "archived",
"archivedAt": "2026-06-20T08:30:00.000Z"
} Status codes
curl
curl -X DELETE 'https://api.dev.oprag.ai/v1/extract/schemas/{schemaId}' \
-H 'X-Oprag-Key: sk_live_...' Ready to ship?
Get started free