# Deepread API

> Full DeepRead API reference. All endpoints, auth, request/response formats, blueprints, webhooks, error handling, and code examples for OCR, structured extraction, form filling, and PII redaction.

- Skill: `deepread-tech/deepread-api` (Agent Skill)
- Install (CLI): `npx skillmds@latest add deepread-tech/deepread-api`
- Raw SKILL.md: https://api.skillmd.com/api/skills/deepread-tech/deepread-api/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: deepread-tech (https://skillmd.com/u/deepread-tech)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/deepread-tech/deepread-api

---


# DeepRead API Reference

You are helping a developer integrate DeepRead into their application. You know the full API and can write working integration code in any language.

**Base URL:** `https://api.deepread.tech`
**Auth:** `X-API-Key` header with key from `https://www.deepread.tech/dashboard` or via the device authorization flow (see Agent Authentication below)

---

## Agent Authentication (Device Authorization Flow)

These endpoints let an AI agent obtain an API key without the user ever copy/pasting secrets. Based on OAuth 2.0 Device Authorization Grant (RFC 8628).

### POST /v1/agent/device/code — Request a Device Code

**Auth:** None (public endpoint)
**Content-Type:** `application/json`

```json
{"agent_name": "my-agent"}
```

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `agent_name` | string | No | Display name shown to the user during approval (e.g. "Claude Code", "My CI Bot"). Optional but strongly recommended — without it, the user sees "Unknown Agent". |

**Response (200 OK):**
```json
{
  "device_code": "a7f3c9d2e1b8...",
  "user_code": "HXKP-3MNV",
  "verification_uri": "https://www.deepread.tech/activate",
  "verification_uri_complete": "https://www.deepread.tech/activate?code=HXKP-3MNV",
  "expires_in": 900,
  "interval": 5
}
```

| Field | Description |
|-------|-------------|
| `device_code` | Secret code for polling — never show this to the user |
| `user_code` | Short code the user enters in their browser (format: `XXXX-XXXX`) |
| `verification_uri` | Base URL for manual code entry |
| `verification_uri_complete` | URL with code pre-filled — open this to skip manual entry (preferred) |
| `expires_in` | Seconds until the code expires (default: 900 = 15 minutes) |
| `interval` | Minimum seconds between poll requests |

---

### POST /v1/agent/device/token — Poll for API Key

**Auth:** None (public endpoint)
**Content-Type:** `application/json`

```json
{"device_code": "a7f3c9d2e1b8..."}
```

Poll this endpoint every `interval` seconds after the user has been shown the code.

**Responses:**

| Scenario | `error` field | `api_key` field | Action |
|----------|---------------|-----------------|--------|
| User hasn't acted yet | `"authorization_pending"` | `null` | Wait `interval` seconds, poll again |
| User approved | `null` | `"sk_live_..."` | Save the key, stop polling |
| User denied | `"access_denied"` | `null` | Stop polling, inform user |
| Code expired | `"expired_token"` | `null` | Start over with a new device code |

The response always includes all three fields (`error`, `api_key`, `key_prefix`). Check `api_key != null` to detect success — don't rely on key presence alone.

**Important:**
- The `api_key` is returned **exactly once**. After you retrieve it, the server clears it. Store it immediately.
- The `key_prefix` is a non-secret identifier for the key (useful for display/logging).
- Never show `device_code` or `api_key` to the user.

---

**What happens on the user's side (you don't need to call these):**
- User opens `verification_uri_complete` — the code is pre-filled, no typing needed
- User logs in (or signs up + confirms email for new users)
- User sees your agent name and clicks Approve → redirected to dashboard
- Once approved, the next poll to `/v1/agent/device/token` returns the `api_key`

---

## Processing

### POST /v1/process — Submit a Document

Uploads a document for async processing. Returns immediately with a job ID.

**Auth:** `X-API-Key: YOUR_KEY`
**Content-Type:** `multipart/form-data`

