# Parse by Conversion Tools - Complete API Reference > Parse is a document data extraction API. Send a PDF or an image, receive structured JSON, and optionally export that JSON as a CSV or Excel spreadsheet. This document is self-contained: everything needed to write a correct client is here. - Website: https://parse.conversiontools.io - Human documentation: https://parse.conversiontools.io/docs - API base URL: `https://api-parse.conversiontools.io` - Concise index: https://parse.conversiontools.io/llms.txt --- ## 1. Read this first: the async contract **Extraction is asynchronous by default.** There is exactly one correct client shape: 1. Start an extraction. The API replies `202 Accepted` with an extraction id and `"status": "processing"`. 2. Poll `GET /v1/extractions/{id}` on an interval. Two seconds is a good default. 3. Stop when `status` is `completed` or `failed`. Those are the only terminal values. 4. On `completed`, read `data`. On `failed`, read `error`. Rules a generated client must follow: - **Branch on the `status` field in the JSON body.** Never branch on the HTTP status code alone, never on how long the request took, and never on whether `data` happens to be present. - **`202` means "accepted and running".** It is not an error and it is not a result. A client that treats 202 as a failure, or that reads `data` from the 202 body, is broken. - **`data` is `null` while an extraction is still processing.** `status: "processing"` with `data: null` is the normal in-progress state. - **A failed extraction is still HTTP 200.** `GET /v1/extractions/{id}` returns 200 with `"status": "failed"` and a human-readable `error` string. Handle it explicitly. - **Bound the poll loop.** Use a maximum number of attempts or a wall-clock deadline, and surface a timeout instead of polling forever. - **Do not re-submit the document because a poll feels slow.** Re-sending the same document while the first run is still going returns the *same* extraction id (see section 8), so a retry loop that re-uploads only wastes bandwidth. If you would rather not poll at all, pass `webhook_url` when you start the extraction (see section 7) and let Parse call you when the extraction reaches a terminal state. `wait` (section 5) is a convenience for small documents only: it holds the request open for up to 120 seconds and returns the result inline if it finishes in time. It never changes the contract - the envelope always carries `status`, and if the document outlives the window you get the same 202 and the same id. --- ## 2. Authentication Every request requires a Bearer token: ``` Authorization: Bearer pk_live_0123456789abcdef... ``` API keys are created in the dashboard at https://parse.conversiontools.io/dashboard/api-keys. A key looks like `pk_live_` followed by 48 hexadecimal characters. Keys can be revoked individually at any time. Store keys in environment variables. Never commit them and never ship them in client-side code. A missing or malformed `Authorization` header, or an unknown/revoked key, returns `401` (see section 10). ## 3. Identifiers `file_id`, the extraction `id`, and `schema_id` are opaque 32-character lowercase hexadecimal strings, for example `3f1a9c7e5b2d4e6f8a0b1c2d3e4f5a6b`. They carry no prefix and no structure. Do not parse them, do not assume a format, and do not generate them yourself. ## 4. Endpoint map | Method | Path | Purpose | | --- | --- | --- | | POST | `/v1/extract` | Start an extraction (multipart upload, JSON with a `file_id`, or a `file_url` in either form) | | POST | `/v1/upload` | Upload a document and receive a `file_id` | | GET | `/v1/extractions/{id}` | Poll an extraction and read its result | | DELETE | `/v1/extractions/{id}` | Delete an extraction and all of its stored data | | POST | `/v1/extractions/{id}/export` | Start a CSV or Excel export of a completed extraction | | GET | `/v1/extractions/{id}/export?format=` | Poll the export, then download the file | | GET | `/v1/usage` | Current month's page consumption and plan limits | | POST | `/v1/schemas` | Create a reusable extraction schema | | GET | `/v1/schemas` | List schemas | | GET | `/v1/schemas/{id}` | Retrieve one schema | | PUT | `/v1/schemas/{id}` | Replace a schema | | DELETE | `/v1/schemas/{id}` | Delete a schema | Every endpoint is also reachable under a `/v1/parse/...` prefix (`/v1/parse/extract`, `/v1/parse/extractions/{id}`, and so on). The two forms are identical. Use the `/v1/...` form documented here. --- ## 5. POST /v1/extract - start an extraction Two request shapes are accepted, chosen by `Content-Type`. ### 5A. multipart/form-data - upload and extract in one call | Field | Type | Required | Description | | --- | --- | --- | --- | | `file` | file | one of `file` / `file_url` | The document to extract from (PDF or image, see section 12) | | `file_url` | string | one of `file` / `file_url` | Public http(s) address of the document, instead of uploading it (section 5D) | | `filename` | string | no | Name to store a `file_url` document under. Defaults to the name the server gives, or the last part of the URL | | `schema_id` | string | no | Id of a saved schema. Controls which fields are extracted | | `fields` | string | no | Inline schema: a JSON-encoded **array** of field definitions (section 9). Ignored when `schema_id` resolves to a saved schema | | `wait` | number | no | Seconds to hold the request open. Default `0` (return immediately), maximum `120` | | `webhook_url` | string | no | Public http(s) URL notified when the extraction reaches a terminal state (section 7) | | `no_cache` | string | no | Send `1` to force a fresh extraction. Omit the field entirely otherwise | | `output_format` | string | no | `csv` or `xlsx`. Starts a spreadsheet export automatically once the extraction completes (section 6) | | `password` | string | no | Document password for an encrypted PDF (section 12) | Caution with `no_cache` in multipart requests: any non-empty value counts as "on", including the string `"0"`. If you want the default caching behaviour, do not send the field at all. **Default response, HTTP 202:** ```json { "success": true, "id": "3f1a9c7e5b2d4e6f8a0b1c2d3e4f5a6b", "status": "processing", "message": "Extraction started. Poll GET /v1/extractions/:id for the result, or use webhook_url." } ``` **With `wait`, when the document finishes inside the window, HTTP 200:** ```json { "success": true, "id": "3f1a9c7e5b2d4e6f8a0b1c2d3e4f5a6b", "filename": "invoice.pdf", "status": "completed", "data": { "vendor_name": "Acme Corp", "invoice_number": "INV-2024-001", "date": "2024-01-15", "total": 1650.0 }, "pages_used": 1, "validation": null, "error": null, "created_at": "2026-07-26T10:00:00.000Z", "completed_at": "2026-07-26T10:00:41.000Z" } ``` With `wait`, a `failed` extraction also arrives as HTTP 200 with `"status": "failed"` and an `error` string. Check `status`. If the wait window expires first, the response is a 202 identical to the default one except for the message: `"Extraction is still processing. Poll GET /v1/extractions/:id for the result."` Example: ```bash curl -X POST https://api-parse.conversiontools.io/v1/extract \ -H "Authorization: Bearer $PARSE_API_KEY" \ -F "file=@invoice.pdf" \ -F "wait=60" ``` ### 5B. application/json - extract from an already-uploaded file, or from a URL | Field | Type | Required | Description | | --- | --- | --- | --- | | `file_id` | string | one of `file_id` / `file_url` | Id returned by `POST /v1/upload` | | `file_url` | string | one of `file_id` / `file_url` | Public http(s) address of the document (section 5D) | | `filename` | string | no | Name to store a `file_url` document under | | `schema_id` | string | no | Id of a saved schema | | `fields` | array | no | Inline schema: an array of field definitions (section 9). Ignored when `schema_id` resolves | | `webhook_url` | string | no | Public http(s) URL notified on a terminal state (section 7) | | `no_cache` | boolean | no | `true` forces a fresh extraction | | `output_format` | string | no | `csv` or `xlsx`, starts a spreadsheet export on completion | | `password` | string | no | Document password for an encrypted PDF (section 12) | **`wait` is not supported in the JSON form.** It is read only from multipart form fields. A JSON request always returns immediately, so this flow must always poll or use a webhook. **Response, HTTP 202:** ```json { "success": true, "id": "3f1a9c7e5b2d4e6f8a0b1c2d3e4f5a6b", "status": "processing" } ``` Example: ```bash curl -X POST https://api-parse.conversiontools.io/v1/extract \ -H "Authorization: Bearer $PARSE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"file_id": "8c1d...", "schema_id": "b47a...", "webhook_url": "https://example.com/hooks/parse"}' ``` ### 5C. Cached and in-flight responses Both request shapes can short-circuit (see section 8): - **Stored result reused, HTTP 200**, with `"status": "completed"`, `"cached": true`, and the data inline. No pages are consumed. - **Identical document already running, HTTP 202**, with the *same* extraction id as the run already in progress and `"message": "This document is already being extracted. Poll GET /v1/extractions/:id for the result."` A client that branches on `status` handles all of these without special cases. A `webhook_url` sent with either request is still called (section 7): at once for a stored result, and when the running extraction finishes for a request attached to it. ### 5D. Extracting from a URL (`file_url`) Instead of uploading the bytes, send the document's address as `file_url`, in either request shape. Parse downloads it and then treats it exactly like an upload: the same size limit, the same page accounting, the same caching. This suits automation tools that hold a file only as a link. ```bash curl -X POST https://api-parse.conversiontools.io/v1/extract \ -H "Authorization: Bearer $PARSE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"file_url": "https://example.com/files/invoice.pdf", "filename": "invoice.pdf", "schema_id": "b47a...", "webhook_url": "https://example.com/hooks/parse"}' ``` Rules: - Send exactly one of `file` / `file_id` and `file_url`. Both, or neither, is a `400`. - The URL must be a public `http` or `https` address that answers without login. Loopback, private, link-local and other internal addresses are refused, including when a redirect leads to one. Up to 5 redirects are followed. - The download is part of the request: it must complete within 20 seconds, and the file may be at most 100 MB. A larger file is a `413`. - An empty file, an HTTP error from the link, a timeout or an unreachable host is a `400` whose `error` says what went wrong, with `"param": "file_url"` and a `code` (section 10). Nothing is created and no pages are consumed. - The link must lead to the file itself. A link that answers with a web page (a preview, share or sign-in page) is a `400` with `"code": "FILE_URL_WEB_PAGE"`. In an automation tool this usually means the mapped field holds a link to the file rather than the file: map the field that holds the file itself (often named File or Attachment). - The document type is detected from the downloaded bytes, so the link does not need a file extension. --- ## 6. Uploading, polling, deleting, exporting ### POST /v1/upload `multipart/form-data` with a single `file` field. No page cost. ```bash curl -X POST https://api-parse.conversiontools.io/v1/upload \ -H "Authorization: Bearer $PARSE_API_KEY" \ -F "file=@invoice.pdf" ``` ```json { "success": true, "file_id": "8c1d5e2a7b394c6d8e0f1a2b3c4d5e6f", "filename": "invoice.pdf", "size": 52480 } ``` One uploaded file can back several extractions, for example with different schemas. ### GET /v1/extractions/{id} The poll endpoint. Returns HTTP 200 for every extraction state. ```bash curl https://api-parse.conversiontools.io/v1/extractions/3f1a9c7e5b2d4e6f8a0b1c2d3e4f5a6b \ -H "Authorization: Bearer $PARSE_API_KEY" ``` ```json { "success": true, "id": "3f1a9c7e5b2d4e6f8a0b1c2d3e4f5a6b", "filename": "invoice.pdf", "status": "completed", "data": { "vendor_name": "Acme Corp", "invoice_number": "INV-2024-001", "date": "2024-01-15", "line_items": [ { "description": "Consulting Services", "quantity": 10, "unit_price": 150.0, "amount": 1500.0 } ], "subtotal": 1500.0, "tax": 150.0, "total": 1650.0, "currency": "USD" }, "data_expired": false, "pages_used": 1, "validation": { "status": "passed", "warnings": [], "coercions": [] }, "error": null, "created_at": "2026-07-26T10:00:00.000Z", "completed_at": "2026-07-26T10:00:41.000Z" } ``` Response fields: | Field | Type | Notes | | --- | --- | --- | | `status` | string | `processing` while running. Terminal values: `completed`, `failed`. Treat any other value as still running | | `data` | object or null | The extracted JSON. `null` until the extraction completes | | `data_expired` | boolean | `true` for old extractions whose stored result is no longer readable. Re-run the document | | `pages_used` | number | Pages charged for this extraction | | `validation` | object or null | Present when the extraction used a schema. See below | | `error` | string or null | Human-readable failure reason when `status` is `failed` | | `filename` | string | Original document name | | `created_at` / `completed_at` | ISO 8601 string | `completed_at` is `null` until terminal | `validation` reports how well the extracted data matched the schema. It is **not** the extraction's own outcome: ```json { "status": "passed", "warnings": [], "coercions": ["total: coerced string to number"] } ``` `validation.status` is one of `passed`, `partial`, `failed`, `unvalidated`. A `validation.status` of `failed` still means the extraction itself succeeded and `data` is present. Always read the top-level `status` to decide whether the job finished. ### DELETE /v1/extractions/{id} Deletes the extraction's stored result and any export files, and reduces the record to a tombstone kept only for usage accounting. ```json { "success": true, "id": "3f1a9c7e5b2d4e6f8a0b1c2d3e4f5a6b", "deleted": true } ``` Source documents are deleted automatically within 24 hours of processing. Extraction results are kept until you delete them, here or from the dashboard. ### POST /v1/extractions/{id}/export Converts a completed extraction's JSON into a spreadsheet. Exports are free, repeatable, and consume no pages. Nested arrays are flattened: parent fields repeat on every row, so the output is ready for spreadsheets and imports. Body: `{"format": "csv"}` or `{"format": "xlsx"}`. ```bash curl -X POST https://api-parse.conversiontools.io/v1/extractions/3f1a.../export \ -H "Authorization: Bearer $PARSE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"format": "xlsx"}' ``` Conversion started or still running, HTTP 202: ```json { "success": true, "status": "processing", "format": "xlsx", "progress": 0 } ``` Already finished, HTTP 200: ```json { "success": true, "status": "ready", "format": "xlsx" } ``` The call is idempotent per format: repeated calls return the running conversion or the finished file's status rather than starting a duplicate. Errors: `409` with `"code": "NOT_READY"` if the extraction has not completed yet, `410` with `"code": "RESULT_EXPIRED"` if the stored result is gone. ### GET /v1/extractions/{id}/export?format=csv|xlsx Polls the export and, once ready, returns the file itself. - While converting: HTTP 200 with JSON `{"success": true, "status": "processing", "format": "xlsx", "progress": 42}`. - On conversion failure: HTTP 200 with JSON `{"success": true, "status": "failed", "format": "xlsx", "error": "..."}`. - When ready: HTTP 200 with the **file bytes**, `Content-Disposition: attachment`, and `Content-Type: text/csv; charset=utf-8` or `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`. - If no export was ever requested for that format: `404` with `"code": "EXPORT_NOT_STARTED"`. Check the response `Content-Type` before parsing: `application/json` means the export is still processing or failed, anything else is the file. ```bash curl -o invoice.xlsx \ "https://api-parse.conversiontools.io/v1/extractions/3f1a.../export?format=xlsx" \ -H "Authorization: Bearer $PARSE_API_KEY" ``` Passing `output_format` on the extract request starts the export automatically as soon as the extraction completes, so you can go straight to this endpoint. ### GET /v1/usage ```json { "success": true, "plan": "free", "pages_used": 42, "pages_limit": 100, "pages_remaining": 58, "reset_date": "2026-08-01T00:00:00.000Z" } ``` `reset_date` is the first day of the next calendar month, when the counter resets. --- ## 7. Webhooks Pass `webhook_url` when starting an extraction and Parse sends one HTTP POST when the extraction reaches a terminal state. You never have to poll. Every request that carries a `webhook_url` is called back exactly once, including requests that do not start a new extraction: - **Answered from a stored result** (`"cached": true`): the webhook is called right away, with the stored extraction's id. - **Attached to an identical document already running**: the webhook is called when that extraction finishes, with its id. So a client that waits for the callback never waits forever, and does not need `no_cache` to get one. Delivery details: - Method `POST`, `Content-Type: application/json`, `User-Agent: Parse-Webhook/1`. - Respond with any 2xx. Non-2xx, a redirect, or no answer within 5 seconds counts as a failure. - Up to 3 attempts with a short increasing delay. Delivery never blocks, delays, or fails the extraction itself. - The URL must be a public `http` or `https` address. Loopback, private, link-local and other internal addresses are rejected when the extraction is submitted, and re-checked immediately before delivery. Payload: ```json { "event": "extraction.completed", "id": "3f1a9c7e5b2d4e6f8a0b1c2d3e4f5a6b", "status": "completed", "pages_used": 2, "error": null, "created_at": "2026-07-26T10:00:00.000Z", "completed_at": "2026-07-26T10:00:41.000Z" } ``` `event` is `extraction.completed` or `extraction.failed`. **The payload never contains the extracted data.** It is a thin notification: the receiver fetches the result from `GET /v1/extractions/{id}` over the authenticated API. That way document contents are never posted to a caller-supplied endpoint. The notification is not signed, so treat it as a hint rather than as trusted input: on receipt, call `GET /v1/extractions/{id}` with your API key and use what the API returns. An unknown id simply returns 404. --- ## 8. Repeat submissions, caching and page cost - **Identical document, identical fields, same account:** the stored result is returned immediately with `"cached": true`, and **no pages are consumed**. Matching is on the document's byte content plus the resolved field list, so editing a schema's fields invalidates the match while renaming the schema does not. - **Identical document still processing:** the request attaches to the run already in progress and returns the *same* extraction id with HTTP 202. Bounded to submissions made within the last 30 minutes. - **`no_cache`** disables both behaviours and forces a fresh, separately billed extraction. - A `webhook_url` is honoured in both cases (section 7), so there is no need for `no_cache` just to receive a callback. Page accounting: - One page is reserved when the extraction starts, and reconciled to the real page count when it completes. - Failed extractions are refunded automatically. - Cached results cost nothing. - Exports cost nothing. --- ## 9. Schemas Without a schema, Parse returns a free-form JSON representation of the document. With a schema, you decide exactly which fields come back, their types, and their shape. Pass a saved schema by `schema_id`, or send an inline `fields` array with the extraction. ### Field definition | Key | Type | Notes | | --- | --- | --- | | `name` | string | Required. `snake_case`: lowercase letters, digits and underscores, must start with a letter. Max 64 characters. `_id`, `__proto__`, `constructor` and `prototype` are reserved | | `type` | string | Required. One of `string`, `number`, `date`, `boolean`, `array`, `object` | | `description` | string | Optional, max 500 characters. Describing the field in plain language measurably improves accuracy | | `label` | string | Optional display label | | `required` | boolean | Optional | | `examples` | array | Optional, up to 5 sample values | | `aliases` | array of strings | Optional, up to 10 alternative names the field may appear under in the document | | `items` | string | **Required when `type` is `array`.** One of `string`, `number`, `date`, `boolean`, `object` | | `fields` | array | Required when `type` is `object`, or when `type` is `array` and `items` is `object`. Nested field definitions | | `validation` | object | Optional. `regex` (string fields only), `enum` (array of up to 50 strings), `min` / `max` (number fields only) | Note the shape of arrays of objects: `items` is a **string** naming the item type, and the nested definitions live in a sibling `fields` array. There is no nested `items: { type, fields }` object. Limits: nesting depth 4, 50 fields per level, 200 fields in total. ### POST /v1/schemas ```bash curl -X POST https://api-parse.conversiontools.io/v1/schemas \ -H "Authorization: Bearer $PARSE_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "invoice", "description": "Standard invoice extraction", "fields": [ { "name": "vendor_name", "type": "string", "description": "Name of the vendor or supplier" }, { "name": "invoice_number", "type": "string", "description": "Invoice reference number" }, { "name": "date", "type": "date", "description": "Invoice date" }, { "name": "total", "type": "number", "description": "Total amount due" }, { "name": "line_items", "type": "array", "items": "object", "description": "One entry per invoice line", "fields": [ { "name": "description", "type": "string" }, { "name": "quantity", "type": "number" }, { "name": "unit_price", "type": "number" }, { "name": "amount", "type": "number" } ] } ] }' ``` Response, HTTP 201: ```json { "success": true, "schema": { "id": "b47a2c9e1d8f4a6b0c3d5e7f9a1b2c3d", "name": "invoice", "description": "Standard invoice extraction", "fields": [], "created_at": "2026-07-26T10:00:00.000Z", "updated_at": "2026-07-26T10:00:00.000Z" } } ``` (`fields` echoes the normalized definitions you sent; it is shown empty here only to keep the example short.) Validation failures return `400` with a `details` array of `{ path, code, message }` objects. ### GET /v1/schemas ```json { "success": true, "schemas": [ { "id": "b47a2c9e1d8f4a6b0c3d5e7f9a1b2c3d", "name": "invoice", "description": "Standard invoice extraction", "fields": [], "usage_count": 12, "created_at": "2026-07-26T10:00:00.000Z", "updated_at": "2026-07-26T10:00:00.000Z" } ] } ``` ### GET /v1/schemas/{id} Returns `{ "success": true, "schema": { ... } }` with the same object shape as the list entry. ### PUT /v1/schemas/{id} Full replace. Send `name`, `description` and the complete `fields` array, exactly as for create. Returns the updated schema. ### DELETE /v1/schemas/{id} ```json { "success": true, "message": "Schema deleted" } ``` --- ## 10. Errors Errors are a **flat** JSON object with a top-level `error` string. There is no `success: false` envelope and no nested error object. Decide that a request failed from the HTTP status code, then read `error` for the message. ```json { "error": "Human-readable message", "param": "field_name", "value": "the offending value", "details": [{ "path": "fields[0].type", "code": "INVALID_TYPE", "message": "..." }], "code": "MACHINE_CODE", "limit": 100 } ``` Only `error` is always present. `param`, `value`, `details`, `code` and `limit` appear when relevant. | Status | Meaning | Body | | --- | --- | --- | | `400` | Validation error | `{"error": "file_id is required", "param": "file_id"}` | | `400` | Bad schema | `{"error": "Invalid fields schema", "param": "fields", "details": [...]}` | | `400` | Bad callback target | `{"error": "webhook_url must point to a public address", "param": "webhook_url"}` | | `400` | Both or neither of file and file_url | `{"error": "Send either file_id or file_url, not both.", "param": "file_url"}` | | `400` | Link refused | `{"error": "file_url must point to a public address", "param": "file_url", "code": "FILE_URL_NOT_ALLOWED"}` | | `400` | Link could not be fetched | `{"error": "Could not download the file from file_url: the server answered HTTP 404. ...", "param": "file_url", "code": "FILE_URL_HTTP_ERROR"}`. Other codes: `FILE_URL_INVALID`, `FILE_URL_UNREACHABLE`, `FILE_URL_TIMEOUT`, `FILE_URL_TOO_MANY_REDIRECTS`, `FILE_URL_EMPTY` | | `400` | Link opens a web page, not the file | `{"error": "file_url returned a web page (HTML), not the document itself. ...", "param": "file_url", "code": "FILE_URL_WEB_PAGE"}` | | `413` | Linked file too large | `{"error": "The file at file_url is larger than the 100 MB limit.", "param": "file_url", "code": "FILE_URL_TOO_LARGE"}` | | `400` | Bad export format | `{"error": "output_format must be one of: csv, xlsx", "param": "output_format"}` | | `401` | Missing header | `{"error": "Missing or invalid Authorization header", "param": "authorization"}` | | `401` | Unknown or revoked key | `{"error": "Invalid API key", "param": "authorization"}` | | `403` | Plan object limit | `{"error": "Schema limit reached (5 for free plan)", "code": "LIMIT_REACHED"}` | | `404` | Not found | `{"error": "Extraction not found", "param": "id", "value": "..."}` | | `404` | Export never started | `{"error": "Export not started", "code": "EXPORT_NOT_STARTED"}` | | `409` | Export requested too early | `{"error": "Extraction is not completed yet", "code": "NOT_READY"}` | | `410` | Stored result gone | `{"error": "Extraction result is no longer available", "code": "RESULT_EXPIRED"}` | | `429` | Monthly page limit | See below | | `500` | Server error | `{"error": "Extraction failed"}` | Both request shapes report the monthly page limit with `code: "LIMIT_REACHED"`. The `error` wording differs slightly; branch on `code`, not on the message text. A human-readable `message` and a `remedy` link are included: ```json { "error": "Monthly page limit exceeded", "code": "LIMIT_REACHED", "message": "You've reached your monthly page limit (100 pages).", "remedy": { "text": "View plans and upgrade", "path": "/pricing" } } ``` Treat any `429` as "out of pages for this month". Do not retry it automatically; the counter resets on the first day of the next month (`reset_date` from `GET /v1/usage`). Retry guidance: retry `500` responses at most twice with exponential backoff. Never auto-retry `400`, `401`, `403`, `404`, `409`, `410` or `429`. --- ## 11. Complete integration examples Each example does the same thing: upload a document, start an extraction, poll until a terminal status, handle failure, print the result. ### 11A. curl and bash Requires `jq`. ```bash #!/usr/bin/env bash set -euo pipefail API="https://api-parse.conversiontools.io" AUTH="Authorization: Bearer ${PARSE_API_KEY:?set PARSE_API_KEY}" DOCUMENT="${1:?usage: extract.sh }" POLL_INTERVAL=2 MAX_ATTEMPTS=300 # 300 x 2s = 10 minutes # 1. Upload file_id=$(curl -sS -X POST "$API/v1/upload" -H "$AUTH" -F "file=@$DOCUMENT" | jq -r '.file_id') [ "$file_id" != "null" ] || { echo "upload failed" >&2; exit 1; } # 2. Start the extraction. The response is 202 with an id, or 200 with a reused result. start=$(curl -sS -X POST "$API/v1/extract" \ -H "$AUTH" -H "Content-Type: application/json" \ -d "{\"file_id\": \"$file_id\"}") extraction_id=$(echo "$start" | jq -r '.id') status=$(echo "$start" | jq -r '.status') result="$start" # 3. Poll until a terminal status. Branch on status, never on timing. attempt=0 while [ "$status" != "completed" ] && [ "$status" != "failed" ]; do attempt=$((attempt + 1)) if [ "$attempt" -gt "$MAX_ATTEMPTS" ]; then echo "timed out waiting for extraction $extraction_id" >&2 exit 1 fi sleep "$POLL_INTERVAL" result=$(curl -sS "$API/v1/extractions/$extraction_id" -H "$AUTH") status=$(echo "$result" | jq -r '.status') done # 4. Handle both terminal states. if [ "$status" = "failed" ]; then echo "extraction failed: $(echo "$result" | jq -r '.error')" >&2 exit 1 fi echo "$result" | jq '.data' ``` ### 11B. Python (requests) ```python """Upload a document to Parse, extract it, and print the structured result.""" import os import sys import time import requests API = "https://api-parse.conversiontools.io" API_KEY = os.environ["PARSE_API_KEY"] HEADERS = {"Authorization": f"Bearer {API_KEY}"} POLL_INTERVAL_SECONDS = 2.0 POLL_TIMEOUT_SECONDS = 600.0 TERMINAL_STATUSES = {"completed", "failed"} class ExtractionError(RuntimeError): """The extraction reached a terminal 'failed' state.""" def upload(path: str) -> str: with open(path, "rb") as handle: response = requests.post( f"{API}/v1/upload", headers=HEADERS, files={"file": handle}, timeout=300 ) response.raise_for_status() return response.json()["file_id"] def start_extraction(file_id: str, schema_id: str | None = None) -> dict: body: dict[str, object] = {"file_id": file_id} if schema_id: body["schema_id"] = schema_id response = requests.post(f"{API}/v1/extract", headers=HEADERS, json=body, timeout=60) if response.status_code == 429: raise ExtractionError(response.json().get("error", "monthly page limit reached")) response.raise_for_status() # 202 = accepted and running. 200 = a stored result was reused ("cached": true). return response.json() def wait_for_result(extraction_id: str) -> dict: """Poll until the extraction is completed or failed. Bounded by a deadline.""" deadline = time.monotonic() + POLL_TIMEOUT_SECONDS while time.monotonic() < deadline: response = requests.get( f"{API}/v1/extractions/{extraction_id}", headers=HEADERS, timeout=30 ) response.raise_for_status() payload = response.json() status = payload.get("status") if status == "completed": return payload if status == "failed": raise ExtractionError(payload.get("error") or "extraction failed") # Anything else means still running. data is None until it completes. time.sleep(POLL_INTERVAL_SECONDS) raise TimeoutError( f"extraction {extraction_id} did not finish within {POLL_TIMEOUT_SECONDS:.0f}s" ) def extract_document(path: str, schema_id: str | None = None) -> dict: file_id = upload(path) started = start_extraction(file_id, schema_id) # Branch on status, never on the HTTP code or on whether "data" is present. if started.get("status") in TERMINAL_STATUSES: if started["status"] == "failed": raise ExtractionError(started.get("error") or "extraction failed") return started return wait_for_result(started["id"]) if __name__ == "__main__": result = extract_document(sys.argv[1]) print(result["data"]) ``` ### 11C. Node (fetch, Node 18 or newer) ```javascript // Upload a document to Parse, extract it, and print the structured result. import { readFile } from 'node:fs/promises'; import { basename } from 'node:path'; const API = 'https://api-parse.conversiontools.io'; const API_KEY = process.env.PARSE_API_KEY; const AUTH = { Authorization: 'Bearer ' + API_KEY }; const POLL_INTERVAL_MS = 2000; const POLL_TIMEOUT_MS = 600000; const TERMINAL_STATUSES = new Set(['completed', 'failed']); const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); async function upload(path) { const form = new FormData(); form.append('file', new Blob([await readFile(path)]), basename(path)); const response = await fetch(API + '/v1/upload', { method: 'POST', headers: AUTH, body: form }); const payload = await response.json(); if (!response.ok) throw new Error(payload.error ?? 'upload failed with ' + response.status); return payload.file_id; } async function startExtraction(fileId, schemaId) { const body = schemaId ? { file_id: fileId, schema_id: schemaId } : { file_id: fileId }; const response = await fetch(API + '/v1/extract', { method: 'POST', headers: { ...AUTH, 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); const payload = await response.json(); if (!response.ok) throw new Error(payload.error ?? 'extract failed with ' + response.status); // 202 = accepted and running. 200 = a stored result was reused ("cached": true). return payload; } async function waitForResult(extractionId) { const deadline = Date.now() + POLL_TIMEOUT_MS; while (Date.now() < deadline) { const response = await fetch(API + '/v1/extractions/' + extractionId, { headers: AUTH }); const payload = await response.json(); if (!response.ok) throw new Error(payload.error ?? 'poll failed with ' + response.status); if (payload.status === 'completed') return payload; if (payload.status === 'failed') throw new Error(payload.error ?? 'extraction failed'); // Anything else means still running. data is null until it completes. await sleep(POLL_INTERVAL_MS); } throw new Error('extraction ' + extractionId + ' did not finish within the timeout'); } export async function extractDocument(path, schemaId) { const fileId = await upload(path); const started = await startExtraction(fileId, schemaId); // Branch on status, never on the HTTP code or on whether "data" is present. if (TERMINAL_STATUSES.has(started.status)) { if (started.status === 'failed') throw new Error(started.error ?? 'extraction failed'); return started; } return waitForResult(started.id); } const result = await extractDocument(process.argv[2]); console.log(JSON.stringify(result.data, null, 2)); ``` ### 11D. Webhook variant (no polling) Start the extraction with a `webhook_url`, then fetch the result when the notification arrives. ```bash curl -X POST https://api-parse.conversiontools.io/v1/extract \ -H "Authorization: Bearer $PARSE_API_KEY" \ -F "file=@invoice.pdf" \ -F "webhook_url=https://example.com/hooks/parse" ``` Receiver, Node with Express: ```javascript import express from 'express'; const API = 'https://api-parse.conversiontools.io'; const AUTH = { Authorization: 'Bearer ' + process.env.PARSE_API_KEY }; const app = express(); app.use(express.json()); app.post('/hooks/parse', async (req, res) => { const { id, status } = req.body ?? {}; // Acknowledge immediately: a slow handler counts as a delivery failure. res.sendStatus(200); if (!id) return; if (status === 'failed') { console.error('extraction ' + id + ' failed'); return; } // The notification never carries the extracted data. Fetch it over the API, // which also verifies that the id really belongs to your account. const response = await fetch(API + '/v1/extractions/' + id, { headers: AUTH }); if (!response.ok) return; const extraction = await response.json(); if (extraction.status === 'completed') { console.log(JSON.stringify(extraction.data, null, 2)); } }); app.listen(3000); ``` Receiver, Python with Flask: ```python import os import threading import requests from flask import Flask, request API = "https://api-parse.conversiontools.io" HEADERS = {"Authorization": f"Bearer {os.environ['PARSE_API_KEY']}"} app = Flask(__name__) def fetch_result(extraction_id: str) -> None: response = requests.get(f"{API}/v1/extractions/{extraction_id}", headers=HEADERS, timeout=30) if not response.ok: return extraction = response.json() if extraction.get("status") == "completed": print(extraction["data"]) @app.post("/hooks/parse") def parse_hook(): payload = request.get_json(silent=True) or {} extraction_id = payload.get("id") # Acknowledge within 5 seconds; do the fetching off the request path. if extraction_id and payload.get("status") == "completed": threading.Thread(target=fetch_result, args=(extraction_id,), daemon=True).start() return "", 200 ``` --- ## 12. Supported documents, limits and retention Input formats: - PDF, including scanned PDFs. - Images: JPEG, PNG, GIF, WebP, TIFF, BMP, AVIF, HEIC/HEIF. The format is detected from the file's content, not from its extension or the declared MIME type. Encrypted PDFs: - A PDF locked with an **owner** password only (permissions restrictions, opens without a prompt) is handled automatically. Send nothing extra. - A PDF locked with a **user** password must be accompanied by `password`. Without it the extraction fails and the error names the missing password rather than blaming the file. - Passwords are used for the run and then removed from the stored record. Any character is accepted except control characters, which are stripped; the value is capped at 256 characters. Limits: - Maximum document size: 100 MB, uploaded or fetched from a `file_url`. - `file_url` download: must complete within 20 seconds. - `wait`: maximum 120 seconds. - Monthly pages: 100 on Free, 500 on Starter, 5,000 on Pro. Call `GET /v1/usage` for the live numbers rather than hardcoding them. - Saved schemas: 5 on Free, 10 on Starter, unlimited on Pro. - API keys: 3 on Free, 5 on Starter, unlimited on Pro. Retention: - Source documents are deleted automatically within 24 hours of processing. - Extraction results are stored until you delete them, via `DELETE /v1/extractions/{id}` or from the dashboard. - Webhook notifications never carry document contents. --- ## 13. Document types with worked schemas The API is schema-driven, so it is not limited to this list - you describe the fields you want for any document. These are the types with a published schema and a sample response, useful when you need a starting schema rather than writing one from scratch. | Document type | Reference page | | --- | --- | | Any document, general | https://parse.conversiontools.io/use-cases/data-extraction-api | | PDF, digital or scanned | https://parse.conversiontools.io/use-cases/pdf-parsing-api | | Invoice | https://parse.conversiontools.io/use-cases/invoice-extraction | | Receipt | https://parse.conversiontools.io/use-cases/receipt-parsing | | Credit note / credit memo | https://parse.conversiontools.io/use-cases/credit-note-extraction | | Remittance advice | https://parse.conversiontools.io/use-cases/remittance-advice-extraction | | Bank statement | https://parse.conversiontools.io/use-cases/bank-statement-to-json | | Financial statement, 10-Q, 10-K | https://parse.conversiontools.io/use-cases/financial-statement-extraction | | Tax form (W-2, 1099) | https://parse.conversiontools.io/use-cases/tax-form-extraction | | Utility bill | https://parse.conversiontools.io/use-cases/utility-bill-extraction | | Payslip / pay stub | https://parse.conversiontools.io/use-cases/payslip-extraction | | Purchase order | https://parse.conversiontools.io/use-cases/purchase-order-extraction | | Quotation / RFQ response | https://parse.conversiontools.io/use-cases/quotation-extraction | | Bill of lading | https://parse.conversiontools.io/use-cases/bill-of-lading-extraction | | Packing list | https://parse.conversiontools.io/use-cases/packing-list-extraction | | Delivery / consignment note | https://parse.conversiontools.io/use-cases/delivery-note-extraction | | Contract | https://parse.conversiontools.io/use-cases/contract-data-extraction | | Lease agreement | https://parse.conversiontools.io/use-cases/lease-agreement-extraction | | Insurance claim | https://parse.conversiontools.io/use-cases/insurance-claim-extraction | | Resume / CV | https://parse.conversiontools.io/use-cases/resume-parsing | | Passport / ID card | https://parse.conversiontools.io/use-cases/id-document-extraction | ID documents are a field-extraction case only: Parse reads the printed fields and the MRZ. It does not verify identity, perform liveness checks, or detect forged documents. --- ## 14. Checklist for a generated client - [ ] Send `Authorization: Bearer ` on every request, from an environment variable. - [ ] Treat `202` from `POST /v1/extract` as success, and keep the `id`. - [ ] Poll `GET /v1/extractions/{id}` roughly every 2 seconds. - [ ] Stop only on `status == "completed"` or `status == "failed"`. Do not stop on a timing heuristic. - [ ] Raise or return an error on `failed`, using the `error` string. - [ ] Bound the poll loop with a deadline or an attempt cap, and report a timeout. - [ ] Handle the 200 short-circuit: a cached result comes back already `completed`. - [ ] Read errors from the flat top-level `error` field, not from a nested object. - [ ] Do not auto-retry `429`. Surface it as "monthly page limit reached". - [ ] Never re-upload the same document to "hurry it up".