{"openapi":"3.1.0","jsonSchemaDialect":"https://json-schema.org/draft/2020-12/schema","info":{"title":"txtfetch extract API","version":"1.0.0","summary":"Turn any document into clean plain text over a simple HTTP API.","description":"Pass a URL or POST a file (PDF, DOCX, PPTX, XLSX, HTML, EML, scanned\nimages, …) and get the extracted text back as JSON. One endpoint, two\nways to call it, one response shape.\n\nLarge uploads or documents automatically run through an async job path\n(`202` + poll `GET /v1/extract/{job_id}`); `?async=true` forces it for any\nrequest.\n","contact":{"url":"https://txtfetch.com/contact"},"license":{"name":"Proprietary","url":"https://txtfetch.com/compliance"}},"servers":[{"url":"https://api.txtfetch.com","description":"Production"}],"security":[{"bearerAuth":[]}],"tags":[{"name":"extract","description":"Document text extraction"}],"paths":{"/v1/extract":{"post":{"operationId":"extractDocument","tags":["extract"],"summary":"Extract text from a document","description":"Provide exactly one input source: a multipart `file` upload, or a\n`?url=` for txtfetch to fetch server-side (SSRF-guarded). Large\ninputs are automatically routed to the async job path; pass\n`?async=true` to force it for any request.\n","parameters":[{"$ref":"#/components/parameters/UrlParam"},{"$ref":"#/components/parameters/AsyncParam"},{"$ref":"#/components/parameters/WebhookUrlParam"},{"$ref":"#/components/parameters/IdempotencyKeyHeader"}],"requestBody":{"description":"A multipart `file` part. Omit the body entirely when using `?url=`\ninstead.\n","required":false,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","contentEncoding":"binary","description":"The document to extract text from."}},"required":["file"]}}}},"responses":{"200":{"description":"Extraction completed synchronously. The response shape depends on\n`?format=`: the default `text` returns `ExtractSuccess`;\n`format=json` (standard quality) returns `JsonSuccess`.\n","headers":{"X-Quota-Limit":{"$ref":"#/components/headers/XQuotaLimit"},"X-Quota-Remaining":{"$ref":"#/components/headers/XQuotaRemaining"},"X-Quota-Reset":{"$ref":"#/components/headers/XQuotaReset"}},"content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ExtractSuccess"},{"$ref":"#/components/schemas/JsonSuccess"}]},"examples":{"text":{"summary":"format=text (default)","value":{"status":"success","extracted_text":"Q3 revenue grew 34% year over year, driven by...","metadata":{"content_type":"application/pdf","bytes":482913,"chars":812,"ocr":false}}},"json":{"summary":"format=json","value":{"status":"success","elements":[{"type":"heading","text":"Quarterly Report","level":1,"page":null,"offset":0,"bbox":null}],"metadata":{"content_type":"application/pdf","bytes":482913,"chars":812,"ocr":false,"format":"json","tier":"standard","pages":3,"vlm":false}}}}}}},"202":{"description":"Large input accepted for async processing (auto-routed, or\n`?async=true` was set). Poll `GET /v1/extract/{job_id}` for the\nresult. `X-Quota-*` headers reflect the pre-check snapshot taken\nat accept time — the job is metered on completion, not here.\n","headers":{"X-Quota-Limit":{"$ref":"#/components/headers/XQuotaLimit"},"X-Quota-Remaining":{"$ref":"#/components/headers/XQuotaRemaining"},"X-Quota-Reset":{"$ref":"#/components/headers/XQuotaReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobAccepted"},"example":{"status":"processing","job_id":"3fa85f64-5717-4562-b3fc-2c963f66afa6"}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"413":{"$ref":"#/components/responses/TooLarge"},"415":{"$ref":"#/components/responses/UnsupportedFormat"},"422":{"$ref":"#/components/responses/UnprocessableDocument"},"429":{"$ref":"#/components/responses/TooManyRequests"},"501":{"$ref":"#/components/responses/NotImplemented"},"504":{"$ref":"#/components/responses/Timeout"}}}},"/v1/extract/{job_id}":{"get":{"operationId":"getExtractJob","tags":["extract"],"summary":"Poll the status of an async extraction job","description":"Returns the same shape and status code the synchronous\n`POST /v1/extract` would have returned, once the job finishes.\n","parameters":[{"name":"job_id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"The `job_id` returned by a `202` from `POST /v1/extract`.","example":"3fa85f64-5717-4562-b3fc-2c963f66afa6"}],"responses":{"200":{"description":"Either the job is still running, or it finished successfully.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/JobProcessing"},{"$ref":"#/components/schemas/ExtractSuccess"},{"$ref":"#/components/schemas/JsonSuccess"}]},"examples":{"processing":{"summary":"Job is still running","value":{"status":"processing","job_id":"3fa85f64-5717-4562-b3fc-2c963f66afa6"}},"success":{"summary":"Job finished successfully (format=text, default)","value":{"status":"success","extracted_text":"Q3 revenue grew 34% year over year, driven by...","metadata":{"content_type":"application/pdf","bytes":482913,"chars":812,"ocr":false}}},"success_json":{"summary":"Job finished successfully (format=json)","value":{"status":"success","elements":[{"type":"heading","text":"Quarterly Report","level":1,"page":null,"offset":0,"bbox":null}],"metadata":{"content_type":"application/pdf","bytes":482913,"chars":812,"ocr":false,"format":"json","tier":"standard","pages":3,"vlm":false}}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"413":{"$ref":"#/components/responses/TooLarge"},"415":{"$ref":"#/components/responses/UnsupportedFormat"},"422":{"$ref":"#/components/responses/UnprocessableDocument"},"501":{"$ref":"#/components/responses/NotImplemented"},"504":{"$ref":"#/components/responses/Timeout"}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"tf_live","description":"Every request carries an API key issued from the early-access\nprogram: `Authorization: Bearer tf_live_...`.\n"}},"parameters":{"UrlParam":{"name":"url","in":"query","required":false,"description":"An `https://` URL for txtfetch to fetch server-side instead of a\nfile upload. Mutually exclusive with the multipart `file` body.\n","schema":{"type":"string","format":"uri"},"example":"https://example.com/whitepaper.docx"},"AsyncParam":{"name":"async","in":"query","required":false,"description":"Force the async job path (`202` + `job_id`) instead of waiting\nsynchronously. Large uploads and large `?url=` targets are routed to\nthe async path automatically even when this is omitted.\n","schema":{"type":"boolean","default":false},"example":true},"WebhookUrlParam":{"name":"webhook_url","in":"query","required":false,"description":"An `https://`/`http://` URL txtfetch will POST the job result to once\nit finishes, instead of (or in addition to) polling\n`GET /v1/extract/{job_id}`. The delivery is signed — see\n`X-Txtfetch-Signature` in the async & webhooks guide\n(https://txtfetch.com/docs/async) — and retried up to 3 times with a\nshort backoff; a delivery failure never fails the underlying job.\n","schema":{"type":"string","format":"uri"},"example":"https://example.com/webhooks/txtfetch"},"IdempotencyKeyHeader":{"name":"Idempotency-Key","in":"header","required":false,"description":"A client-generated key so a retried `POST /v1/extract` is not\nprocessed twice: a repeat under the same key replays the original\nresponse, while a repeat with a different body/target under the same\nkey returns `bad_request`. See the idempotency guide\n(https://txtfetch.com/docs/idempotency) for the full semantics; both\nofficial SDKs generate and reuse one automatically on retry.\n","schema":{"type":"string"},"example":"3f29b6e4-9c1a-4b8e-9c2a-1e6f0a2d5b3c"}},"responses":{"Unauthorized":{"description":"No `Authorization` header was supplied. API Gateway rejects the\nrequest before the authorizer Lambda runs, so this is the platform's\nown error body, not this API's typed `ExtractError` envelope — match\non HTTP status, never on a `code` field.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthFailure"},"example":{"message":"Unauthorized"}}}},"Forbidden":{"description":"The `Authorization` header carried a malformed, unknown, or revoked\nAPI key: the REQUEST authorizer (`api/authorizer/`) ran and denied\nthe request. Same platform body as `401` — match on HTTP status, not\na `code` field. A just-revoked key can still be accepted for up to 60\nseconds, the authorizer's result-cache TTL.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthFailure"},"example":{"message":"Forbidden"}}}},"BadRequest":{"description":"The request was malformed (e.g. no `file` part or `?url=`), or a\nserver-side `?url=` fetch failed (blocked address, connection error,\nnon-2xx, or timeout).\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtractError"},"examples":{"bad_request":{"summary":"Malformed request","value":{"status":"error","error":{"code":"bad_request","message":"provide a file upload or ?url="}}},"fetch_failed":{"summary":"Server-side URL fetch failed","value":{"status":"error","error":{"code":"fetch_failed","message":"failed to fetch the document from the given url"}}}}}}},"UnsupportedFormat":{"description":"The input's real (byte-detected) type isn't a supported document format.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtractError"},"example":{"status":"error","error":{"code":"unsupported_format","message":"input appears to be an executable binary"}}}}},"TooLarge":{"description":"The input exceeds the maximum accepted size.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtractError"},"example":{"status":"error","error":{"code":"too_large","message":"upload exceeds max size of 10485760 bytes; use ?url= for larger documents"}}}}},"UnprocessableDocument":{"description":"The document is encrypted/password-protected, or extraction ran but\nproduced no text.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtractError"},"examples":{"encrypted":{"summary":"Password-protected document","value":{"status":"error","error":{"code":"encrypted","message":"document is encrypted or password-protected"}}},"extraction_failed":{"summary":"Extraction produced no text","value":{"status":"error","error":{"code":"extraction_failed","message":"extraction produced no text"}}}}}}},"Timeout":{"description":"Extraction did not complete within the processing budget.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtractError"},"example":{"status":"error","error":{"code":"timeout","message":"operation exceeded the extraction budget"}}}}},"NotFound":{"description":"The `job_id` is malformed, unknown, or has expired.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtractError"},"example":{"status":"error","error":{"code":"not_found","message":"unknown or expired job id"}}}}},"NotImplemented":{"description":"The request asked for a valid but not-yet-shipped response mode:\n`format=markdown` (any quality), or `quality=premium` (any format).\n`format=json`+`quality=standard` is shipped — see `JsonSuccess`.\n","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtractError"},"example":{"status":"error","error":{"code":"not_implemented","message":"format=markdown quality=standard is not implemented yet"}}}}},"TooManyRequests":{"description":"Too many requests for this API key: its per-minute rate limit, its\nmonthly document quota, or its abuse circuit breaker has tripped,\nchecked in that order — rate limiting is the outermost gate and\ncounts every accepted request (including ones that go on to fail);\nthe abuse circuit breaker (`abuse_detected`) is checked next, ahead\nof the monthly quota, and trips on a burst of abusive *outcomes*\n(repeated malformed input, and especially oversized/body-bomb\nrejections) rather than raw volume — distinct from `rate_limited`\n(per-minute volume) and `quota_exceeded` (monthly successes); the\nmonthly quota counts only successful extractions and is checked\nlast, rejecting before any extraction work runs (no Tika, no async\ninvoke). Retry after the `Retry-After` delta-seconds, or once\n`X-RateLimit-Reset` / `X-Quota-Reset` (unix seconds) passes.\n`X-Quota-*` headers only appear on a `quota_exceeded` response;\n`X-RateLimit-*` headers appear on all three, since the rate-limit\ncheck always runs first.\n","headers":{"Retry-After":{"description":"Seconds until the relevant window (rate limit, or monthly quota) resets.","schema":{"type":"integer"},"example":42},"X-RateLimit-Limit":{"description":"This API key's per-minute request limit.","schema":{"type":"integer"},"example":60},"X-RateLimit-Remaining":{"description":"Requests remaining in the current window (0 when blocked).","schema":{"type":"integer"},"example":0},"X-RateLimit-Reset":{"description":"Unix epoch seconds when the current window resets.","schema":{"type":"integer"},"example":1700000060},"X-Quota-Limit":{"$ref":"#/components/headers/XQuotaLimit"},"X-Quota-Remaining":{"$ref":"#/components/headers/XQuotaRemaining"},"X-Quota-Reset":{"$ref":"#/components/headers/XQuotaReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExtractError"},"examples":{"quota_exceeded":{"summary":"Monthly document quota exceeded","value":{"status":"error","error":{"code":"quota_exceeded","message":"monthly quota exceeded for this API key"}}},"rate_limited":{"summary":"Per-minute rate limit exceeded","value":{"status":"error","error":{"code":"rate_limited","message":"per-minute request rate exceeded for this API key"}}},"abuse_detected":{"summary":"Abuse circuit breaker tripped (repeated-failure / body-bomb lockout)","value":{"status":"error","error":{"code":"abuse_detected","message":"this API key is temporarily locked out for repeated abusive requests"}}}}}}}},"headers":{"XQuotaLimit":{"description":"This API key's monthly document quota (its plan's `quotaDocs`).","schema":{"type":"integer"},"example":10000},"XQuotaRemaining":{"description":"Documents remaining in the current billing period, floored at 0.","schema":{"type":"integer"},"example":9412},"XQuotaReset":{"description":"Epoch seconds when the monthly quota resets (the next UTC month boundary).","schema":{"type":"integer"},"example":1785715200}},"schemas":{"AuthFailure":{"type":"object","description":"API Gateway/authorizer platform error body for a `401`/`403` — not\nthis API's typed `ExtractError` envelope, and has no stable `code`\nfield to match on.\n","properties":{"message":{"type":"string"}},"required":["message"],"additionalProperties":false},"ExtractMetadata":{"type":"object","description":"Details about the extracted document.","properties":{"content_type":{"type":"string","description":"The detected MIME type of the input.","example":"application/pdf"},"bytes":{"type":"integer","minimum":0,"description":"Size of the input in bytes.","example":482913},"chars":{"type":"integer","minimum":0,"description":"Length of `extracted_text` in characters.","example":812},"ocr":{"type":"boolean","description":"Whether OCR (Tesseract) was used to produce the text.","example":false}},"required":["content_type","bytes","chars","ocr"],"additionalProperties":false},"ExtractSuccess":{"type":"object","description":"A completed extraction, whether returned synchronously or via job polling.","properties":{"status":{"type":"string","const":"success"},"extracted_text":{"type":"string","description":"The extracted plain text."},"metadata":{"$ref":"#/components/schemas/ExtractMetadata"}},"required":["status","extracted_text","metadata"],"additionalProperties":false},"AdditiveMetadata":{"type":"object","description":"`ExtractMetadata`'s fields plus the additive fields present on\nnon-`text` responses (`format=json`/`markdown`, or\n`quality=premium`) — see docs/response-contract.md. Flattened\n(rather than `allOf`-composed with `ExtractMetadata`) so its sibling\n`additionalProperties: false` doesn't reject these additive fields.\n","properties":{"content_type":{"type":"string","description":"The detected MIME type of the input.","example":"application/pdf"},"bytes":{"type":"integer","minimum":0,"description":"Size of the input in bytes.","example":482913},"chars":{"type":"integer","minimum":0,"description":"Length of `extracted_text` in characters.","example":812},"ocr":{"type":"boolean","description":"Whether OCR (Tesseract) was used to produce the text.","example":false},"format":{"type":"string","enum":["text","markdown","json"]},"tier":{"type":"string","enum":["standard","premium"]},"pages":{"type":["integer","null"],"description":"Page/sheet count, when the format/engine reports one."},"vlm":{"type":"boolean","description":"Whether the VLM (premium) tier ran."},"usage":{"type":"object","description":"Present only when the VLM (premium) tier ran.","properties":{"model":{"type":"string"},"input_tokens":{"type":"integer","minimum":0},"output_tokens":{"type":"integer","minimum":0}},"required":["model","input_tokens","output_tokens"],"additionalProperties":false}},"required":["content_type","bytes","chars","ocr","format","tier","pages","vlm"],"additionalProperties":false},"Cell":{"type":"object","description":"One cell of a table's `cells` grid.","properties":{"text":{"type":"string"},"colspan":{"type":"integer","minimum":1},"rowspan":{"type":"integer","minimum":1},"header":{"type":"boolean"}},"required":["text","colspan","rowspan","header"],"additionalProperties":false},"Element":{"type":"object","description":"One block of the `elements` array — see\ndocs/element-json-schema.md for the full per-type field list and\ndesign rationale. `bbox` is always `null` on this (Tika-backed) path.\n","properties":{"type":{"type":"string","enum":["heading","paragraph","list","list_item","table","image","code"]},"text":{"type":"string"},"page":{"type":["integer","null"]},"offset":{"type":"integer","minimum":0},"bbox":{"type":"null"},"level":{"type":"integer","minimum":1,"maximum":6,"description":"Only present when `type` is `heading`."},"ordered":{"type":"boolean","description":"Only present when `type` is `list`."},"items":{"type":"array","items":{"$ref":"#/components/schemas/Element"},"description":"Only present on `list` (required) and `list_item` (optional, nested sublists)."},"markdown":{"type":"string","description":"Only present when `type` is `table` — GFM."},"html":{"type":"string","description":"Only present when `type` is `table` — sanitized `<table>`."},"cells":{"type":"array","items":{"type":"array","items":{"$ref":"#/components/schemas/Cell"}},"description":"Only present when `type` is `table` — the 2-D grid, header row first when present."},"rows":{"type":"integer","minimum":0,"description":"Only present when `type` is `table`."},"cols":{"type":"integer","minimum":0,"description":"Only present when `type` is `table`."},"alt":{"type":"string","description":"Only present when `type` is `image`."},"caption":{"type":["string","null"],"description":"Only present when `type` is `image`."}},"required":["type","text","page","offset","bbox"],"additionalProperties":false},"TableElement":{"description":"The `table`-typed shape of `Element`, documented separately for clarity.","allOf":[{"$ref":"#/components/schemas/Element"},{"type":"object","properties":{"type":{"const":"table"}},"required":["type","markdown","html","cells","rows","cols"]}]},"JsonSuccess":{"type":"object","description":"A completed `format=json` extraction, whether returned synchronously or via job polling.","properties":{"status":{"type":"string","const":"success"},"elements":{"type":"array","items":{"$ref":"#/components/schemas/Element"}},"metadata":{"$ref":"#/components/schemas/AdditiveMetadata"}},"required":["status","elements","metadata"],"additionalProperties":false},"JobAccepted":{"type":"object","description":"Returned with HTTP 202 when `POST /v1/extract` routes to the async job path.","properties":{"status":{"type":"string","const":"processing"},"job_id":{"type":"string","format":"uuid","description":"Poll `GET /v1/extract/{job_id}` with this id."}},"required":["status","job_id"],"additionalProperties":false},"JobProcessing":{"type":"object","description":"Returned with HTTP 200 from `GET /v1/extract/{job_id}` while the job\nis still running. Same shape as `JobAccepted`, different status code.\n","properties":{"status":{"type":"string","const":"processing"},"job_id":{"type":"string","format":"uuid"}},"required":["status","job_id"],"additionalProperties":false},"ErrorCode":{"type":"string","description":"Stable machine-readable error identifier. Each code maps to a fixed\nHTTP status:\n\n| code                 | HTTP status |\n|----------------------|-------------|\n| `bad_request`        | 400         |\n| `fetch_failed`       | 400         |\n| `not_found`          | 404         |\n| `unsupported_format` | 415         |\n| `too_large`          | 413         |\n| `encrypted`          | 422         |\n| `extraction_failed`  | 422         |\n| `timeout`            | 504         |\n| `not_implemented`    | 501         |\n| `quota_exceeded`     | 429         |\n| `rate_limited`       | 429         |\n| `abuse_detected`     | 429         |\n","enum":["bad_request","unsupported_format","too_large","encrypted","extraction_failed","timeout","fetch_failed","not_found","not_implemented","quota_exceeded","rate_limited","abuse_detected"]},"ExtractError":{"type":"object","description":"The response shape for every non-success status code.","properties":{"status":{"type":"string","const":"error"},"error":{"type":"object","properties":{"code":{"$ref":"#/components/schemas/ErrorCode"},"message":{"type":"string","description":"Human-readable detail; not stable, don't match on it."}},"required":["code","message"],"additionalProperties":false}},"required":["status","error"],"additionalProperties":false}}}}