| Parameter | Type | Required | Default | Description |
|-----------|------|----------|---------|-------------|
| `file` | File | Yes | — | PDF, PNG, JPG, or JPEG |
| `pipeline` | string | No | `"standard"` | Accuracy tier: `"standard"` (multi-model consensus) or `"fast"` (single-pass OCR) |
| `searchable_pdf` | string | No | `"false"` | Set `"true"` to also produce a searchable PDF. **Standard tier only.** |
| `schema` | string | No | — | JSON Schema for structured extraction |
| `blueprint_id` | string | No | — | Blueprint UUID (mutually exclusive with schema) |
| `include_images` | string | No | `"true"` | Generate preview images and page data |
| `include_pages` | string | No | `"false"` | Per-page breakdown (auto-enabled when include_images=true) |
| `webhook_url` | string | No | — | HTTPS URL to notify on completion |
| `version` | string | No | — | Pipeline version for reproducibility |

**Note:** Provide `schema` OR `blueprint_id`, not both. Without either, only OCR text is returned.

**Response (200 OK):**
```json
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "queued"
}
```

**Errors:**
| Status | Meaning |
|--------|---------|
| 400 | Invalid schema, unsupported file type, both schema and blueprint_id provided |
| 401 | Invalid or missing API key |
| 413 | File exceeds plan limit (15MB free, 50MB paid) |
| 429 | Monthly page quota exceeded or rate limit hit |

---

### GET /v1/jobs/{job_id} — Get Results

Poll until `status` is `completed` or `failed`. Recommended: wait 5s, then poll every 5-10s with exponential backoff, max 5 minutes.

**Auth:** `X-API-Key: YOUR_KEY`

**Response (completed):**

Every response carries `"schema_version": "dp02"`.

```json
{
  "id": "550e8400-...",
  "status": "completed",
  "schema_version": "dp02",
  "pipeline": "standard",
  "created_at": "2025-01-18T10:30:00Z",
  "completed_at": "2025-01-18T10:32:15Z",
  "document": {
    "page_count": 3,
    "content": {
      "format": "markdown",
      "text": "Full extracted text in markdown",
      "text_preview": "First 500 characters...",
      "text_url": "https://..."
    },
    "layout": {
      "version": "dp.lite.v1",
      "page": {"index": 1},
      "layout": {"blocks": [{"id": "b1", "type": "heading", "content": "INVOICE"}]}
    }
  },
  "extraction": {
    "fields": [
      {"key": "vendor", "value": "Acme Inc", "needs_review": false, "location": {"page": 1}},
      {"key": "total", "value": 1250.00, "needs_review": true, "review_reason": "Outside typical range", "location": {"page": 1}}
    ]
  },
  "pages": [
    {
      "page_number": 1,
      "content": {"format": "markdown", "text": "Page 1 text..."},
      "fields": [],
      "needs_review": false
    }
  ],
  "review": {
    "needs_review": true,
    "quality_score": 0.96,
    "fields_total": 20,
    "fields_needing_review": 1,
    "review_rate": 0.05,
    "flags": [
      {"scope": "document", "severity": "high", "reason": "total outside typical range"}
    ]
  },
  "artifacts": {
    "preview_url": "https://preview.deepread.tech/token123..."
  },
  "webhook": {
    "url": "https://yourapp.com/webhook",
    "delivered": true
  },
  "meta": {
    "engine_version": "1.1.0",
    "cost_breakdown": {}
  }
}
```

**Notes:**
- `document.content.text_url` is provided when full text exceeds 1MB — fetch from this URL instead
- `document.content.text_preview` is always the first 500 characters
- `extraction.fields` is only present if `schema` or `blueprint_id` was provided — a **list** of `{key, value, needs_review, review_reason?, location.page}`
- `pages` is present when `include_pages=true` or `include_images=true`; per-page auto-detected fields are in `pages[].fields[]`
- `document.layout` is a typed object (was the old `result.json_string` blob)
- `artifacts.preview_url` is a shareable link (no auth needed) to the HIL review interface
- Bounding boxes (with `include_markers=true`) are returned under top-level `grounding[]`

**Response (failed):**
```json
{
  "id": "550e8400-...",
  "status": "failed",
  "error": "PDF parsing failed: file may be corrupted"
}
```

**Statuses:** `queued` → `processing` → `completed` or `failed`

---

### GET /v1/preview/{token} — Public Preview (No Auth)

