The Document Parse synchronous API supports up to 100 pages per request, and the asynchronous API supports up to 2,000 pages.
For documents that exceed the synchronous limit, you have two options: split the document into chunks and call the synchronous API for each chunk, or use the asynchronous API. Choose the approach that best fits your workflow.
If you prefer using the synchronous API for large documents (more than 100 pages), you can split the PDF into chunks of up to 100 pages, send each chunk individually, and then merge the results into a single unified response.
This approach gives you fine-grained control over each request, such as customizing options per chunk and handling errors immediately.
For a simpler way to just split a PDF into separate files, see Split a long PDF into shorter PDFs in the Tips & FAQ page.
The script below processes an entire PDF end-to-end. Replace UPSTAGE_API_KEY_INJECT with your API key and ./sample.pdf with your file path.
# pip install pypdf requestsimport requestsimport ioimport reimport jsonimport timefrom pypdf import PdfReader, PdfWriterMAX_RETRIES = 3CHUNK_SIZE = 100API_URL = "https://api.upstage.ai/v1/document-digitization"API_KEY = "UPSTAGE_API_KEY_INJECT"FILE_PATH = "./sample.pdf"REQUEST_OPTIONS = { "model": "document-parse", "output_formats": "['html', 'markdown', 'text']",}def parse_large_pdf(file_path, api_key): headers = {"Authorization": f"Bearer {api_key}"} reader = PdfReader(file_path) total_pages = len(reader.pages) html_parts, markdown_parts, text_parts = [], [], [] all_elements = [] api_value, model_value = "", "" total_usage_pages = 0 element_id_offset = 0 failed_chunks = [] for i in range(0, total_pages, CHUNK_SIZE): end_page = min(i + CHUNK_SIZE, total_pages) writer = PdfWriter() for page_num in range(i, end_page): writer.add_page(reader.pages[page_num]) res_json = _request_with_retry(writer, i, end_page, headers) if res_json is None: failed_chunks.append((i + 1, end_page)) continue if not api_value: api_value = res_json.get("api", "") if not model_value: model_value = res_json.get("model", "") chunk_elements = res_json.get("elements", []) # Regex-based ID remapping: works for API-generated HTML with simple # numeric IDs. If you render the output in a browser, consider # sanitizing the HTML with a proper parser. for el in chunk_elements: old_id = el["id"] new_id = old_id + element_id_offset el["id"] = new_id el["page"] += i if "content" in el and "html" in el["content"]: el["content"]["html"] = re.sub( rf"\bid=['\"]{old_id}['\"]", f"id='{new_id}'", el["content"]["html"], ) all_elements.append(el) chunk_html = res_json.get("content", {}).get("html", "") if chunk_html: def update_html_id(match): return f"id='{int(match.group(1)) + element_id_offset}'" html_parts.append( re.sub(r"\bid=['\"](\d+)['\"]", update_html_id, chunk_html) ) chunk_md = res_json.get("content", {}).get("markdown", "") if chunk_md: markdown_parts.append(chunk_md) chunk_text = res_json.get("content", {}).get("text", "") if chunk_text: text_parts.append(chunk_text) total_usage_pages += res_json.get("usage", {}).get("pages", 0) element_id_offset += len(chunk_elements) if failed_chunks: print(f"Warning: {len(failed_chunks)} chunk(s) failed: {failed_chunks}") return { "api": api_value, "model": model_value, "content": { "html": "\n".join(html_parts), "markdown": "\n\n".join(markdown_parts), "text": "\n".join(text_parts), }, "elements": all_elements, "usage": {"pages": total_usage_pages}, }def _request_with_retry(writer, chunk_start, chunk_end, headers): with io.BytesIO() as pdf_bytes: writer.write(pdf_bytes) for attempt in range(MAX_RETRIES): pdf_bytes.seek(0) files = { "document": ( f"chunk_{chunk_start}.pdf", pdf_bytes, "application/pdf", ) } try: response = requests.post( API_URL, headers=headers, files=files, data=REQUEST_OPTIONS ) if response.status_code == 200: return response.json() elif response.status_code == 429: if attempt < MAX_RETRIES - 1: wait = 5 * (2**attempt) print( f"Rate limit hit. Waiting {wait}s... " f"({attempt + 1}/{MAX_RETRIES})" ) time.sleep(wait) elif 400 <= response.status_code < 500: raise ValueError( f"Client error ({response.status_code}): {response.text}" ) else: if attempt < MAX_RETRIES - 1: wait = 3 * (2**attempt) print( f"Server error ({response.status_code}). " f"Retrying in {wait}s... ({attempt + 1}/{MAX_RETRIES})" ) time.sleep(wait) except requests.exceptions.RequestException as e: if attempt < MAX_RETRIES - 1: wait = 3 * (2**attempt) print( f"Network error: {e}. Retrying in {wait}s... " f"({attempt + 1}/{MAX_RETRIES})" ) time.sleep(wait) print( f"Failed to process pages {chunk_start + 1}β{chunk_end}. Skipping chunk." ) return Noneif __name__ == "__main__": result = parse_large_pdf(FILE_PATH, API_KEY) with open("result.json", "w", encoding="utf-8") as f: json.dump(result, f, ensure_ascii=False, indent=2) print(f"Done. {result['usage']['pages']} pages processed. Saved to result.json")
The script sends chunks sequentially (one at a time) to stay within rate limits. The Document Parse synchronous API enforces both RPS (Requests per second) and PPM (Pages per minute) limits. Sending multiple chunks concurrently can easily exceed these limits and result in 429 Too Many Requests errors. If you do encounter 429 errors, the script retries with exponential backoff automatically. To raise the limit itself, upgrade your commitment tier by purchasing credits at the credit purchase page.
Users can submit an inference request for a supported document with up to 2,000 pages using the asynchronous inference request API in Document Parse. Upon receiving the request, the API immediately returns a Request ID. After the input file is transferred and validated, it is divided into batches of 10 pages each, and inference is performed on each batch. Users can check the status of each batch in real-time using the response from the asynchronous inference request
API. The inference results of each batch can be downloaded using the provided download_url in the response. The results will be available for downloading for 30 days from the time of the request.
{ "results": [ { "request_id": "e7b1b3b0-1b3b-4b3b-8b3b-1b3b3b3b3b3b", "deleted": true }, { "request_id": "a1c2e3f4-5678-4abc-9def-0123456789ab", "deleted": false, "error": { "code": "request_not_found", "message": "The request could not be found.", "type": "invalid_request_error" } } ]}
Deletion cannot be undone. Download the results you need before deleting.
This endpoint returns 200 whenever the call itself is valid β even if every request ID failed. Check results[].deleted rather than relying on the HTTP status alone. Problems with the call itself, such as sending more than 100 IDs or an empty list, return 4xx.
In Document Parse Async APIs, errors can occur in three different scenarios: request errors, batch-inference errors, or failures to retrieve the request result.
Request errors: This error occurs when the input document cannot be handled by the API or if there's an error during processing, such as image conversion or page split. In case of a request failure, instead of returning a request ID, the API returns an object with error code and message.
The table below shows the error message and code for typical error cases.
error message
http status code
invalid model name
400
no document in the request
400
uploaded document is too large. max allowed size is 500MB
413
unsupported document format
415
Batch inference error: Due to the engine dividing the input document into batches of 10 pages each, each batch may have a different status for model inference. As the input file has already been validated before inference, batch inference errors are most likely caused by internal server errors. When this occurs, the API response for retrieving the result will show a failed status in the batches field with a failure_message.
The below code block shows the schema of batches field in the API response for inference result.
Failures to retrieve the request result:
In the Document Parse Async API, inference results are stored for 30 days, but the download_url is valid for only 15 minutes after it is issued. Each call to requests/{request_id} generates a new pre-signed URL. If you try to access an expired URL, a 403 error may occur. For long documents, generating multiple URLs at once can sometimes fail. To get the result again, re-fetch it using the same request_id β this will issue a fresh download_url without additional credits.
The Async API uses a queue-based batch processing system. Requests are not processed immediately but are placed in a queue and executed when capacity becomes available.
During peak hours or high system load, jobs may remain in the queue for an extended period. The maximum supported waiting and processing time is up to 72 hours.
This is expected behavior and does not indicate an error as long as the job status remains scheduled or started.