# OCR

Extract text from a PDF or a document image, with no chat model involved. You pay the per-page fee and no token charge. To have a model answer questions about the document instead, attach the file to a Chat Completions request, as described in [Read documents](/en/docs/documents), where kenari reads it for any model as ordinary input tokens. This endpoint is the same paid reading as the `ocr` engine there.

`POST /v1/ocr`

## Request

Send a JSON body with either `file` or `reuse_id`, never both.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `file` | object | one of `file` or `reuse_id` | The document to read. |
| `file.filename` | string | yes, with `file` | The file name. It is echoed back as `name`. |
| `file.file_data` | string | yes, with `file` | A base64 data URL: `data:<type>;base64,<contents>`. A plain http URL is not fetched. |
| `reuse_id` | string | one of `file` or `reuse_id` | A `reuse_id` from an earlier reading on your account. Serves the stored text at no charge. |
| `engine` | string | no | Only `ocr` is accepted. Any other value is refused. |

Accepted types are `application/pdf`, `image/png`, `image/jpeg`, `image/webp`, `image/gif` and `image/tiff`. Two limits apply and either can refuse first: a page limit per document that kenari sets, and a size limit of about 15 MB. A document over either limit is refused, not read and not charged. The refusal message names the page limit when that is the one hit.

## Response

The response has the extracted text and what the reading cost. `pages` is the number of pages actually read, and the charge is computed from it. `cost_micro_idr` is the fee in micro-Rupiah (Rupiah times 1,000,000), and it is `0` when the request only reused a stored reading. `content` holds the extracted text. `confidence` and `low_confidence` mean the same as in a chat response with the `ocr` engine: a mean per-page score, which is `null` when the engine gives no score, and a warning flag, not a guarantee. `reuse_id` lets you read the same document again at no charge, from this endpoint or from a chat request with the `ocr` engine.

```json
{
  "id": "req_8f286cec-075b-4083",
  "pages": 3,
  "cost_micro_idr": 225000000,
  "name": "invoice.pdf",
  "hash": "sha256:a3b6919a...",
  "content": [{"type": "text", "text": "# INVOICE\n..."}],
  "confidence": 0.9892,
  "low_confidence": false,
  "reuse_id": "ocr_8f286cec-075b-4083-a462-fc89170feb4f"
}
```

`reuse_id` is present when the reading was stored. Sending the same document again on the same account returns the stored reading and charges nothing, as long as the first reading was stored and carried a `reuse_id`. Treat any figure read from a scan as unverified until a person has checked it.

## Examples

Replace `invoice.pdf` with your own file. Each example encodes it as base64 first, and the curl example needs `jq`.

### curl

```bash
{ printf 'data:application/pdf;base64,'; base64 < invoice.pdf | tr -d '\n'; } > invoice.dataurl

jq -n --rawfile data invoice.dataurl '{file: {filename: "invoice.pdf", file_data: $data}}' \
  | curl https://kenari.id/v1/ocr \
    -H "Authorization: Bearer $KENARI_API_KEY" \
    -H "Content-Type: application/json" \
    -d @-
```

To read again without paying, send the `reuse_id` instead of the file:

```bash
curl https://kenari.id/v1/ocr \
  -H "Authorization: Bearer $KENARI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reuse_id": "ocr_8f286cec-075b-4083-a462-fc89170feb4f"}'
```

### Python

```python
import base64
import os
import requests

with open("invoice.pdf", "rb") as f:
    encoded = base64.b64encode(f.read()).decode()

response = requests.post(
    "https://kenari.id/v1/ocr",
    headers={"Authorization": f"Bearer {os.environ['KENARI_API_KEY']}"},
    json={
        "file": {
            "filename": "invoice.pdf",
            "file_data": f"data:application/pdf;base64,{encoded}",
        }
    },
)
response.raise_for_status()
result = response.json()

print(result["pages"], result["cost_micro_idr"])
print(result["content"][0]["text"])
```

### JavaScript

```js
import { readFile } from "node:fs/promises";

const encoded = (await readFile("invoice.pdf")).toString("base64");

const response = await fetch("https://kenari.id/v1/ocr", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.KENARI_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    file: {
      filename: "invoice.pdf",
      file_data: `data:application/pdf;base64,${encoded}`,
    },
  }),
});
if (!response.ok) throw new Error(await response.text());
const result = await response.json();

console.log(result.pages, result.cost_micro_idr);
console.log(result.content[0].text);
```

## Billing

Reading is billed per page read, from your balance. Plans do not cover it. kenari checks that your balance can cover the document before it reads anything. If it cannot, the request is refused with `402` and nothing is charged, and a failed reading is not charged either. A reuse is free. See [How billing works](/en/docs/billing) for balance and price dimensions.

## Errors

| Status | Code | When |
| --- | --- | --- |
| 400 | `bad_request` | The body has neither `file` nor `reuse_id`, or both. `file_data` is not a base64 data URL, is empty, or is not valid base64. The type is not accepted. The document is over the size limit or the page limit. `engine` is not `ocr`. The `reuse_id` is unknown for your account. |
| 400 | `bad_request` | The key is restricted to certain models and was not granted **Document reading**, or document reading is switched off. A shared API key sent a `reuse_id` that it did not create. |
| 402 | `insufficient_balance` | Your balance cannot cover the document. The message says that document reading is billed per page from your balance and that plans do not cover it, and it suggests sending the file to a model that reads files. It shows the balance available and the most this document can reserve. |
| 503 | `upstream_error` | The reading failed. You are not charged. Retry. |

A body that is not valid JSON, a request without `Content-Type: application/json`, and a `file` object with a missing or wrong-typed `filename` or `file_data` are rejected before the endpoint runs, with a plain-text body and no `code`: `400` for invalid JSON, `415` for the missing content type, and `422` for a missing or wrong-typed field. See [Errors](/en/docs/errors#plain-text-responses).

A key restricted to certain models needs **Document reading** ticked under **Paid capabilities**. See [Authentication and API keys](/en/docs/authentication). See [Errors](/en/docs/errors) for the other codes.

## See also

- [Read documents](/en/docs/documents)
- [Images and files as input](/en/docs/vision)
- [How billing works](/en/docs/billing)
- [Authentication and API keys](/en/docs/authentication)