Returns document preview data. Anyone with the token can view — no API key needed. Use for sharing results with stakeholders.

```json
{
  "file_name": "invoice.pdf",
  "status": "completed",
  "created_at": "2025-01-18T10:30:00Z",
  "pages": [
    {
      "page_number": 1,
      "image_url": "https://...",
      "text": "Page text...",
      "hil_flag": false,
      "data": {}
    }
  ],
  "data": {},
  "metadata": {"page_count": 1, "pipeline": "standard", "review_percentage": 0}
}
```

---

### GET /v1/pipelines — List Pipelines (No Auth)

- **standard** — Multi-model consensus (GPT-5 Mini + Gemini 2.5 Flash), dual OCR with LLM judge, ~45-60s
- **fast** — Single-pass OCR for speed and lower cost

Searchable PDF is an **add-on, not a tier**: send `pipeline=standard` + `searchable_pdf=true` to also get a searchable PDF (`artifacts.searchable_pdf_url`). Legacy `pipeline=searchable` still works as an alias for that combination.

---

### Schema: Arrays of Objects

Array-valued fields (`"type": "array"`) can carry any `items.properties` you define — not just `{description, amount}`. Each property in `items.properties` is extracted as its own typed field per row, with your `datapoint_prompt` / `description` passed to the model.

```json
{
  "type": "object",
  "properties": {
    "documents": {
      "type": "array",
      "description": "Every invoice entry on the summary bill.",
      "items": {
        "type": "object",
        "properties": {
          "vendor_name":     {"type": "string"},
          "account_number":  {"type": ["string", "null"]},
          "invoice_date":    {"type": "string", "format": "date"},
          "invoice_total":   {"type": "number"},
          "line_items": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "description": {"type": "string"},
                "amount":      {"type": "number"}
              }
            }
          }
        },
        "required": ["vendor_name", "invoice_total"]
      }
    }
  }
}
```

Notes:

- Nullable types use `"type": ["string", "null"]` (standard JSON Schema).
- Required properties inside `items` are enforced; optional ones default to `null`.
- Deep nesting (array → object → array → object) is supported up to 6 levels.
- The legacy two-field `{description, amount}` shape still works unchanged — existing integrations are unaffected.

---

## Blueprints & Optimizer

Blueprints are optimized, versioned schemas. The optimizer takes your sample documents + expected values and enhances field descriptions for 20-30% accuracy improvement.

### GET /v1/blueprints/ — List Blueprints

**Auth:** `X-API-Key: YOUR_KEY`

Returns all blueprints with active version and accuracy metrics.

### GET /v1/blueprints/{blueprint_id} — Get Blueprint Details

**Auth:** `X-API-Key: YOUR_KEY`

Returns blueprint with all versions, active version schema, and accuracy metrics.

### POST /v1/optimize — Start Optimization

**Auth:** `X-API-Key: YOUR_KEY`

```json
{
  "name": "utility_invoice",
  "description": "Utility bill extraction",
  "document_type": "invoice",
  "initial_schema": {"type": "object", "properties": {...}},
  "training_documents": ["path1.pdf", "path2.pdf"],
  "ground_truth_data": [{"vendor": "Electric Co", "total": 150.00}, ...],
  "target_accuracy": 95.0,
  "max_iterations": 5,
  "max_cost_usd": 10.0
}
```

- `initial_schema` is optional — auto-generated from ground truth if omitted
- Minimum 2 training documents
- `validation_split` (default 0.3) — fraction held out for validation

**Response:**
```json
{
  "job_id": "...",
  "blueprint_id": "...",
  "status": "pending"
}
```

### POST /v1/optimize/resume — Resume Optimization

Resume a failed job or start a new optimization run for an existing blueprint.

### GET /v1/blueprints/jobs/{job_id} — Optimization Job Status

**Auth:** `X-API-Key: YOUR_KEY`

```json
{
  "status": "running",
  "iteration": 2,
  "baseline_accuracy": 68.0,
  "current_accuracy": 88.0,
  "target_accuracy": 95.0,
  "total_cost": 1.82,
  "max_cost_usd": 10.0
}
```

