{"openapi":"3.0.3","info":{"title":"FastOCR API","version":"1.0.0","description":"Asynchronous REST API for document OCR and bank statement extraction. Upload a PDF or an image, and get the extracted text or a searchable PDF back, across 100+ languages. The v1 document API accepts PDF files up to 1 GiB and PNG, JPG, JPEG, GIF, WebP and BMP images up to 20 MiB. An image counts as one page. Bank statements (PDF up to 50 MiB, PNG, JPG, JPEG and WebP images up to 20 MiB) return structured JSON, CSV, Excel and accounting files (QBO, QuickBooks CSV, QFX, OFX).\n\nFlow: POST /v1/documents (create the document and get a presigned upload URL), PUT the file to that URL, POST /v1/documents/{documentId}/start, poll GET /v1/documents/{documentId} until the status is completed, partial, or failed, then GET /v1/documents/{documentId}/output?format=text|pdf for a short-lived download link.\n\nBank statement flow: POST /v1/bank-statements (size_bytes is optional), PUT the file to the upload URL, then poll GET /v1/bank-statements/{statementId}. The poll starts the statement once the file is uploaded and, when the status is completed, returns a download url for every output. POST /v1/bank-statements/{statementId}/start and GET /v1/bank-statements/{statementId}/output?format=json|csv|xlsx|qbo|quickbooks_csv|qfx|ofx remain available.\n\nAccess: Any signed-in FastOCR account can create an API key and start right away. Sign in and create an API key in the developer console at https://fastocr.org/developers/keys. You accept the API terms (https://fastocr.org/api-terms) with your first key. Each account gets 20 free pages per month, shared with PDFs on the website. When you need more than the free pages, buy a Page Pack on the pricing page at https://fastocr.org/pricing?from=api#packs. A document that runs out of pages fails with buy_pages. After you buy pages, call /start again on the same document. Bank statements use the same statement pages as the website converter, including its free statement pages, and the same statement packs; a statement that runs out of pages fails with buy_pages and is started again with POST /v1/bank-statements/{statementId}/start.\n\nErrors: request errors return the error envelope {\"error\": {type, code, message, request_id}}. Branch on code, never on message, and treat an unknown code as new behavior. A document or statement that fails during processing is not an HTTP error: GET returns 200 with status failed and an error object. Errors generated by the API gateway before a request reaches the API (401 missing Authorization header, 403 unknown, malformed, or revoked key, 429 above a request rate limit) have the body {\"message\": \"...\"} and, on newer deployments, an additional error object with type gateway_error (see GatewayError). If the error object is absent, rely on the HTTP status and message.\n\nRate limits: each API key and each source address has its own request rate limit on /v1, in addition to the shared throttle of the gateway. The limits are not published. A request above a limit returns 429 with the body {\"message\": \"Too Many Requests\"} and a Retry-After header. Back off and retry on 429.\n\nConcurrency: 2 documents at a time; 10 for accounts with an API contract or 10,000 or more pages bought. A start request over the limit returns 429 concurrency_limit with a Retry-After header. Bank statements follow the same rule, counted separately from documents; a poll over the limit returns 200 with status queued and a Retry-After header. Very large documents may take longer to start.\n\nBalance: GET /v1/balance returns the pages left on the account. Page Packs, the same pages you can buy for the website, are one balance for the website and the API.\n\nVersioning and compatibility: /v1 is additive-only. Fields are added, never renamed, removed, or retyped; status codes and existing error codes do not change. New fields, new statuses, and new error codes may appear: clients must ignore unknown fields and treat an unknown error code as processing_failed (document errors) or by its HTTP status (request errors). Anything breaking would ship as /v2 with at least 6 months of /v1 overlap.","contact":{"name":"FastOCR Support","url":"https://fastocr.org","email":"support@fastocr.org"},"license":{"name":"Proprietary","url":"https://fastocr.org/terms"},"termsOfService":"https://fastocr.org/api-terms"},"servers":[{"url":"https://api.fastocr.org","description":"FastOCR public API"}],"security":[{"BearerAuth":[]}],"tags":[{"name":"Documents","description":"PDF and image document OCR."},{"name":"Bank Statements","description":"Bank statement extraction to JSON, CSV, Excel and accounting files."},{"name":"Account","description":"Page balance."}],"paths":{"/v1/documents":{"post":{"operationId":"createDocument","tags":["Documents"],"summary":"Create a document","description":"Create a document for a PDF or an image and receive a presigned upload URL. PUT the file to upload_url within one hour with the Content-Type of its file extension: application/pdf for .pdf, image/png for .png, image/jpeg for .jpg and .jpeg, image/gif for .gif, image/webp for .webp, image/bmp for .bmp. Then call the start endpoint. The upload must be exactly size_bytes long or start returns 400 size_mismatch.","parameters":[{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","maxLength":255},"description":"A unique value for this create request, such as a UUID, at most 255 characters. Repeating a request with the same key and the same body within 24 hours returns the original 201 response with the original document id. If the original upload_url has expired while the document is still awaiting_upload, the replay carries a fresh upload_url and expires_at. Reusing the key with a different body returns 409 idempotency_key_conflict. The body is compared on filename, size_bytes, sha256, and external_id. The header name is matched case-insensitively."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentCreateRequest"},"example":{"filename":"scan.pdf","size_bytes":1048576,"external_id":"INV-1001"}}}},"responses":{"201":{"description":"Document created. The first request and any idempotent replay both return 201; a replay returns the original body, with a fresh upload_url and expires_at when the original URL has expired and the document is still awaiting_upload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateResponse"},"example":{"id":"3f9c2b1e-7d54-4a8e-9b0a-5c1e8d2f6a47","status":"awaiting_upload","upload_url":"https://fastocr-uploads.s3.amazonaws.com/uploads/...","expires_at":"2026-10-01T13:00:00.123456+00:00"}}}},"400":{"description":"The request is invalid. error.code is one of: invalid_json (body is not valid JSON or is not a JSON object), missing_idempotency_key (no Idempotency-Key header), invalid_idempotency_key (Idempotency-Key is longer than 255 characters), invalid_filename (filename is missing, not a string, or longer than 255 characters), unsupported_file_type (filename does not end in .pdf, .png, .jpg, .jpeg, .gif, .webp or .bmp), invalid_size_bytes (size_bytes is not a positive integer), file_too_large (size_bytes is over 1,073,741,824 for a PDF or over 20,971,520 for an image), invalid_sha256 (sha256 is not the base64 encoding of the 32-byte digest), invalid_external_id (external_id is not a string of at most 256 characters). error.type is invalid_request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"invalid_request","code":"unsupported_file_type","message":"Only PDF, PNG, JPG, JPEG, GIF, WebP and BMP files are supported","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"401":{"description":"No Authorization header. Returned by the gateway before the request reaches the API. The body has message and, on newer deployments, an error object with code unauthorized; if error is absent, rely on the HTTP status and message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"},"example":{"message":"Unauthorized","error":{"type":"gateway_error","code":"unauthorized","message":"Unauthorized","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"403":{"description":"The Authorization header is present but the key is unknown, malformed, or revoked. Returned by the gateway with HTTP 403, not 401. The body has message and, on newer deployments, an error object with code access_denied; if error is absent, rely on the HTTP status and message. The message text is generated by the gateway and can change. A key that was revoked in the last 60 seconds can still be accepted because the gateway caches key checks for up to 60 seconds. A request to a route that does not exist also returns 403, with code missing_authentication_token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"},"example":{"message":"User is not authorized to access this resource with an explicit deny in an identity-based policy","error":{"type":"gateway_error","code":"access_denied","message":"User is not authorized to access this resource with an explicit deny in an identity-based policy","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"409":{"description":"The Idempotency-Key was already used with a different request body. error.code is idempotency_key_conflict and error.type is conflict.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"conflict","code":"idempotency_key_conflict","message":"Idempotency-Key was already used for a different request body","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"429":{"description":"The request was above a request rate limit: the per-key or per-address limit on /v1, or the shared throttle of the gateway. Returned before the request reaches the API. The body has message and, when the gateway produced the response, an error object with code throttled; a per-key or per-address 429 has message only and a Retry-After header. Back off, honour Retry-After when present, and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"},"example":{"message":"Too Many Requests","error":{"type":"gateway_error","code":"throttled","message":"Too Many Requests","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"500":{"description":"Unexpected server error. error.type is error and error.code is not a stable value; treat any 5xx as retryable with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"error","code":"AppError","message":"Internal server error","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}}}},"get":{"operationId":"listDocuments","tags":["Documents"],"summary":"List documents","description":"List the account's documents, newest created first. Each page holds up to limit documents when more exist. A page can be short while next_cursor is set, so keep following next_cursor until it is null.","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Maximum number of documents to return for this page, from 1 to 100."},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"Opaque cursor from the previous response's next_cursor. A cursor expires 24 hours after it is issued. An expired, malformed, or forged cursor returns 400 invalid_cursor."},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["awaiting_upload","processing","completed","partial","failed"]},"description":"Return only documents with this public status. Any other value returns 400 invalid_status."},{"name":"updated_since","in":"query","required":false,"schema":{"type":"string","example":"2026-10-01T00:00:00Z"},"description":"Return documents whose last status change, or creation time if the status never changed, is at or after this value. Send an ISO 8601 date or datetime. Z, any offset, and a missing offset all work. A missing offset means UTC. A value that is not a date is never rejected and is compared as text. In a raw query string, write a + offset as %2B, or use Z."}],"responses":{"200":{"description":"A page of documents.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentListResponse"},"example":{"documents":[{"id":"3f9c2b1e-7d54-4a8e-9b0a-5c1e8d2f6a47","status":"completed","external_id":"INV-1001","pages_billed":10,"pages_total":10,"pages_processed":10,"is_truncated":false,"outputs":{"text":{"available":true},"pdf":{"available":true}}}],"next_cursor":null}}}},"400":{"description":"The query is invalid. error.code is one of: invalid_limit (limit is not an integer from 1 to 100), invalid_status (status is not a public status), invalid_cursor (cursor is malformed, forged, or older than 24 hours). error.type is invalid_request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"invalid_request","code":"invalid_limit","message":"limit must be between 1 and 100","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"401":{"description":"No Authorization header. Returned by the gateway before the request reaches the API. The body has message and, on newer deployments, an error object with code unauthorized; if error is absent, rely on the HTTP status and message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"},"example":{"message":"Unauthorized","error":{"type":"gateway_error","code":"unauthorized","message":"Unauthorized","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"403":{"description":"The Authorization header is present but the key is unknown, malformed, or revoked. Returned by the gateway with HTTP 403, not 401. The body has message and, on newer deployments, an error object with code access_denied; if error is absent, rely on the HTTP status and message. The message text is generated by the gateway and can change. A key that was revoked in the last 60 seconds can still be accepted because the gateway caches key checks for up to 60 seconds. A request to a route that does not exist also returns 403, with code missing_authentication_token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"},"example":{"message":"User is not authorized to access this resource with an explicit deny in an identity-based policy","error":{"type":"gateway_error","code":"access_denied","message":"User is not authorized to access this resource with an explicit deny in an identity-based policy","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"429":{"description":"The request was above a request rate limit: the per-key or per-address limit on /v1, or the shared throttle of the gateway. Returned before the request reaches the API. The body has message and, when the gateway produced the response, an error object with code throttled; a per-key or per-address 429 has message only and a Retry-After header. Back off, honour Retry-After when present, and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"},"example":{"message":"Too Many Requests","error":{"type":"gateway_error","code":"throttled","message":"Too Many Requests","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"500":{"description":"Unexpected server error. error.type is error and error.code is not a stable value; treat any 5xx as retryable with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"error","code":"AppError","message":"Internal server error","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}}}}},"/v1/documents/{documentId}":{"get":{"operationId":"getDocument","tags":["Documents"],"summary":"Get a document","description":"Return the document's current state. A document that failed during processing returns 200 with status failed and an error object.","parameters":[{"name":"documentId","in":"path","required":true,"schema":{"type":"string"},"description":"Document ID returned by the create request."}],"responses":{"200":{"description":"The document.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DocumentStatusResponse"},"examples":{"completed":{"summary":"Completed","value":{"id":"3f9c2b1e-7d54-4a8e-9b0a-5c1e8d2f6a47","status":"completed","external_id":"INV-1001","pages_billed":10,"pages_total":10,"pages_processed":10,"is_truncated":false,"outputs":{"text":{"available":true},"pdf":{"available":true}}}},"failed":{"summary":"Failed during processing","value":{"id":"3f9c2b1e-7d54-4a8e-9b0a-5c1e8d2f6a47","status":"failed","external_id":null,"error":{"type":"processing_error","code":"timeout","message":"This file took too long to process. Please try again.","retryable":true}}}}}}},"401":{"description":"No Authorization header. Returned by the gateway before the request reaches the API. The body has message and, on newer deployments, an error object with code unauthorized; if error is absent, rely on the HTTP status and message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"},"example":{"message":"Unauthorized","error":{"type":"gateway_error","code":"unauthorized","message":"Unauthorized","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"403":{"description":"The Authorization header is present but the key is unknown, malformed, or revoked. Returned by the gateway with HTTP 403, not 401. The body has message and, on newer deployments, an error object with code access_denied; if error is absent, rely on the HTTP status and message. The message text is generated by the gateway and can change. A key that was revoked in the last 60 seconds can still be accepted because the gateway caches key checks for up to 60 seconds. A request to a route that does not exist also returns 403, with code missing_authentication_token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"},"example":{"message":"User is not authorized to access this resource with an explicit deny in an identity-based policy","error":{"type":"gateway_error","code":"access_denied","message":"User is not authorized to access this resource with an explicit deny in an identity-based policy","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"404":{"description":"No document with this ID exists for the account. error.code is document_not_found and error.type is not_found. IDs that belong to another account, or to a deleted document, also return 404.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"not_found","code":"document_not_found","message":"Document not found","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"429":{"description":"The request was above a request rate limit: the per-key or per-address limit on /v1, or the shared throttle of the gateway. Returned before the request reaches the API. The body has message and, when the gateway produced the response, an error object with code throttled; a per-key or per-address 429 has message only and a Retry-After header. Back off, honour Retry-After when present, and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"},"example":{"message":"Too Many Requests","error":{"type":"gateway_error","code":"throttled","message":"Too Many Requests","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"500":{"description":"Unexpected server error. error.type is error and error.code is not a stable value; treat any 5xx as retryable with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"error","code":"AppError","message":"Internal server error","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}}}},"delete":{"operationId":"deleteDocument","tags":["Documents"],"summary":"Delete a document","description":"Request deletion of the document and its files. The files and the document record are removed in the background. GET never returns a deleting status: until the record is gone the document keeps its last status, and after that GET and DELETE return 404 document_not_found. Repeating the request while deletion is pending returns 202 again.","parameters":[{"name":"documentId","in":"path","required":true,"schema":{"type":"string"},"description":"Document ID returned by the create request."}],"responses":{"202":{"description":"Deletion was requested.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteResponse"},"example":{"status":"deleting"}}}},"401":{"description":"No Authorization header. Returned by the gateway before the request reaches the API. The body has message and, on newer deployments, an error object with code unauthorized; if error is absent, rely on the HTTP status and message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"},"example":{"message":"Unauthorized","error":{"type":"gateway_error","code":"unauthorized","message":"Unauthorized","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"403":{"description":"The Authorization header is present but the key is unknown, malformed, or revoked. Returned by the gateway with HTTP 403, not 401. The body has message and, on newer deployments, an error object with code access_denied; if error is absent, rely on the HTTP status and message. The message text is generated by the gateway and can change. A key that was revoked in the last 60 seconds can still be accepted because the gateway caches key checks for up to 60 seconds. A request to a route that does not exist also returns 403, with code missing_authentication_token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"},"example":{"message":"User is not authorized to access this resource with an explicit deny in an identity-based policy","error":{"type":"gateway_error","code":"access_denied","message":"User is not authorized to access this resource with an explicit deny in an identity-based policy","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"404":{"description":"No document with this ID exists for the account. error.code is document_not_found and error.type is not_found. IDs that belong to another account, or to a deleted document, also return 404.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"not_found","code":"document_not_found","message":"Document not found","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"409":{"description":"The document is processing and cannot be deleted. error.code is document_processing and error.type is conflict.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"conflict","code":"document_processing","message":"Processing documents cannot be deleted","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"429":{"description":"The request was above a request rate limit: the per-key or per-address limit on /v1, or the shared throttle of the gateway. Returned before the request reaches the API. The body has message and, when the gateway produced the response, an error object with code throttled; a per-key or per-address 429 has message only and a Retry-After header. Back off, honour Retry-After when present, and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"},"example":{"message":"Too Many Requests","error":{"type":"gateway_error","code":"throttled","message":"Too Many Requests","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"500":{"description":"Unexpected server error. error.type is error and error.code is not a stable value; treat any 5xx as retryable with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"error","code":"AppError","message":"Internal server error","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}}}}},"/v1/documents/{documentId}/start":{"post":{"operationId":"startDocument","tags":["Documents"],"summary":"Start document processing","description":"Start OCR after the file is uploaded. No request body. Calling it for any document that is already in flight (processing, or in its final steps) returns 202 with status processing. A document that failed with error.code buy_pages can be started again after you buy pages. Page usage is reserved once the real page count is known, after this call returns.","parameters":[{"name":"documentId","in":"path","required":true,"schema":{"type":"string"},"description":"Document ID returned by the create request."}],"responses":{"202":{"description":"Processing started, or the document is already processing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StartResponse"},"example":{"status":"processing"}}}},"400":{"description":"The upload is not ready. error.code is upload_not_found (no file at the upload URL yet) or size_mismatch (the uploaded size differs from size_bytes). error.type is invalid_request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"invalid_request","code":"upload_not_found","message":"Uploaded document was not found","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"401":{"description":"No Authorization header. Returned by the gateway before the request reaches the API. The body has message and, on newer deployments, an error object with code unauthorized; if error is absent, rely on the HTTP status and message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"},"example":{"message":"Unauthorized","error":{"type":"gateway_error","code":"unauthorized","message":"Unauthorized","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"403":{"description":"The Authorization header is present but the key is unknown, malformed, or revoked. Returned by the gateway with HTTP 403, not 401. The body has message and, on newer deployments, an error object with code access_denied; if error is absent, rely on the HTTP status and message. The message text is generated by the gateway and can change. A key that was revoked in the last 60 seconds can still be accepted because the gateway caches key checks for up to 60 seconds. A request to a route that does not exist also returns 403, with code missing_authentication_token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"},"example":{"message":"User is not authorized to access this resource with an explicit deny in an identity-based policy","error":{"type":"gateway_error","code":"access_denied","message":"User is not authorized to access this resource with an explicit deny in an identity-based policy","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"404":{"description":"No document with this ID exists for the account. error.code is document_not_found and error.type is not_found. IDs that belong to another account, or to a deleted document, also return 404.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"not_found","code":"document_not_found","message":"Document not found","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"409":{"description":"The document cannot be started from its current state. error.code is document_not_startable and error.type is conflict. Only a document that is awaiting upload, or that failed with buy_pages, can be started. A document already processing returns 202.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"conflict","code":"document_not_startable","message":"Document cannot be started from its current state","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"429":{"description":"Two different 429 responses are possible. (1) The handler's concurrency_limit: too many of the account's documents are processing; the body is the ErrorResponse envelope with error.type rate_limit, the Retry-After header is present, and the document stays awaiting_upload, so call start again after the delay. (2) A request rate limit: the request was above the per-key or per-address limit on /v1, or the shared throttle of the gateway; the body is the GatewayError shape (an error object with code throttled when the gateway produced it; a per-key or per-address 429 has message only and a Retry-After header), so back off and retry.","headers":{"Retry-After":{"description":"Seconds to wait before calling start again. Present only for concurrency_limit, not for the gateway's throttled.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/ErrorResponse"},{"$ref":"#/components/schemas/GatewayError"}]},"example":{"error":{"type":"rate_limit","code":"concurrency_limit","message":"Too many API documents are processing; retry later","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"500":{"description":"Unexpected server error. error.type is error and error.code is not a stable value; treat any 5xx as retryable with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"error","code":"AppError","message":"Internal server error","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}}}}},"/v1/documents/{documentId}/output":{"get":{"operationId":"getDocumentOutput","tags":["Documents"],"summary":"Get a document output link","description":"Return a presigned download link, valid for one hour, for the extracted text (a UTF-8 text file) or the searchable PDF (for an image, a one-page PDF of the image with a text layer). Available once the status is completed or partial.","parameters":[{"name":"documentId","in":"path","required":true,"schema":{"type":"string"},"description":"Document ID returned by the create request."},{"name":"format","in":"query","required":true,"schema":{"type":"string","enum":["text","pdf"]},"description":"text for plain text, pdf for a searchable PDF."}],"responses":{"200":{"description":"A short-lived presigned output link.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OutputResponse"},"example":{"format":"text","url":"https://fastocr-uploads.s3.amazonaws.com/results/...","expires_at":"2026-10-01T13:00:00.123456+00:00","pages_billed":10,"pages_total":10}}}},"400":{"description":"format is missing or is not text or pdf. error.code is invalid_format and error.type is invalid_request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"invalid_request","code":"invalid_format","message":"format must be text or pdf","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"401":{"description":"No Authorization header. Returned by the gateway before the request reaches the API. The body has message and, on newer deployments, an error object with code unauthorized; if error is absent, rely on the HTTP status and message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"},"example":{"message":"Unauthorized","error":{"type":"gateway_error","code":"unauthorized","message":"Unauthorized","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"403":{"description":"The Authorization header is present but the key is unknown, malformed, or revoked. Returned by the gateway with HTTP 403, not 401. The body has message and, on newer deployments, an error object with code access_denied; if error is absent, rely on the HTTP status and message. The message text is generated by the gateway and can change. A key that was revoked in the last 60 seconds can still be accepted because the gateway caches key checks for up to 60 seconds. A request to a route that does not exist also returns 403, with code missing_authentication_token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"},"example":{"message":"User is not authorized to access this resource with an explicit deny in an identity-based policy","error":{"type":"gateway_error","code":"access_denied","message":"User is not authorized to access this resource with an explicit deny in an identity-based policy","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"404":{"description":"No document with this ID exists for the account. error.code is document_not_found and error.type is not_found. IDs that belong to another account, or to a deleted document, also return 404. A format=pdf request for a document whose searchable PDF could not be generated also returns 404, with error.code output_unavailable; request format=text.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"not_found","code":"document_not_found","message":"Document not found","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"409":{"description":"The document has not completed. error.code is document_not_ready and error.type is conflict. This includes documents that failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"conflict","code":"document_not_ready","message":"Document is not ready","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"429":{"description":"The request was above a request rate limit: the per-key or per-address limit on /v1, or the shared throttle of the gateway. Returned before the request reaches the API. The body has message and, when the gateway produced the response, an error object with code throttled; a per-key or per-address 429 has message only and a Retry-After header. Back off, honour Retry-After when present, and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"},"example":{"message":"Too Many Requests","error":{"type":"gateway_error","code":"throttled","message":"Too Many Requests","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"500":{"description":"Unexpected server error. error.type is error and error.code is not a stable value; treat any 5xx as retryable with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"error","code":"AppError","message":"Internal server error","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}}}}},"/v1/balance":{"get":{"operationId":"getBalance","tags":["Account"],"summary":"Get the page balance","description":"Return the shared Document OCR allowance. Documents on the website and API use the same plan/trial allowance, then shared purchased pages. document_pages is authoritative; legacy free/contract fields remain for compatibility.","responses":{"200":{"description":"The account's current balance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BalanceResponse"},"example":{"object":"balance","pages_remaining":2480,"free_pages_per_month":20,"free_pages_remaining":15,"free_pages_reset_at":"2026-11-01T00:00:00+00:00","contract_pages_per_month":null,"contract_pages_remaining":null,"statement_pages_remaining":0}}}},"401":{"description":"No Authorization header. Returned by the gateway before the request reaches the API. The body has message and, on newer deployments, an error object with code unauthorized; if error is absent, rely on the HTTP status and message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"},"example":{"message":"Unauthorized","error":{"type":"gateway_error","code":"unauthorized","message":"Unauthorized","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"403":{"description":"The Authorization header is present but the key is unknown, malformed, or revoked. Returned by the gateway with HTTP 403, not 401. The body has message and, on newer deployments, an error object with code access_denied; if error is absent, rely on the HTTP status and message. The message text is generated by the gateway and can change. A key that was revoked in the last 60 seconds can still be accepted because the gateway caches key checks for up to 60 seconds. A request to a route that does not exist also returns 403, with code missing_authentication_token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"},"example":{"message":"User is not authorized to access this resource with an explicit deny in an identity-based policy","error":{"type":"gateway_error","code":"access_denied","message":"User is not authorized to access this resource with an explicit deny in an identity-based policy","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"429":{"description":"The request was above a request rate limit: the per-key or per-address limit on /v1, or the shared throttle of the gateway. Returned before the request reaches the API. The body has message and, when the gateway produced the response, an error object with code throttled; a per-key or per-address 429 has message only and a Retry-After header. Back off, honour Retry-After when present, and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"},"example":{"message":"Too Many Requests","error":{"type":"gateway_error","code":"throttled","message":"Too Many Requests","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"500":{"description":"Unexpected server error. error.type is error and error.code is not a stable value; treat any 5xx as retryable with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"error","code":"AppError","message":"Internal server error","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}}}}},"/v1/bank-statements":{"post":{"operationId":"createBankStatement","tags":["Bank Statements"],"summary":"Create a bank statement","description":"Create a statement for a bank statement PDF or image and receive a presigned upload URL. PUT the file to upload_url within one hour with the Content-Type of its file extension: application/pdf for .pdf, image/png for .png, image/jpeg for .jpg and .jpeg, image/webp for .webp. Then poll GET /v1/bank-statements/{statementId}: it starts the statement once the file is uploaded and returns download links when it is completed. You may also call the start endpoint yourself. size_bytes is optional; when you send it, the upload must be exactly that long or the statement fails with invalid_upload (start returns 400 size_mismatch).","parameters":[{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","maxLength":255},"description":"A unique value for this create request, such as a UUID, at most 255 characters. Repeating a request with the same key and the same body within 24 hours returns the original 201 response with the original statement id. If the original upload_url has expired while the statement is still awaiting_upload, the replay carries a fresh upload_url and expires_at. Reusing the key with a different body returns 409 idempotency_key_conflict. The body is compared on filename, size_bytes and external_id. The header name is matched case-insensitively."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatementCreateRequest"},"example":{"filename":"statement-2026-10.pdf","external_id":"ACME-2026-10"}}}},"responses":{"201":{"description":"Statement created. The first request and any idempotent replay both return 201; a replay returns the original body, with a fresh upload_url and expires_at when the original URL has expired and the statement is still awaiting_upload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatementCreateResponse"},"example":{"id":"stmt_3f9c2b1e7d54","status":"awaiting_upload","upload_url":"https://fastocr-uploads.s3.amazonaws.com/uploads/...","expires_at":"2026-10-01T13:00:00.123456+00:00"}}}},"400":{"description":"The request is invalid. error.code is one of: invalid_json (body is not valid JSON or is not a JSON object), missing_idempotency_key (no Idempotency-Key header), invalid_idempotency_key (Idempotency-Key is longer than 255 characters), invalid_filename (filename is missing, not a string, or longer than 255 characters), unsupported_file_type (filename does not end in .pdf, .png, .jpg, .jpeg or .webp), invalid_size_bytes (size_bytes is present but not a positive integer), file_too_large (size_bytes is over 52,428,800 for a PDF or over 20,971,520 for an image), invalid_external_id (external_id is not a string of at most 256 characters). error.type is invalid_request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"invalid_request","code":"unsupported_file_type","message":"Bank statements must be PDF, PNG, JPG, JPEG, or WebP files","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"401":{"description":"No Authorization header. Returned by the gateway before the request reaches the API. The body has message and, on newer deployments, an error object with code unauthorized; if error is absent, rely on the HTTP status and message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"},"example":{"message":"Unauthorized","error":{"type":"gateway_error","code":"unauthorized","message":"Unauthorized","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"403":{"description":"The Authorization header is present but the key is unknown, malformed, or revoked. Returned by the gateway with HTTP 403, not 401. The body has message and, on newer deployments, an error object with code access_denied; if error is absent, rely on the HTTP status and message. The message text is generated by the gateway and can change. A key that was revoked in the last 60 seconds can still be accepted because the gateway caches key checks for up to 60 seconds. A request to a route that does not exist also returns 403, with code missing_authentication_token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"},"example":{"message":"User is not authorized to access this resource with an explicit deny in an identity-based policy","error":{"type":"gateway_error","code":"access_denied","message":"User is not authorized to access this resource with an explicit deny in an identity-based policy","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"409":{"description":"The Idempotency-Key was already used with a different request body. error.code is idempotency_key_conflict and error.type is conflict.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"conflict","code":"idempotency_key_conflict","message":"Idempotency-Key was already used for a different request body","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"429":{"description":"The request was above a request rate limit: the per-key or per-address limit on /v1, or the shared throttle of the gateway. Returned before the request reaches the API. The body has message and, when the gateway produced the response, an error object with code throttled; a per-key or per-address 429 has message only and a Retry-After header. Back off, honour Retry-After when present, and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"},"example":{"message":"Too Many Requests","error":{"type":"gateway_error","code":"throttled","message":"Too Many Requests","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"500":{"description":"Unexpected server error. error.type is error and error.code is not a stable value; treat any 5xx as retryable with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"error","code":"AppError","message":"Internal server error","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"503":{"description":"The bank statement API is temporarily unavailable. error.code is bank_statement_unavailable and error.type is unavailable. Retry later with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"unavailable","code":"bank_statement_unavailable","message":"The bank statement API is temporarily unavailable. Please try again later.","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}}}},"get":{"operationId":"listBankStatements","tags":["Bank Statements"],"summary":"List bank statements","description":"List the account's statements, newest created first. Each page holds up to limit statements when more exist. A page can be short while next_cursor is set, so keep following next_cursor until it is null. The list does not start statements and carries no download links: outputs show only available (and accounts), so GET the single statement for links. An uploaded statement that nobody has polled yet is still awaiting_upload here.","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Maximum number of statements to return for this page, from 1 to 100."},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"},"description":"Opaque cursor from the previous response's next_cursor. A cursor expires 24 hours after it is issued. An expired, malformed, or forged cursor returns 400 invalid_cursor."},{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["awaiting_upload","queued","processing","completed","failed"]},"description":"Return only statements with this public status. Any other value returns 400 invalid_status."},{"name":"updated_since","in":"query","required":false,"schema":{"type":"string","example":"2026-10-01T00:00:00Z"},"description":"Return statements whose last status change, or creation time if the status never changed, is at or after this value. Send an ISO 8601 date or datetime. Z, any offset, and a missing offset all work. A missing offset means UTC. A value that is not a date is never rejected and is compared as text. In a raw query string, write a + offset as %2B, or use Z."}],"responses":{"200":{"description":"A page of statements.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatementListResponse"},"example":{"bank_statements":[{"id":"stmt_3f9c2b1e7d54","status":"completed","external_id":"ACME-2026-10","pages_billed":3,"pages_total":3,"has_transactions":true,"needs_review":false,"coverage":"complete","outputs":{"json":{"available":true},"xlsx":{"available":true},"qbo":{"available":true,"accounts":[1,2]}}}],"next_cursor":null}}}},"400":{"description":"The query is invalid. error.code is one of: invalid_limit (limit is not an integer from 1 to 100), invalid_status (status is not a public status), invalid_cursor (cursor is malformed, forged, or older than 24 hours). error.type is invalid_request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"invalid_request","code":"invalid_limit","message":"limit must be between 1 and 100","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"401":{"description":"No Authorization header. Returned by the gateway before the request reaches the API. The body has message and, on newer deployments, an error object with code unauthorized; if error is absent, rely on the HTTP status and message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"},"example":{"message":"Unauthorized","error":{"type":"gateway_error","code":"unauthorized","message":"Unauthorized","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"403":{"description":"The Authorization header is present but the key is unknown, malformed, or revoked. Returned by the gateway with HTTP 403, not 401. The body has message and, on newer deployments, an error object with code access_denied; if error is absent, rely on the HTTP status and message. The message text is generated by the gateway and can change. A key that was revoked in the last 60 seconds can still be accepted because the gateway caches key checks for up to 60 seconds. A request to a route that does not exist also returns 403, with code missing_authentication_token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"},"example":{"message":"User is not authorized to access this resource with an explicit deny in an identity-based policy","error":{"type":"gateway_error","code":"access_denied","message":"User is not authorized to access this resource with an explicit deny in an identity-based policy","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"429":{"description":"The request was above a request rate limit: the per-key or per-address limit on /v1, or the shared throttle of the gateway. Returned before the request reaches the API. The body has message and, when the gateway produced the response, an error object with code throttled; a per-key or per-address 429 has message only and a Retry-After header. Back off, honour Retry-After when present, and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"},"example":{"message":"Too Many Requests","error":{"type":"gateway_error","code":"throttled","message":"Too Many Requests","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"500":{"description":"Unexpected server error. error.type is error and error.code is not a stable value; treat any 5xx as retryable with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"error","code":"AppError","message":"Internal server error","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"503":{"description":"The bank statement API is temporarily unavailable. error.code is bank_statement_unavailable and error.type is unavailable. Retry later with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"unavailable","code":"bank_statement_unavailable","message":"The bank statement API is temporarily unavailable. Please try again later.","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}}}}},"/v1/bank-statements/{statementId}":{"get":{"operationId":"getBankStatement","tags":["Bank Statements"],"summary":"Get a bank statement","description":"Return the statement's current state. If the status is awaiting_upload and the file has been uploaded, this call starts the statement, so polling after the upload is enough. If the account is at its concurrency limit, the statement is queued, the response carries a Retry-After header, and a later GET tries again. If the uploaded file is over the size limit or does not match size_bytes, the statement fails with invalid_upload. When the status is completed, every available output carries a presigned download url and expires_at (one hour); GET the statement again for fresh links. A statement that failed during processing returns 200 with status failed and an error object. A statement that failed with buy_pages is not started by GET: add pages, then call the start endpoint.","parameters":[{"name":"statementId","in":"path","required":true,"schema":{"type":"string"},"description":"Statement ID returned by the create request."}],"responses":{"200":{"description":"The statement. A queued statement also carries a Retry-After header.","headers":{"Retry-After":{"description":"Seconds to wait before polling again. Present only while the status is queued.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatementStatusResponse"},"examples":{"completed":{"summary":"Completed","value":{"id":"stmt_3f9c2b1e7d54","status":"completed","external_id":"ACME-2026-10","pages_billed":3,"pages_total":3,"has_transactions":true,"needs_review":false,"coverage":"complete","outputs":{"json":{"available":true,"url":"https://fastocr-uploads.s3.amazonaws.com/results/...","expires_at":"2026-10-01T13:00:00.123456+00:00"},"xlsx":{"available":true,"url":"https://fastocr-uploads.s3.amazonaws.com/results/...","expires_at":"2026-10-01T13:00:00.123456+00:00"},"qbo":{"available":true,"accounts":[1,2],"files":[{"account":1,"url":"https://fastocr-uploads.s3.amazonaws.com/results/...","expires_at":"2026-10-01T13:00:00.123456+00:00"},{"account":2,"url":"https://fastocr-uploads.s3.amazonaws.com/results/...","expires_at":"2026-10-01T13:00:00.123456+00:00"}]}}}},"queued":{"summary":"Waiting for a free slot","value":{"id":"stmt_3f9c2b1e7d54","status":"queued","external_id":null,"has_transactions":true}},"failed":{"summary":"Failed during processing","value":{"id":"stmt_3f9c2b1e7d54","status":"failed","external_id":null,"has_transactions":true,"error":{"type":"invalid_request","code":"not_a_bank_statement","message":"This does not look like a bank statement. Upload a bank account statement as a PDF from your online banking and try again. You have not been charged.","retryable":false}}}}}}},"401":{"description":"No Authorization header. Returned by the gateway before the request reaches the API. The body has message and, on newer deployments, an error object with code unauthorized; if error is absent, rely on the HTTP status and message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"},"example":{"message":"Unauthorized","error":{"type":"gateway_error","code":"unauthorized","message":"Unauthorized","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"403":{"description":"The Authorization header is present but the key is unknown, malformed, or revoked. Returned by the gateway with HTTP 403, not 401. The body has message and, on newer deployments, an error object with code access_denied; if error is absent, rely on the HTTP status and message. The message text is generated by the gateway and can change. A key that was revoked in the last 60 seconds can still be accepted because the gateway caches key checks for up to 60 seconds. A request to a route that does not exist also returns 403, with code missing_authentication_token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"},"example":{"message":"User is not authorized to access this resource with an explicit deny in an identity-based policy","error":{"type":"gateway_error","code":"access_denied","message":"User is not authorized to access this resource with an explicit deny in an identity-based policy","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"404":{"description":"No statement with this ID exists for the account. error.code is statement_not_found and error.type is not_found. IDs that belong to another account, or to a deleted statement, also return 404.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"not_found","code":"statement_not_found","message":"Statement not found","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"429":{"description":"The request was above a request rate limit: the per-key or per-address limit on /v1, or the shared throttle of the gateway. Returned before the request reaches the API. The body has message and, when the gateway produced the response, an error object with code throttled; a per-key or per-address 429 has message only and a Retry-After header. Back off, honour Retry-After when present, and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"},"example":{"message":"Too Many Requests","error":{"type":"gateway_error","code":"throttled","message":"Too Many Requests","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"500":{"description":"Unexpected server error. error.type is error and error.code is not a stable value; treat any 5xx as retryable with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"error","code":"AppError","message":"Internal server error","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"503":{"description":"The bank statement API is temporarily unavailable. error.code is bank_statement_unavailable and error.type is unavailable. Retry later with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"unavailable","code":"bank_statement_unavailable","message":"The bank statement API is temporarily unavailable. Please try again later.","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}}}},"delete":{"operationId":"deleteBankStatement","tags":["Bank Statements"],"summary":"Delete a bank statement","description":"Request deletion of the statement and its files. The files and the statement record are removed in the background. GET never returns a deleting status: until the record is gone the statement keeps its last status, and after that GET and DELETE return 404 statement_not_found. Repeating the request while deletion is pending returns 202 again.","parameters":[{"name":"statementId","in":"path","required":true,"schema":{"type":"string"},"description":"Statement ID returned by the create request."}],"responses":{"202":{"description":"Deletion was requested.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteResponse"},"example":{"status":"deleting"}}}},"401":{"description":"No Authorization header. Returned by the gateway before the request reaches the API. The body has message and, on newer deployments, an error object with code unauthorized; if error is absent, rely on the HTTP status and message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"},"example":{"message":"Unauthorized","error":{"type":"gateway_error","code":"unauthorized","message":"Unauthorized","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"403":{"description":"The Authorization header is present but the key is unknown, malformed, or revoked. Returned by the gateway with HTTP 403, not 401. The body has message and, on newer deployments, an error object with code access_denied; if error is absent, rely on the HTTP status and message. The message text is generated by the gateway and can change. A key that was revoked in the last 60 seconds can still be accepted because the gateway caches key checks for up to 60 seconds. A request to a route that does not exist also returns 403, with code missing_authentication_token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"},"example":{"message":"User is not authorized to access this resource with an explicit deny in an identity-based policy","error":{"type":"gateway_error","code":"access_denied","message":"User is not authorized to access this resource with an explicit deny in an identity-based policy","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"404":{"description":"No statement with this ID exists for the account. error.code is statement_not_found and error.type is not_found. IDs that belong to another account, or to a deleted statement, also return 404.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"not_found","code":"statement_not_found","message":"Statement not found","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"409":{"description":"The statement is processing and cannot be deleted. error.code is statement_processing and error.type is conflict.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"conflict","code":"statement_processing","message":"Processing statements cannot be deleted","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"429":{"description":"The request was above a request rate limit: the per-key or per-address limit on /v1, or the shared throttle of the gateway. Returned before the request reaches the API. The body has message and, when the gateway produced the response, an error object with code throttled; a per-key or per-address 429 has message only and a Retry-After header. Back off, honour Retry-After when present, and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"},"example":{"message":"Too Many Requests","error":{"type":"gateway_error","code":"throttled","message":"Too Many Requests","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"500":{"description":"Unexpected server error. error.type is error and error.code is not a stable value; treat any 5xx as retryable with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"error","code":"AppError","message":"Internal server error","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"503":{"description":"The bank statement API is temporarily unavailable. error.code is bank_statement_unavailable and error.type is unavailable. Retry later with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"unavailable","code":"bank_statement_unavailable","message":"The bank statement API is temporarily unavailable. Please try again later.","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}}}}},"/v1/bank-statements/{statementId}/start":{"post":{"operationId":"startBankStatement","tags":["Bank Statements"],"summary":"Start bank statement processing","description":"Start extraction after the file is uploaded. No request body. Optional: polling GET /v1/bank-statements/{statementId} starts the statement for you. Calling it for any statement that is already in flight returns 202 with status processing. A statement that failed with error.code buy_pages can be started again after you add pages. Page usage is reserved once the real page count is known, after this call returns.","parameters":[{"name":"statementId","in":"path","required":true,"schema":{"type":"string"},"description":"Statement ID returned by the create request."}],"responses":{"202":{"description":"Processing started, or the statement is already processing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StartResponse"},"example":{"status":"processing"}}}},"400":{"description":"The upload is not ready. error.code is one of: upload_not_found (no file at the upload URL yet), size_mismatch (the uploaded size differs from size_bytes), file_too_large (the uploaded file is over 52,428,800 bytes for a PDF or 20,971,520 bytes for an image). error.type is invalid_request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"invalid_request","code":"upload_not_found","message":"Uploaded file was not found","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"401":{"description":"No Authorization header. Returned by the gateway before the request reaches the API. The body has message and, on newer deployments, an error object with code unauthorized; if error is absent, rely on the HTTP status and message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"},"example":{"message":"Unauthorized","error":{"type":"gateway_error","code":"unauthorized","message":"Unauthorized","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"403":{"description":"The Authorization header is present but the key is unknown, malformed, or revoked. Returned by the gateway with HTTP 403, not 401. The body has message and, on newer deployments, an error object with code access_denied; if error is absent, rely on the HTTP status and message. The message text is generated by the gateway and can change. A key that was revoked in the last 60 seconds can still be accepted because the gateway caches key checks for up to 60 seconds. A request to a route that does not exist also returns 403, with code missing_authentication_token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"},"example":{"message":"User is not authorized to access this resource with an explicit deny in an identity-based policy","error":{"type":"gateway_error","code":"access_denied","message":"User is not authorized to access this resource with an explicit deny in an identity-based policy","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"404":{"description":"No statement with this ID exists for the account. error.code is statement_not_found and error.type is not_found. IDs that belong to another account, or to a deleted statement, also return 404.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"not_found","code":"statement_not_found","message":"Statement not found","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"409":{"description":"The statement cannot be started from its current state. error.code is statement_not_startable and error.type is conflict. Only a statement that is awaiting upload, or that failed with buy_pages, can be started, and not one that is being deleted. A statement already processing returns 202.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"conflict","code":"statement_not_startable","message":"Statement cannot be started from its current state","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"429":{"description":"Two different 429 responses are possible. (1) The handler's concurrency_limit: too many of the account's statements are processing; the body is the ErrorResponse envelope with error.type rate_limit, the Retry-After header is present, and the statement stays awaiting_upload, so call start again after the delay. (2) A request rate limit: the request was above the per-key or per-address limit on /v1, or the shared throttle of the gateway; the body is the GatewayError shape (an error object with code throttled when the gateway produced it; a per-key or per-address 429 has message only and a Retry-After header), so back off and retry.","headers":{"Retry-After":{"description":"Seconds to wait before calling start again. Present only for concurrency_limit, not for the gateway's throttled.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"anyOf":[{"$ref":"#/components/schemas/ErrorResponse"},{"$ref":"#/components/schemas/GatewayError"}]},"example":{"error":{"type":"rate_limit","code":"concurrency_limit","message":"Too many bank statement jobs are processing; retry later","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"500":{"description":"Unexpected server error. error.type is error and error.code is not a stable value; treat any 5xx as retryable with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"error","code":"AppError","message":"Internal server error","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"503":{"description":"The bank statement API is temporarily unavailable. error.code is bank_statement_unavailable and error.type is unavailable. Retry later with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"unavailable","code":"bank_statement_unavailable","message":"The bank statement API is temporarily unavailable. Please try again later.","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}}}}},"/v1/bank-statements/{statementId}/output":{"get":{"operationId":"getBankStatementOutput","tags":["Bank Statements"],"summary":"Get a bank statement output link","description":"Return a presigned download link, valid for one hour, for one output format. Available once the status is completed. The single statement GET already carries these links for every format; use this endpoint for include=source, schema=3, or one account of an accounting format. The accounting formats (qbo, quickbooks_csv, qfx, ofx) have one file per account: when the statement has several accounts, account is required.","parameters":[{"name":"statementId","in":"path","required":true,"schema":{"type":"string"},"description":"Statement ID returned by the create request."},{"name":"format","in":"query","required":true,"schema":{"type":"string","enum":["json","csv","xlsx","qbo","quickbooks_csv","qfx","ofx"]},"description":"json for the structured data, csv, xlsx for a spreadsheet, qbo, quickbooks_csv, qfx or ofx for accounting software."},{"name":"account","in":"query","required":false,"schema":{"type":"integer","minimum":1},"description":"The 1-based account number, for the accounting formats only. Required when the statement has several accounts. Any other format returns 400 invalid_account."},{"name":"schema","in":"query","required":false,"schema":{"type":"string","enum":["3"]},"description":"Send 3 to require the schema 3 document. A statement without schema 3 output returns 404 output_not_found. Any other value returns 400 invalid_schema."},{"name":"include","in":"query","required":false,"schema":{"type":"string","enum":["source"]},"description":"source adds the source evidence to format=json. Any other value, or include with another format, returns 400 invalid_include."}],"responses":{"200":{"description":"A short-lived presigned output link.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatementOutputResponse"},"example":{"format":"xlsx","schema":"3","url":"https://fastocr-uploads.s3.amazonaws.com/results/...","expires_at":"2026-10-01T13:00:00.123456+00:00","pages_billed":3,"pages_total":3}}}},"400":{"description":"The query is invalid. error.code is one of: invalid_format (format is missing or is not one of the listed formats), invalid_schema (schema is not 3), invalid_include (include is not source, or is used with a format other than json), invalid_account (account is not a positive integer, or is used with a format that is not an accounting format), account_required (the statement has several accounts for this accounting format and account is missing). error.type is invalid_request.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"invalid_request","code":"invalid_format","message":"format must be json, csv, xlsx, qbo, quickbooks_csv, qfx, or ofx","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"401":{"description":"No Authorization header. Returned by the gateway before the request reaches the API. The body has message and, on newer deployments, an error object with code unauthorized; if error is absent, rely on the HTTP status and message.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"},"example":{"message":"Unauthorized","error":{"type":"gateway_error","code":"unauthorized","message":"Unauthorized","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"403":{"description":"The Authorization header is present but the key is unknown, malformed, or revoked. Returned by the gateway with HTTP 403, not 401. The body has message and, on newer deployments, an error object with code access_denied; if error is absent, rely on the HTTP status and message. The message text is generated by the gateway and can change. A key that was revoked in the last 60 seconds can still be accepted because the gateway caches key checks for up to 60 seconds. A request to a route that does not exist also returns 403, with code missing_authentication_token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"},"example":{"message":"User is not authorized to access this resource with an explicit deny in an identity-based policy","error":{"type":"gateway_error","code":"access_denied","message":"User is not authorized to access this resource with an explicit deny in an identity-based policy","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"404":{"description":"error.code is statement_not_found (no statement with this ID exists for the account, or it was deleted) or output_not_found (the statement has no such output: the format was not produced, the account does not exist, or schema 3 was requested for a statement without it). error.type is not_found.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"not_found","code":"statement_not_found","message":"Statement not found","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"409":{"description":"The statement has not completed. error.code is statement_not_ready and error.type is conflict. This includes statements that failed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"conflict","code":"statement_not_ready","message":"Statement is not ready","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"429":{"description":"The request was above a request rate limit: the per-key or per-address limit on /v1, or the shared throttle of the gateway. Returned before the request reaches the API. The body has message and, when the gateway produced the response, an error object with code throttled; a per-key or per-address 429 has message only and a Retry-After header. Back off, honour Retry-After when present, and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GatewayError"},"example":{"message":"Too Many Requests","error":{"type":"gateway_error","code":"throttled","message":"Too Many Requests","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"500":{"description":"Unexpected server error. error.type is error and error.code is not a stable value; treat any 5xx as retryable with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"error","code":"AppError","message":"Internal server error","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}},"503":{"description":"The bank statement API is temporarily unavailable. error.code is bank_statement_unavailable and error.type is unavailable. Retry later with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"},"example":{"error":{"type":"unavailable","code":"bank_statement_unavailable","message":"The bank statement API is temporarily unavailable. Please try again later.","request_id":"c6af9ac6-7b61-11e6-9a41-93e8deadbeef"}}}}}}}}},"components":{"securitySchemes":{"BearerAuth":{"type":"http","scheme":"bearer","description":"Send an API key as Authorization: Bearer <key>. Keys start with fok_live_. Any signed-in FastOCR account can create an API key and start right away. Sign in and create an API key in the developer console at https://fastocr.org/developers/keys. You accept the API terms (https://fastocr.org/api-terms) with your first key. Each account gets 20 free pages per month, shared with PDFs on the website. When you need more than the free pages, buy a Page Pack on the pricing page at https://fastocr.org/pricing?from=api#packs. A document that runs out of pages fails with buy_pages. After you buy pages, call /start again on the same document."}},"schemas":{"DocumentCreateRequest":{"type":"object","required":["filename","size_bytes"],"properties":{"filename":{"type":"string","minLength":1,"description":"The original filename, at most 255 characters. It must end in .pdf, .png, .jpg, .jpeg, .gif, .webp or .bmp (case-insensitive); other file types return 400 unsupported_file_type. The extension sets the file type and the Content-Type of the upload.","maxLength":255},"size_bytes":{"type":"integer","format":"int64","minimum":1,"maximum":1073741824,"description":"Exact size of the file in bytes. The maximum is 1 GiB for a PDF and 20 MiB (20,971,520 bytes) for an image. Start returns 400 size_mismatch if the uploaded file differs."},"sha256":{"type":"string","description":"Optional. The SHA-256 digest of the file as base64 of the 32 raw digest bytes (44 characters), the encoding S3 uses for x-amz-checksum-sha256. Any other value returns 400 invalid_sha256, including hex digests. The digest is attached to the presigned upload URL, so S3 rejects an upload whose content does not match. The value is part of the idempotency body comparison.","pattern":"^[A-Za-z0-9+/]{43}=$","minLength":44,"maxLength":44},"external_id":{"type":"string","nullable":true,"description":"Optional value of your own, at most 256 characters, stored with the document and returned as external_id. Not required to be unique. A value that is not a string of at most 256 characters returns 400 invalid_external_id.","maxLength":256}}},"CreateResponse":{"type":"object","required":["id","status","upload_url","expires_at"],"properties":{"id":{"type":"string","description":"Document ID (a UUID)."},"status":{"type":"string","enum":["awaiting_upload"]},"upload_url":{"type":"string","format":"uri","description":"Presigned S3 PUT URL. Send the file with the Content-Type of its extension (application/pdf for a PDF; see createDocument for images)."},"expires_at":{"type":"string","format":"date-time","description":"One hour after the upload URL was issued; UTC with a +00:00 offset. A replay after expiry returns a fresh value while the document is awaiting_upload."}}},"StartResponse":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["processing"]}}},"DeleteResponse":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["deleting"],"description":"Only ever returned by DELETE. GET does not return this value."}}},"OutputResponse":{"type":"object","required":["format","url","expires_at"],"properties":{"format":{"type":"string","enum":["text","pdf"]},"url":{"type":"string","format":"uri","description":"Presigned download URL, valid for one hour."},"expires_at":{"type":"string","format":"date-time"},"pages_billed":{"type":"integer","minimum":0},"pages_total":{"type":"integer","minimum":0}}},"DocumentStatusResponse":{"type":"object","required":["id","status","external_id"],"properties":{"id":{"type":"string"},"status":{"type":"string","enum":["awaiting_upload","processing","completed","partial","failed"],"description":"awaiting_upload: created, not started. processing: running. completed: done. partial: done, but the account's remaining pages covered only the first pages_billed pages of the document (is_truncated is true). failed: see error. There is no deleting status."},"external_id":{"type":"string","nullable":true},"pages_billed":{"type":"integer","minimum":0,"description":"Pages charged against the account's page allowance. Present once pages are reserved."},"pages_total":{"type":"integer","minimum":0,"description":"Pages in the uploaded PDF (1 for an image). Present once the page count is known."},"pages_processed":{"type":"integer","minimum":0,"description":"Pages processed, when reported."},"is_truncated":{"type":"boolean","description":"True when fewer pages were processed than the PDF has. Present once set by processing."},"outputs":{"$ref":"#/components/schemas/DocumentOutputs"},"error":{"$ref":"#/components/schemas/JobError"}}},"DocumentOutputs":{"type":"object","properties":{"text":{"$ref":"#/components/schemas/OutputAvailability"},"pdf":{"$ref":"#/components/schemas/OutputAvailability"}}},"OutputAvailability":{"type":"object","required":["available"],"properties":{"available":{"type":"boolean"}}},"DocumentListResponse":{"type":"object","required":["documents","next_cursor"],"properties":{"documents":{"type":"array","items":{"$ref":"#/components/schemas/DocumentStatusResponse"}},"next_cursor":{"type":"string","nullable":true,"description":"Pass as cursor to read the next page. Null when there are no more pages. The next page starts right after the last document returned."}}},"ErrorResponse":{"type":"object","required":["error"],"properties":{"error":{"$ref":"#/components/schemas/Error"}}},"Error":{"type":"object","required":["type","code","message","request_id"],"properties":{"type":{"type":"string","description":"invalid_request (400), not_found (404), conflict (409), rate_limit (429), or error (500). New values may be added."},"code":{"type":"string","description":"Stable machine-readable code, see the 4xx responses on each operation. New codes may be added; do not branch on message."},"message":{"type":"string"},"request_id":{"type":"string","description":"Include this when contacting support."}}},"GatewayError":{"type":"object","required":["message"],"additionalProperties":true,"properties":{"message":{"type":"string"},"error":{"$ref":"#/components/schemas/GatewayErrorDetail"}},"description":"Gateway-generated body for 401, 403, and gateway 429 responses. message is always present. error is present on newer deployments; if it is absent, rely on the HTTP status and message. Other properties may appear."},"GatewayErrorDetail":{"type":"object","required":["type","code","message","request_id"],"properties":{"type":{"type":"string","description":"gateway_error today. New values may be added."},"code":{"type":"string","description":"unauthorized (401), access_denied (403, unknown, malformed, or revoked key), throttled (429), missing_authentication_token (403 on a route that does not exist, only when the request has no Authorization header; the same request with a Bearer key returns 403 gateway_error), or gateway_error for anything else. New codes may be added; handle an unknown code by the HTTP status."},"message":{"type":"string","description":"Same text as the top-level message."},"request_id":{"type":"string","description":"Include this when contacting support."}}},"JobError":{"type":"object","required":["type","code","message","retryable"],"description":"Present when status is failed. Branch on code; new codes may be added, and an unknown code should be handled as processing_failed.","properties":{"type":{"type":"string","description":"insufficient_credits, invalid_request, rate_limit, or processing_error. New values may be added."},"code":{"type":"string","description":"buy_pages (the account is out of pages; buy API pages at https://fastocr.org/developers/keys, then call start again on the same document), password_protected, unreadable_document, no_text_detected, corrupted_file (the image or file could not be decoded), image_too_large (the image is too large or too high-resolution to read; upload a smaller copy), too_many_pages, empty_file, timeout, out_of_memory, or processing_failed. Statements add: unreadable_statement, not_a_bank_statement, unsupported_statement_type, scan_limit_reached, statement_pages_unavailable (nothing was charged; try again), and invalid_upload (the uploaded file is over the size limit or does not match size_bytes; create a new statement and upload again)."},"message":{"type":"string"},"retryable":{"type":"boolean","description":"True for timeout, out_of_memory, statement_pages_unavailable, and processing_failed."}}},"BalanceResponse":{"type":"object","required":["object","pages_remaining","free_pages_per_month","free_pages_remaining","free_pages_reset_at","contract_pages_per_month","contract_pages_remaining","statement_pages_remaining"],"properties":{"object":{"type":"string","enum":["balance"]},"pages_remaining":{"type":"integer","description":"Pages left in all Page Packs. They also work on the website."},"free_pages_per_month":{"type":"integer","description":"Legacy compatibility breakdown. Use document_pages for the shared document allowance. Free pages every account gets each month, shared with PDFs on the website."},"free_pages_remaining":{"type":"integer","description":"Legacy compatibility breakdown. Use document_pages for the shared document allowance. Free pages left this month."},"free_pages_reset_at":{"type":"string","format":"date-time","description":"Legacy compatibility breakdown. Use document_pages for the shared document allowance. When the monthly pages reset, in UTC."},"contract_pages_per_month":{"type":"integer","nullable":true,"description":"Legacy compatibility breakdown. Use document_pages for the shared document allowance. Pages a month under an API contract; null without one."},"contract_pages_remaining":{"type":"integer","nullable":true,"description":"Legacy compatibility breakdown. Use document_pages for the shared document allowance. Contract pages left this month; null without a contract."},"statement_pages_remaining":{"type":"integer","description":"Bank statement pages left, shared with the website, including the free statement pages a new account receives on its first statement. Document OCR does not spend them."},"document_pages":{"$ref":"#/components/schemas/DocumentPagesBalance"}}},"DocumentPagesBalance":{"type":"object","description":"Authoritative Document OCR allowance shared by website and API. Use this summary instead of adding legacy free/contract fields.","required":["monthly_limit","monthly_used","monthly_remaining","purchased_remaining","purchased_expires_at","total_remaining","is_unlimited","enforced","resets_at"],"properties":{"monthly_limit":{"type":"integer","nullable":true,"description":"Monthly document allowance; null for an unlimited tier. A nominal finite limit can remain during an uncapped grace period."},"monthly_used":{"type":"integer","description":"Monthly document pages charged across website and API combined."},"monthly_remaining":{"type":"integer","nullable":true,"description":"Monthly pages left; null when usage is uncapped."},"purchased_remaining":{"type":"integer","description":"Unspent purchased document pages shared by website and API."},"purchased_expires_at":{"type":"string","format":"date-time","nullable":true,"description":"Earliest expiry among purchased pages that still have pages left; null when none of them expire."},"total_remaining":{"type":"integer","nullable":true,"description":"Monthly remaining plus purchased remaining; null when uncapped. Check is_unlimited."},"is_unlimited":{"type":"boolean","description":"True for an unlimited tier or an uncapped grace period."},"enforced":{"type":"boolean","description":"Whether the monthly document allowance currently caps processing."},"resets_at":{"type":"string","format":"date-time","description":"Next monthly usage reset in UTC. Paid plans reset on their monthly billing date; free accounts reset on the 1st."}}},"StatementCreateRequest":{"type":"object","required":["filename"],"properties":{"filename":{"type":"string","minLength":1,"maxLength":255,"description":"The original filename, at most 255 characters. It must end in .pdf, .png, .jpg, .jpeg or .webp (case-insensitive); other file types return 400 unsupported_file_type. The extension sets the file type and the Content-Type of the upload."},"size_bytes":{"type":"integer","format":"int64","minimum":1,"maximum":52428800,"description":"Optional. Exact size of the file in bytes. The maximum is 50 MiB (52,428,800 bytes) for a PDF and 20 MiB (20,971,520 bytes) for an image. When present, an upload of a different size fails the statement with invalid_upload (start returns 400 size_mismatch). When absent, the real size of the upload is checked against the same limits."},"external_id":{"type":"string","nullable":true,"maxLength":256,"description":"Optional value of your own, at most 256 characters, stored with the statement and returned as external_id. Not required to be unique. A value that is not a string of at most 256 characters returns 400 invalid_external_id."}}},"StatementCreateResponse":{"type":"object","required":["id","status","upload_url","expires_at"],"properties":{"id":{"type":"string","description":"Statement ID."},"status":{"type":"string","enum":["awaiting_upload"]},"upload_url":{"type":"string","format":"uri","description":"Presigned S3 PUT URL. Send the file with the Content-Type of its extension (application/pdf for a PDF; see createBankStatement for images)."},"expires_at":{"type":"string","format":"date-time","description":"One hour after the upload URL was issued; UTC with a +00:00 offset. A replay after expiry returns a fresh value while the statement is awaiting_upload."}}},"StatementOutputResponse":{"type":"object","required":["format","url","expires_at"],"properties":{"format":{"type":"string","enum":["json","csv","xlsx","qbo","quickbooks_csv","qfx","ofx"]},"schema":{"type":"string","enum":["3"],"description":"Present when the statement has schema 3 output."},"url":{"type":"string","format":"uri","description":"Presigned download URL, valid for one hour."},"expires_at":{"type":"string","format":"date-time"},"pages_billed":{"type":"integer","minimum":0},"pages_total":{"type":"integer","minimum":0}}},"StatementFileLink":{"type":"object","required":["url","expires_at"],"properties":{"url":{"type":"string","format":"uri","description":"Presigned download URL, valid for one hour."},"expires_at":{"type":"string","format":"date-time","description":"When the link expires; UTC with a +00:00 offset."}}},"StatementOutputAvailability":{"type":"object","required":["available"],"description":"A format that is not listed was not produced for this statement. url and expires_at appear only on the single statement GET of a completed statement.","properties":{"available":{"type":"boolean"},"url":{"type":"string","format":"uri","description":"Presigned download URL, valid for one hour."},"expires_at":{"type":"string","format":"date-time","description":"When the link expires; UTC with a +00:00 offset."}}},"StatementAccountingOutput":{"type":"object","required":["available","accounts"],"description":"An accounting format has one file per account.","properties":{"available":{"type":"boolean"},"accounts":{"type":"array","items":{"type":"integer","minimum":1},"description":"The 1-based account numbers that have a file in this format."},"files":{"type":"array","items":{"$ref":"#/components/schemas/StatementAccountFile"},"description":"One download link per account. Present only on the single statement GET of a completed statement."}}},"StatementAccountFile":{"type":"object","required":["account","url","expires_at"],"properties":{"account":{"type":"integer","minimum":1},"url":{"type":"string","format":"uri","description":"Presigned download URL, valid for one hour."},"expires_at":{"type":"string","format":"date-time","description":"When the link expires; UTC with a +00:00 offset."}}},"StatementOutputs":{"type":"object","properties":{"json":{"$ref":"#/components/schemas/StatementOutputAvailability"},"csv":{"$ref":"#/components/schemas/StatementOutputAvailability"},"xlsx":{"$ref":"#/components/schemas/StatementOutputAvailability"},"qbo":{"$ref":"#/components/schemas/StatementAccountingOutput"},"quickbooks_csv":{"$ref":"#/components/schemas/StatementAccountingOutput"},"qfx":{"$ref":"#/components/schemas/StatementAccountingOutput"},"ofx":{"$ref":"#/components/schemas/StatementAccountingOutput"}}},"StatementStatusResponse":{"type":"object","required":["id","status","external_id","has_transactions"],"properties":{"id":{"type":"string"},"status":{"type":"string","enum":["awaiting_upload","queued","processing","completed","failed"],"description":"awaiting_upload: created, file not seen yet. queued: the file is uploaded but the account is at its concurrency limit; poll again after Retry-After. processing: running. completed: done. failed: see error. There is no deleting status."},"external_id":{"type":"string","nullable":true},"pages_billed":{"type":"integer","minimum":0,"description":"Pages charged against the account's statement pages. Present once pages are reserved."},"pages_total":{"type":"integer","minimum":0,"description":"Pages in the uploaded file. Present once the page count is known."},"has_transactions":{"type":"boolean","description":"False when the statement was read but contains no transactions."},"needs_review":{"type":"boolean","description":"Present when completed. True when the result could not be fully verified and should be checked by a person."},"coverage":{"type":"string","enum":["complete","unconfirmed","partial"],"nullable":true,"description":"Present when completed. Whether every page of the statement was read."},"outputs":{"$ref":"#/components/schemas/StatementOutputs"},"error":{"$ref":"#/components/schemas/JobError"}}},"StatementListResponse":{"type":"object","required":["bank_statements","next_cursor"],"properties":{"bank_statements":{"type":"array","items":{"$ref":"#/components/schemas/StatementStatusResponse"}},"next_cursor":{"type":"string","nullable":true,"description":"Pass as cursor to read the next page. Null when there are no more pages."}}}}},"externalDocs":{"description":"FastOCR API documentation","url":"https://fastocr.org/docs"}}