> ## Documentation Index
> Fetch the complete documentation index at: https://docs.abbyy.com/llms.txt
> Use this file to discover all available pages before exploring further.

# REST API

Ingest serves the API on port 8080 of the `fineparser-ingest` Service. Submit documents, poll the jobs until they finish, then download the results.

The API has no authentication. Put it behind something that authenticates callers before you expose it. See [Exposing the API](https://docs.abbyy.com/fine-parser/enterprise/best-practices/production-deployment.md#exposing-the-api).

| Endpoint | Purpose |
| - | - |
| `POST /parse` | Submit one or more documents. |
| `GET /jobs/{jobId}` | A job's status. |
| `GET /batch/{batchId}` | The status of every job from one request. |
| `GET /jobs/{jobId}/{name}` | Download a result. |

```bash cURL theme={null}
# Submit
curl -s -F files=@invoice.pdf -F outputTypes=json http://localhost:8080/parse
# {"jobs":[{"file":"invoice.pdf","jobId":"e08f769f-..."}]}

# Poll
curl -s http://localhost:8080/jobs/e08f769f-...
# {"status":"successful","files":["e08f769f-.../invoice.json"],...}

# Download
curl -OJ http://localhost:8080/jobs/e08f769f-.../invoice.json
```

```python Python theme={null}
import pathlib, time

import requests

FP = "http://localhost:8080"

with open("invoice.pdf", "rb") as f:
    r = requests.post(f"{FP}/parse", files={"files": f}, data={"outputTypes": ["json"]})
r.raise_for_status()
job = r.json()["jobs"][0]["jobId"]

while (status := requests.get(f"{FP}/jobs/{job}").json())["status"] in ("pending", "in-progress"):
    time.sleep(3)
if status["status"] != "successful":
    raise RuntimeError(status["failure"])

for entry in status["files"]:
    r = requests.get(f"{FP}/jobs/{entry}")
    r.raise_for_status()
    pathlib.Path(pathlib.Path(entry).name).write_bytes(r.content)
```

## POST /parse

A `multipart/form-data` request.

| Field | Required | Value |
| - | - | - |
| `files` | Yes | A PDF or an image, such as PNG, JPEG, or TIFF. Repeat the field to send several. |
| `outputTypes` | Yes | `doclang`, `json`, `xml`, or `txt`. Repeat the field for several. See [Output formats](https://docs.abbyy.com/fine-parser/enterprise/basics/output-formats.md). |
| `baseProfile` | No | A predefined recognition profile. Requires `userIni`. |
| `userIni` | No | A user profile, as a field or a file. Requires `baseProfile`. See [Recognition settings](https://docs.abbyy.com/fine-parser/enterprise/basics/recognition-settings.md#per-request-profiles). |

The answer is `202 Accepted` with a job ID per file. A request with several files also returns a `batchId`. A file that could not be accepted is listed under `errors`.

```json theme={null}
{"batchId":"c5a6dd90-...","jobs":[{"file":"a.pdf","jobId":"c2f11bb8-..."},{"file":"b.pdf","jobId":"2ab42551-..."}]}
```

`POST /parse` is not idempotent. Submitting again after a timeout may create duplicate jobs.

## GET /jobs/\{jobId}

| Field | Meaning |
| - | - |
| `status` | `pending`, `in-progress`, `successful`, or `failed`. |
| `files` | When `successful`: one entry per result, as `{jobId}/{name}`. |
| `durationSeconds` | When `successful`: seconds from submission to completion. |
| `failure` | When `failed`: `{"reason": ..., "message": ...}`. |

A finished job's status is kept for `orchestrator.jobRetentionSeconds` (24 hours by default). After that this endpoint returns `404`, but the results can still be downloaded.

| `reason` | What to do |
| - | - |
| `profile-rejected` | Fix the profile setting the message names. |
| `ingest-failed` | Check the file opens. It may be corrupt or an unsupported format. |
| `never-started` | Submit it again. |
| `recognition-failed` | Submit it once more. If it fails again, the document is likely unreadable. |
| `finalize-failed` | Check the output volume, then submit again. |
| `timed-out` | See [Unit size and leases](https://docs.abbyy.com/fine-parser/enterprise/concepts/scaling.md#unit-size-and-leases). |

## GET /batch/\{batchId}

Returns `status` for the whole request, and `jobs`, with each job's `jobId`, `file`, and `status` in the same shape as `GET /jobs/{jobId}`. A batch is `successful` only when every job is.

## GET /jobs/\{jobId}/\{name}

Downloads a result. Each `files` entry is already `{jobId}/{name}`, so append it to `/jobs/`. Downloading deletes nothing.

## Errors

Errors are JSON: `{"error":{"code":"...","message":"..."}}`. Branch on `code`.

| `code` | HTTP | Meaning |
| - | - | - |
| `invalid_request` | 400 | A missing or invalid field. The message says which. |
| `no_files_accepted` | 400 | No file could be accepted. `errors` says why for each. |
| `not_found` | 404 | No such job, batch, file, or path. |
| `method_not_allowed` | 405 | Wrong HTTP method. |
| `upload_too_large` | 413 | The request is larger than `ingest.maxUploadSize`. |
| `orchestrator_error` | 502 | An unexpected internal error. |
| `orchestrator_unavailable` | 503 | Briefly unavailable. Retry after `Retry-After` seconds. |

## Limits

| Limit | Setting |
| - | - |
| Request size, all files together | `ingest.maxUploadSize` |
| Form parts per request, files and `outputTypes` together | 5,000 |
| `userIni` size | 1 MiB |

## CORS

To call the API from a browser on another origin, list the origins in `ingest.cors.allowedOrigins`, comma-separated. None are allowed by default.