**Statuses:** `pending` → `initializing` → `running` → `completed`, `failed`, or `cancelled`

### GET /v1/blueprints/jobs/{job_id}/schema — Get Optimized Schema

Returns the optimized JSON schema after optimization completes.

### Using a Blueprint

```bash
curl -X POST https://api.deepread.tech/v1/process \
  -H "X-API-Key: YOUR_KEY" \
  -F "file=@invoice.pdf" \
  -F "blueprint_id=660e8400-..."
```

---

## Webhooks

Pass `webhook_url` when submitting a document to get notified on completion.

**Payload sent to your URL:** identical to the `GET /v1/jobs/{job_id}` response above (same dp02 shape). The job identifier is `id` (not `job_id`), and it carries `"schema_version": "dp02"`.

```json
{
  "id": "550e8400-...",
  "status": "completed",
  "schema_version": "dp02",
  "pipeline": "standard",
  "document": {"page_count": 3, "content": {"format": "markdown", "text": "..."}},
  "extraction": {"fields": [{"key": "vendor", "value": "Acme Inc", "needs_review": false, "location": {"page": 1}}]},
  "review": {"needs_review": false, "fields_total": 20, "fields_needing_review": 0, "review_rate": 0.0, "flags": []},
  "artifacts": {"preview_url": "https://preview.deepread.tech/..."},
  "webhook": {"url": "https://yourapp.com/webhook", "delivered": true}
}
```

**Important:**
- Webhooks are **NOT authenticated** — always fetch the canonical result via `GET /v1/jobs/{job_id}` with your API key
- Must be HTTPS
- Return 2xx to confirm delivery
- Delivery is best-effort — use polling as fallback if webhook not received
- Make your endpoint idempotent (may receive duplicates)

---

## Rate Limits

Every response includes these headers:

| Header | Description |
|--------|-------------|
| `X-RateLimit-Limit` | Monthly pages in your plan |
| `X-RateLimit-Remaining` | Pages remaining this cycle |
| `X-RateLimit-Used` | Pages used this cycle |
| `X-RateLimit-Reset` | Unix timestamp when quota resets |

**Plans:**
| Plan | Pages/month | Max file | Per-doc limit | Rate limit |
|------|-------------|----------|---------------|------------|
| Free | 2,000 | 15 MB | 50 pages | 10 req/min |
| Pro ($99/mo) | 50,000 | 50 MB | Unlimited | 100 req/min |
| Scale | 1,000,000 | 50 MB | Unlimited | 500 req/min |

---

## Error Handling

All errors return:
```json
{"detail": "Human-readable error message"}
```

| Status | Meaning |
|--------|---------|
| 400 | Bad request — invalid schema, unsupported file, both schema + blueprint_id |
| 401 | Invalid or missing API key |
| 404 | Job not found |
| 413 | File too large for your plan |
| 429 | Rate limit or monthly quota exceeded |
| 500 | Server error |

**Quota exceeded (429):**
```json
{
  "detail": {
    "error": "page_count_exceeded",
    "message": "Document has 100 pages, exceeds 50-page limit for FREE plan. Upgrade to PRO.",
    "page_count": 100,
    "max_pages": 50,
    "plan": "free"
  }
}
```

**Common failure reasons in jobs:**
- Document issues: corrupted, unreadable, poor scan quality, processing timeout
- Schema issues: invalid JSON Schema, required fields not found
- Plan limits: file too large, too many pages, quota exceeded

---

## Code Examples

### Python

```python
import requests
import time
import json

API_KEY = "sk_live_YOUR_KEY"
BASE = "https://api.deepread.tech"

# Submit document with structured extraction
schema = {
    "type": "object",
    "properties": {
        "vendor": {"type": "string", "description": "Vendor or company name"},
        "total": {"type": "number", "description": "Total amount due"},
        "due_date": {"type": "string", "description": "Payment due date"}
    }
}

with open("invoice.pdf", "rb") as f:
    resp = requests.post(
        f"{BASE}/v1/process",
        headers={"X-API-Key": API_KEY},
        files={"file": f},
        data={"schema": json.dumps(schema)}
    )
job_id = resp.json()["id"]

# Poll with exponential backoff
delay = 5
while True:
    time.sleep(delay)
    result = requests.get(
        f"{BASE}/v1/jobs/{job_id}",
        headers={"X-API-Key": API_KEY}
    ).json()

    if result["status"] in ("completed", "failed"):
        break
    delay = min(delay * 1.5, 30)  # cap at 30s

# Use results
if result["status"] == "completed":
    text = result["document"]["content"]["text"]
    fields = result.get("extraction", {}).get("fields", [])
    for f in fields:
        if f["needs_review"]:
            print(f"REVIEW: {f['key']} = {f['value']} ({f.get('review_reason')})")
        else:
            print(f"OK: {f['key']} = {f['value']}")
```

