# DEEPSOCH OCR API Documentation

One endpoint, an API key, and a JSON response you can use directly.

## Authentication

Every request carries an API key. Create one in your dashboard (https://ocr.run/dashboard/keys). Keys are shown once and stored hashed, so if you lose one, rotate it rather than trying to recover it.

```
Authorization: Bearer sk_live_...
# or
X-API-Key: sk_live_...
```

`sk_test_` keys behave identically and are metered the same way; the prefix exists so a key pasted into a log or a ticket is identifiable at a glance.

## POST /api/v1/ocr

Send a PDF or an image. Returns typed blocks with pixel-space bounding boxes, parsed tables, and markdown.

```bash
curl -X POST https://ocr.run/api/v1/ocr \
  -H "Authorization: Bearer sk_live_..." \
  -F "file=@invoice.pdf"
```

JSON is accepted too, with either `image_base64` (a bare payload or a full data URL) or `image_url`.

```json
{
  "id": "ocr_3fa2b1c8d94e4f2a9b7c1d3e",
  "pages": 2,
  "duration_ms": 1304,
  "truncated": false,
  "blocks": [
    {
        "type": "title",
        "text": "DEEPSOCH AI",
        "bbox": { "x": 74, "y": 91, "width": 515, "height": 49 },
        "bboxNormalised": { "x1": 60, "y1": 52, "x2": 475, "y2": 80 },
        "page": 1
    },
    {
        "type": "table",
        "text": "Item\tQty\nWidget Pro\t10",
        "html": "<table><tr><td>Item</td>...</table>",
        "rows": [["Item", "Qty"], ["Widget Pro", "10"]],
        "bbox": { "x": 76, "y": 428, "width": 1086, "height": 207 },
        "page": 1
    }
  ],
  "markdown": "## DEEPSOCH AI\n\n| Item | Qty |\n| --- | --- |...",
  "usage": { "units": 1, "remaining_today": 486 }
}
```

### Block types

- `title`
- `text`
- `table`
- `list`
- `formula`
- `image`
- `header`
- `footer`
- `caption`
- `unknown`

`unknown` is a real value, not an error: if the model learns a new label, you get the text with a vague type rather than a failed request.

### Coordinates

`bbox` is in pixels against the page as rendered, with a top-left origin. Draw it directly. `bboxNormalised` keeps the model's own 0–999 space so you can re-derive positions against a different rendering without re-running the job.

## POST /api/v1/ocr/stream

The same parse as server-sent events. First text arrives in roughly a third of a second.

```
data: {"type":"meta","id":"ocr_...","pageCount":2}
data: {"type":"page","page":1}
data: {"type":"delta","delta":"<|det|>","page":1}
data: {"type":"delta","delta":"title","page":1}
...
data: {"type":"page_retry","page":1}
...
data: {"type":"result","result":{ ...same shape as above... }}
data: [DONE]
```

Deltas split mid-token: a single bounding box arrives across roughly eight of them, so accumulate the text for a live view and wait for the single `result` event for structure.

`page_retry` means that page is being read again, and the deltas already sent for it are being replaced — the model either declined to read it or got stuck repeating a line. If you are building a live view, discard that page's accumulated text when this arrives; the result never contains the attempt it replaced. It is safe to ignore if you only use `result`.

## POST /api/v1/id/*

Identity documents, read into one JSON shape. Every endpoint returns the same envelope and uses the same keys for the same facts, so adding a second document type does not mean writing a second parser.

| Endpoint | What is verified | Strength |
|---|---|---|
| POST /api/v1/id/aadhaar | Twelve digits, the Verhoeff check digit, and the reserved prefix rule. | checksum |
| POST /api/v1/id/pan | Shape and the holder-type letter. PAN has no published check digit. | format |
| POST /api/v1/id/passport | The machine-readable zone: four check digits and a composite over the whole zone. Any ICAO passport, not only Indian ones. | checksum |
| POST /api/v1/id/gstin | The base-36 check character, the state code, and the PAN embedded at characters 3 to 12. | checksum |
| POST /api/v1/id/driving-licence | The issuing state, the RTO code and the year of issue. The serial has no check digit. | format |
| POST /api/v1/id/voter-id | Three letters and seven digits. The Election Commission has published no check digit. | format |
| POST /api/v1/id/vehicle-rc | The plate's state and RTO, and the chassis number's length and alphabet. No VIN checksum is claimed — that is a North American rule. | format |
| POST /api/v1/id/cheque (also POST /api/v1/id/bank-passbook) | The IFSC, whose fifth character is a reserved zero. | format |
| POST /api/v1/id/abha (also POST /api/v1/id/uan) | Fourteen digits (twelve for UAN). ABDM publishes no check digit, and we do not invent one. | format |
| POST /api/v1/id/ration-card (also POST /api/v1/id/utility-bill, POST /api/v1/id/rent-agreement) | Extraction only. These carry no identifier anyone publishes a format for. | none |
| POST /api/v1/id/incorporation (also POST /api/v1/id/udyam, POST /api/v1/id/startup-india) | A CIN's listing status, state, year and company class, all read out of the number itself. | format |
| POST /api/v1/id/invoice | The line items against the subtotal, the subtotal plus tax against the total, and any GSTIN on the page against its check character. | arithmetic |
| POST /api/v1/id/purchase-order | Vendor and buyer details, shipping/billing addresses, delivery terms, line items with quantities, rates, and totals. | arithmetic |
| POST /api/v1/id/grn | Goods Receipt Notes with PO references, warehouse, and received/accepted/rejected item quantities. | format |
| POST /api/v1/id/detect | Names the document without extracting it, for routing an upload to the right endpoint. | — |

### Custom JSON Schema Extraction

Every business document endpoint accepts an optional `schema` parameter in `multipart/form-data` or JSON request body. When supplied, the OCR output is extracted and type-normalized according to your exact JSON structure:

```bash
curl -X POST https://ocr.run/api/v1/id/invoice \
  -H "Authorization: Bearer sk_live_..." \
  -F "file=@invoice.pdf" \
  -F 'schema={"invoice_number":"string","vendor":"string","total":"number","items":[{"name":"string","qty":"number","price":"number"}]}'
```

Supported schema types: `string`, `number`, `boolean`, `array`, and nested `object`. Numbers (including Indian comma grouping and currency symbols) are normalized into numeric values, and missing fields return `null` without hallucination. Output is returned under `custom_extraction`.

```bash
curl -X POST https://ocr.run/api/v1/id/aadhaar \
  -H "Authorization: Bearer sk_live_..." \
  -F "file=@front.jpg" \
  -F "back=@back.jpg"        # optional
```

Send both sides. An Aadhaar prints the number, the name and the photograph on the front and the address, the guardian and the enrolment number on the back, so a request carrying one side gets half the document. Both are read in a single pass — the address is understood to belong to the person named on the other side. `back` is optional on every endpoint, and both files count against your plan's size limit together.

```json
{
  "document_type": "aadhaar",
  "detected_type": "aadhaar",
  "type_match": true,
  "fields": {
    "document_number": "7699 8509 2964",
    "name": { "full": "Manju Devi", "first": null, "last": null },
    "date_of_birth": null,
    "year_of_birth": 1990,
    "gender": "Female",
    "guardian": { "relation": "husband", "name": "Saroj Thakur" },
    "address": { "line1": "baksidih", "district": "Hazaribag",
                 "state": "Jharkhand", "pincode": "825313", "country": "IN" },
    "aadhaar": { "enrollment_no": "1180/29104/14909", "vid": null }
  },
  // No issued_on or expires_on: an Aadhaar has neither, and a key that can
  // only ever be null is one more thing for a caller to learn to ignore.
  "validation": {
    "is_valid": true,
    "strength": "checksum",
    "checks": [ { "name": "aadhaar_verhoeff", "passed": true, "severity": "error" } ]
  },
  "usage": { "credits": 25, "remaining_today": 975 }
}
```

A valid number is not a genuine document. These checks prove an identifier is well formed. They cannot tell you the card is real, that it has not been revoked, or that it belongs to the person holding it — a forged Aadhaar carrying a correct check digit passes every one of them. Only UIDAI and the equivalent authorities can confirm an identity.

`validation.strength` says how much weight a result carries:

- **checksum** — a check digit had to agree
- **arithmetic** — the document's own numbers agree with each other — an invoice whose lines sum to its subtotal has been read correctly in a dozen places at once
- **format** — a single misread character can still pass
- **none** — the document type carries no identifier that can be verified offline at all — the fields were read, and nothing about them was checked

A field the document does not carry comes back `null`, never a guess. Aadhaar usually prints a year and no full date, which is why `year_of_birth` is separate from `date_of_birth`.

**Passports read the zone.** The number, both dates and the name come from the two machine-readable lines at the foot of the data page rather than the printed fields above them, because that half is the half covered by check digits. Photograph the whole page including the bottom strip: without the zone the endpoint still answers, with `passport.mrz_present: false` and `strength: "format"` to say that nothing was verified.

Each document returns only the keys it can carry. A passport has both an issue and an expiry date; an Aadhaar has neither, and a cheque has no date of birth. A key that is `null` means the document type has that fact and this one did not print it — which is what tells a year-only Aadhaar from one with a full date. A key that is absent means the document type has no such fact at all.

The ID endpoints return no coordinates. They read the page whole rather than as grounded blocks, which is what lets them find a handwritten account number or a machine-read zone. If you need bounding boxes in the pixels of your own page, use `/api/v1/ocr`, which is unchanged.

**A UAN needs its label.** A UAN is twelve digits and so is an Aadhaar number, and a PF slip frequently prints both. So the UAN is only taken from beside a UAN label: an unlabelled run of twelve digits comes back as nothing rather than as somebody's Aadhaar number relabelled.

**A GSTIN describes itself.** The state and the holder's PAN are positions inside the number, not text read off the certificate, so they carry the same guarantee the check character does. Send the PAN printed elsewhere on the page and `gstin_pan_agreement` compares the two for you.

### Query parameters

| Parameter | Effect |
|---|---|
| `mask=true` | Returns `document_number_masked` instead of the full number. Off by default. |
| `store=false` | Skips archiving the document and its result entirely. |
| `retain_days=1` | Deletes the stored document sooner than your plan would. Fractions are allowed. It can only shorten the window. |
| `raw=false` | Omits the OCR text and blocks from the response. |

25 credits per document, whatever the card. Posting the wrong document to an endpoint returns `422 document_type_mismatch` naming what it actually is, and is not charged.

## GET /api/v1/me

## GET /api/v1/usage

Check your headroom before submitting a batch, rather than discovering the limit through a 429 halfway through.

```bash
curl https://ocr.run/api/v1/me -H "Authorization: Bearer sk_live_..."
curl "https://ocr.run/api/v1/usage?days=30" -H "Authorization: Bearer sk_live_..."
```

### Limits

| Plan | Credits/day | Max file | Pages/run | Keys | Rate |
|---|---|---|---|---|---|
| Free | 1,000 | 15 MB | 10 | 0 | 1/page + 1/100 tok |
| Starter | 5,000 | 25 MB | 50 | 3 | 1/page + 1/100 tok |
| Pro | 25,000 | 50 MB | 200 | 10 | 1/page + 1/100 tok |
| Business | 100,000 | 100 MB | 500 | 50 | 1/page + 1/100 tok |

Every response carries `RateLimit-*` (`RateLimit-Limit`, `RateLimit-Remaining`) and `X-Quota-*` headers. Daily counters reset at 00:00 UTC.

### What a run costs

A run is priced per page, in two parts. On the Free plan that is 1 credit per page, plus 1 credit per 100 output tokens, so a one-page receipt costs about 4 credits. A page of dense tables costs more than a page of prose, because it is more work to read. Other plans may charge a different rate, and the table above carries each one.

The per-page part is charged when the document is counted, before the first page is read. That is why a document you cannot afford is refused with `quota_exceeded` up front rather than part way through. The text part is charged as the model produces it, block by block.

If the allowance runs out mid-document the run stops there. On the streaming endpoint you keep the text that had already arrived and the last event is `quota_exceeded`; on `/api/v1/ocr` the answer is a 429 and no partial document. Either way the model is stopped rather than left running, and the pages it never reached are credited back.

Stop reading the response and the run stops: you pay for the pages the model was asked for, including the one in flight, which has already been rendered and sent. The pages it never reached are credited back within a few seconds. A run that fails outright costs nothing.

## Errors

Every response, success or failure, carries `X-Request-Id`. Quote it and a support question has an answer without reproducing anything.

| Status | error | Meaning |
|---|---|---|
| 400 | `no_input` | No file in the request |
| 401 | `unauthorized` | Missing, malformed or revoked key |
| 413 | `file_too_large` | Above your plan's file ceiling |
| 413 | `too_many_pages` | Above your plan's page ceiling |
| 415 | `unsupported_media` | Not a PDF or supported image |
| 429 | `rate_limited` | Too many requests per minute |
| 429 | `quota_exceeded` | Allowance spent, or too small for this document |
| 502 | `empty_output` | The model returned nothing |
| 503 | `upstream_unavailable` | The model is unreachable |
| 504 | `upstream_timeout` | The model took too long |

A run that fails outright is not charged: whatever was reserved for it is credited back. A run that fails after delivering some pages is charged for those pages only. See [What a run costs](#what-a-run-costs).
