Nebul Docs

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:

ParameterTypeRequiredDescription
modelStringYesThe model ID, deepseek-ai/DeepSeek-OCR.
documentObjectYesThe document specification. See below.
pagesString, Integer, or ArrayNoPages to process for multi-page PDFs.
include_image_base64BooleanNoInclude detected image regions with bounding boxes and base64 data. Defaults to false.
document_annotation_promptStringNoReplaces 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
  }
}
FieldTypeDescription
idStringRequest identifier, ocr- prefixed.
modelStringThe model that processed the document.
pagesArrayOne entry per processed page, in order.
pages[].indexIntegerZero-based page number.
pages[].markdownStringThe extracted text, formatted as markdown.
pages[].imagesArrayDetected image regions; populated with include_image_base64: true.
pages[].dimensionsObjectPage width, height, and dpi.
document_annotationString or nullThe annotation result when document_annotation_prompt was set.
usage_info.pages_processedIntegerNumber of pages processed.
usage_info.doc_size_bytesIntegerUploaded document size.
usage.prompt_tokensIntegerInput tokens, per page rendered.
usage.completion_tokensIntegerOutput 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

FormatMIME type
PDFapplication/pdf
PNGimage/png
JPEGimage/jpeg

Errors

StatusCause
400Unsupported format or malformed request.
404File not found ({"error_type": "not_found"} with the offending file_id).
422Validation error: a required field is missing.
500OCR processing failed.
503OCR 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.

On this page