### JavaScript / Node.js

```javascript
import fs from "fs";

const API_KEY = "sk_live_YOUR_KEY";
const BASE = "https://api.deepread.tech";

// Submit document
const form = new FormData();
form.append("file", fs.createReadStream("invoice.pdf"));
form.append("schema", JSON.stringify({
  type: "object",
  properties: {
    vendor: { type: "string", description: "Vendor or company name" },
    total: { type: "number", description: "Total amount due" }
  }
}));

const { id: jobId } = await fetch(`${BASE}/v1/process`, {
  method: "POST",
  headers: { "X-API-Key": API_KEY },
  body: form
}).then(r => r.json());

// Poll with backoff
let delay = 5000;
let result;
do {
  await new Promise(r => setTimeout(r, delay));
  result = await fetch(`${BASE}/v1/jobs/${jobId}`, {
    headers: { "X-API-Key": API_KEY }
  }).then(r => r.json());
  delay = Math.min(delay * 1.5, 30000);
} while (!["completed", "failed"].includes(result.status));

console.log(result);
```

### cURL

```bash
# Submit with schema
curl -X POST https://api.deepread.tech/v1/process \
  -H "X-API-Key: YOUR_KEY" \
  -F "file=@invoice.pdf" \
  -F 'schema={"type":"object","properties":{"vendor":{"type":"string","description":"Vendor name"},"total":{"type":"number","description":"Total amount"}}}'

# Submit with blueprint
curl -X POST https://api.deepread.tech/v1/process \
  -H "X-API-Key: YOUR_KEY" \
  -F "file=@invoice.pdf" \
  -F "blueprint_id=660e8400-..."

# Get results
curl https://api.deepread.tech/v1/jobs/JOB_ID \
  -H "X-API-Key: YOUR_KEY"

# List blueprints
curl https://api.deepread.tech/v1/blueprints/ \
  -H "X-API-Key: YOUR_KEY"
```

### Agent Device Flow (Python)

```python
import requests
import time
import webbrowser

BASE = "https://api.deepread.tech"

# Step 1: Request a device code
resp = requests.post(f"{BASE}/v1/agent/device/code", json={"agent_name": "my-agent"})
data = resp.json()
device_code = data["device_code"]
uri_complete = data["verification_uri_complete"]
interval = data["interval"]

# Step 2: Open browser with code pre-filled
success = webbrowser.open(uri_complete)
if success:
    print(f"Opened browser: {uri_complete}")
else:
    print(f"Unable to open browser programmatically; please open this URL manually: {uri_complete}")
print("Log in and click Approve. I'll wait here.")

# Step 3: Poll until approved
api_key = None
while True:
    time.sleep(interval)
    resp = requests.post(f"{BASE}/v1/agent/device/token", json={"device_code": device_code})
    result = resp.json()

    if result.get("api_key"):
        api_key = result["api_key"]
        print(f"Got API key: {result['key_prefix']}...")
        break
    elif result.get("error") == "authorization_pending":
        continue
    elif result.get("error") == "access_denied":
        print("User denied the request.")
        break
    elif result.get("error") == "expired_token":
        print("Code expired. Please start over.")
        break

if api_key is None:
    raise SystemExit("Device flow did not complete successfully — no API key obtained.")

# Step 4: Use the key to process documents
with open("invoice.pdf", "rb") as f:
    resp = requests.post(
        f"{BASE}/v1/process",
        headers={"X-API-Key": api_key},
        files={"file": f},
    )
print(resp.json())  # {"id": "...", "status": "queued"}
```

