OCR
Extract text from images and PDF documents into markdown with the OCR API
Preview
OCR is in preview: the API surface and behavior may change before general availability.
The OCR API turns images and PDF documents into markdown text, page by page. Send a PNG, JPEG, or PDF and get extracted text back with per-page dimensions and token usage. deepseek-ai/DeepSeek-OCR serves the requests: a 3B language-vision model that reads text in 100+ languages, tables, and formulas.
There are two ways to run it: upload the file with the Files API and OCR it by ID, or call the OCR endpoint directly with a URL or inline base64. The upload flow is the simpler one; the direct flow fits pipelines that already hold the document somewhere reachable.
Every example reads your key from NEBUL_API_KEY; export it first. The Quick Start covers key setup.
Upload, then OCR
POST /v1/files with purpose: "ocr", then POST /v1/files/{file_id}/ocr. The service resolves the file internally, so there is no signed URL to manage.
import os
import httpx
API_URL = "https://api.inference.nebul.io"
HEADERS = {"Authorization": f"Bearer {os.environ['NEBUL_API_KEY']}"}
# Step 1: upload
with open("document.pdf", "rb") as f:
upload = httpx.post(
f"{API_URL}/v1/files",
files={"file": ("document.pdf", f, "application/pdf")},
data={"purpose": "ocr"},
headers=HEADERS,
)
file_id = upload.json()["id"]
# Step 2: OCR
result = httpx.post(
f"{API_URL}/v1/files/{file_id}/ocr",
json={"model": "deepseek-ai/DeepSeek-OCR"},
headers=HEADERS,
timeout=120.0,
).json()
for page in result["pages"]:
print(f"Page {page['index']}: {page['markdown'][:80]}")
# Step 3: clean up
httpx.delete(f"{API_URL}/v1/files/{file_id}", headers=HEADERS)The upload responds with a JSON object whose id you pass to the OCR step:
{
"id": "01a1447f-0553-77e7-a5b4-b43b71265532",
"filename": "invoice.png",
"purpose": "ocr",
"file_size": 7016,
"status": "complete",
"created_at": "2026-10-16T11:35:37.614433Z"
}For a scanned invoice that says INVOICE 2026-0042, the OCR step returns that line as the start of pages[0].markdown. See the Files API page for the full file lifecycle.
Direct: /v1/ocr
POST https://api.inference.nebul.io/v1/ocr takes the document inline. The request body follows the Mistral OCR SDK format:
| Parameter | Type | Required | Description |
|---|---|---|---|
model | String | Yes | The model ID, deepseek-ai/DeepSeek-OCR. |
document | Object | Yes | The document specification. See below. |
pages | String, Integer, or Array | No | Pages to process for multi-page PDFs. |
include_image_base64 | Boolean | No | Include detected image regions with bounding boxes and base64 data. Defaults to false. |
document_annotation_prompt | String | No | Replaces the default extraction with your prompt; the result lands in document_annotation. |
The document field takes three shapes.
A publicly accessible URL, including data: URIs:
{
"type": "document_url",
"document_url": "https://example.com/document.pdf"
}Inline base64, no upload needed:
{
"type": "content",
"document_content": "data:image/png;base64,iVBORw0KGgo..."
}A file ID from a previous upload:
{
"type": "file_id",
"file_id": "01a1447f-0553-77e7-a5b4-b43b71265532",
"org_id": "019aeb71-0f4d-7044-8fb7-d305c40b9980"
}With file_id, provide org_id in the body or via the X-Org-Id header.
A direct call with inline base64:
import base64
import os
import httpx
with open("image.png", "rb") as f:
data_uri = "data:image/png;base64," + base64.b64encode(f.read()).decode()
response = httpx.post(
"https://api.inference.nebul.io/v1/ocr",
json={
"model": "deepseek-ai/DeepSeek-OCR",
"document": {"type": "content", "document_content": data_uri},
},
headers={"Authorization": f"Bearer {os.environ['NEBUL_API_KEY']}"},
timeout=120.0,
)
print(response.json()["pages"][0]["markdown"])Response
Both endpoints return the same shape:
{
"id": "ocr-1792150538",
"model": "deepseek-ai/DeepSeek-OCR",
"pages": [
{
"index": 0,
"markdown": "INVOICE 2026-0042\n\nNebul Technologies B.V.\n\nItem: GPU inference, 1000 hours\n\nTotal: EUR 125.00",
"images": [],
"dimensions": {
"width": 800,
"height": 400,
"dpi": 200
}
}
],
"document_annotation": null,
"usage_info": {
"pages_processed": 1,
"doc_size_bytes": 7016,
"prompt_tokens": 498,
"completion_tokens": 35,
"total_tokens": 533,
"errors": null
},
"usage": {
"prompt_tokens": 498,
"completion_tokens": 35,
"total_tokens": 533
}
}| Field | Type | Description |
|---|---|---|
id | String | Request identifier, ocr- prefixed. |
model | String | The model that processed the document. |
pages | Array | One entry per processed page, in order. |
pages[].index | Integer | Zero-based page number. |
pages[].markdown | String | The extracted text, formatted as markdown. |
pages[].images | Array | Detected image regions; populated with include_image_base64: true. |
pages[].dimensions | Object | Page width, height, and dpi. |
document_annotation | String or null | The annotation result when document_annotation_prompt was set. |
usage_info.pages_processed | Integer | Number of pages processed. |
usage_info.doc_size_bytes | Integer | Uploaded document size. |
usage.prompt_tokens | Integer | Input tokens, per page rendered. |
usage.completion_tokens | Integer | Output tokens, the extracted text. |
Page selection
For multi-page PDFs, pages cuts latency and token cost by processing a subset:
"pages": 0
"pages": "0-2"
"pages": [0, 3, 4]Indices are zero-based and non-negative; a list holds at most 1,000 pages. Omit the field to process every page.
Structured extraction
document_annotation_prompt replaces the default "read everything" instruction with your own. The answer arrives in document_annotation, and pages[].markdown carries the same output:
response = httpx.post(
f"{API_URL}/v1/files/{file_id}/ocr",
json={
"model": "deepseek-ai/DeepSeek-OCR",
"document_annotation_prompt": "Extract the invoice number and total as JSON",
},
headers=HEADERS,
timeout=120.0,
)
print(response.json()["document_annotation"])For the invoice above, this prints:
data:
{
"invoice": "2026-0042",
"total": "EUR 125.00"
}Ask for JSON and parse defensively: the model may prepend text such as data: before the JSON body.
Supported formats
| Format | MIME type |
|---|---|
application/pdf | |
| PNG | image/png |
| JPEG | image/jpeg |
Errors
| Status | Cause |
|---|---|
| 400 | Unsupported format or malformed request. |
| 404 | File not found ({"error_type": "not_found"} with the offending file_id). |
| 422 | Validation error: a required field is missing. |
| 500 | OCR processing failed. |
| 503 | OCR service unavailable. |
Model
Examples on this page use deepseek-ai/DeepSeek-OCR, the default OCR model. The Model Catalog lists any other OCR models with their specs.