### Agent Device Flow (JavaScript)

```javascript
const fs = require("fs");
const BASE = "https://api.deepread.tech";

// Step 1: Request a device code
const { device_code, verification_uri_complete, interval } = await fetch(
  `${BASE}/v1/agent/device/code`,
  { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ agent_name: "my-agent" }) }
).then(r => r.json());

// Step 2: Open browser with code pre-filled
console.log(`Please open: ${verification_uri_complete}`);
console.log("Log in and click Approve. I'll wait here.");

// Step 3: Poll until approved
let apiKey;
while (true) {
  await new Promise(r => setTimeout(r, interval * 1000));
  const result = await fetch(`${BASE}/v1/agent/device/token`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ device_code }),
  }).then(r => r.json());

  if (result.api_key) {
    apiKey = result.api_key;
    console.log(`Got API key: ${result.key_prefix}...`);
    break;
  } else if (result.error === "authorization_pending") {
    continue;
  } else {
    console.log(`Flow ended: ${result.error}`);
    break;
  }
}

if (!apiKey) {
  throw new Error("Device flow did not complete successfully — no API key obtained.");
}

// Step 4: Use the key
const form = new FormData();
form.append("file", fs.createReadStream("invoice.pdf"));
const job = await fetch(`${BASE}/v1/process`, {
  method: "POST",
  headers: { "X-API-Key": apiKey },
  body: form,
}).then(r => r.json());
console.log(job); // {id: "...", status: "queued"}
```

### Agent Device Flow (cURL)

```bash
# Step 1: Request a device code — save the full response
response=$(curl -s -X POST https://api.deepread.tech/v1/agent/device/code \
  -H "Content-Type: application/json" \
  -d '{"agent_name": "my-agent"}')
device_code=$(echo "$response" | jq -r '.device_code')
verification_uri_complete=$(echo "$response" | jq -r '.verification_uri_complete')
interval=$(echo "$response" | jq -r '.interval')

# Step 2: Open the browser (use the saved URL — code is pre-filled, user clicks Approve)
open "$verification_uri_complete"  # macOS / xdg-open on Linux

# Step 3: Poll for the key (repeat every $interval seconds until api_key is returned)
curl -s -X POST https://api.deepread.tech/v1/agent/device/token \
  -H "Content-Type: application/json" \
  -d "{\"device_code\": \"$device_code\"}"
# → {"error": "authorization_pending"}  (keep polling)
# → {"api_key": "sk_live_...", "key_prefix": "sk_live_abc..."}  (done!)

# Step 4: Use the key
curl -X POST https://api.deepread.tech/v1/process \
  -H "X-API-Key: sk_live_..." \
  -F "file=@invoice.pdf"
```

### Webhook Receiver (Python / Flask)

```python
from flask import Flask, request
import requests

app = Flask(__name__)
API_KEY = "sk_live_YOUR_KEY"

@app.route("/webhook", methods=["POST"])
def handle_webhook():
    payload = request.json
    job_id = payload["id"]  # dp02: webhook uses `id` (was `job_id`)

    # IMPORTANT: Always fetch canonical result from API (webhooks are not authenticated)
    result = requests.get(
        f"https://api.deepread.tech/v1/jobs/{job_id}",
        headers={"X-API-Key": API_KEY}
    ).json()

    # Process result...
    return "", 200  # Return 2xx to confirm delivery
```

---

## Help the Developer

- **No API key yet** → use the device authorization flow (Agent Authentication section) — no copy/paste needed
- **Send a document** → POST /v1/process, show code in their language
- **Structured data** → help write a JSON Schema with descriptive field descriptions
- **Better accuracy** → explain blueprints, help set up optimizer
- **Real-time updates** → set up webhook_url, build receiver endpoint
- **Hitting errors** → check API key, plan limits, file format, schema validity
- **Share results** → use `artifacts.preview_url` from response (no auth needed)
- **Large documents** → use `document.content.text_url` instead of `document.content.text` for docs > 1MB
- **Review workflow** → filter `extraction.fields[]` by `needs_review`, route flagged ones to human review

