> ## 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

Start the container without positional arguments and FineParser runs as an HTTP server on port 8080. It stays up until you stop it. Use this mode when your applications or other services need to send documents to FineParser over the network.

```bash theme={null}
docker run -d --name fineparser -p 8080:8080 --stop-timeout 60 \
  -e FINEPARSER_LICENSE_DATA="$(cat acme.fineparserlicense)" \
  -v fineparser-output:/app/output \
  fineparser
```

Recognition mode, languages, and the license are all set when the container starts, so requests carry no credentials or recognition settings. Results are written to `/app/output`, so mount a volume there or they disappear with the container. See [Configuration](/fine-parser/basics/configuration).

## How a parse works

Parsing is asynchronous. You submit a document and get a job ID back immediately, poll the job until it settles, then download the result. Recognition happens on a single worker inside the container, one document at a time, so the HTTP connection is never held open for the length of a recognition.

<Steps>
  <Step title="Submit">
    `POST /parse` with the document and an output type. The response is `202 Accepted` with the job ID.
  </Step>

  <Step title="Poll">
    `GET /jobs/{id}` until `status` is `successful`, `failed`, or `cancelled`.
  </Step>

  <Step title="Download">
    `GET /jobs/{file}`, where `file` is the path the status response reports. The result streams back as an attachment.
  </Step>
</Steps>

<CodeGroup>
  ```bash cURL theme={null}
  # Submit
  curl -s -X POST http://localhost:8080/parse \
    -F "file=@document.pdf" \
    -F "outputType=doclang"
  # {"jobId":"6c4f1f0e-...","file":"document.pdf"}

  # Poll
  curl -s http://localhost:8080/jobs/6c4f1f0e-...
  # {"status":"successful","file":"6c4f1f0e-.../document.pdf.doclang"}

  # Download
  curl -OJ http://localhost:8080/jobs/6c4f1f0e-.../document.pdf.doclang
  ```

  ```python Python theme={null}
  import time
  import requests

  BASE = "http://localhost:8080"

  with open("document.pdf", "rb") as f:
      submitted = requests.post(
          f"{BASE}/parse",
          files={"file": f},
          data={"outputType": "doclang"},
      )
  submitted.raise_for_status()
  job_id = submitted.json()["jobId"]

  while True:
      job = requests.get(f"{BASE}/jobs/{job_id}").json()
      if job["status"] not in ("pending", "in-progress"):
          break
      time.sleep(1)

  if job["status"] != "successful":
      raise RuntimeError(job.get("error", job["status"]))

  result = requests.get(f"{BASE}/jobs/{job['file']}")
  result.raise_for_status()
  with open("document.doclang", "wb") as out:
      out.write(result.content)
  ```

  ```javascript Node.js theme={null}
  import { openAsBlob } from "node:fs";
  import { writeFile } from "node:fs/promises";
  import { setTimeout as sleep } from "node:timers/promises";

  const BASE = "http://localhost:8080";

  const form = new FormData();
  form.append("file", await openAsBlob("document.pdf"), "document.pdf");
  form.append("outputType", "doclang");

  const submitted = await fetch(`${BASE}/parse`, { method: "POST", body: form });
  if (!submitted.ok) throw new Error(await submitted.text());
  const { jobId } = await submitted.json();

  let job;
  do {
    await sleep(1000);
    job = await (await fetch(`${BASE}/jobs/${jobId}`)).json();
  } while (job.status === "pending" || job.status === "in-progress");

  if (job.status !== "successful") throw new Error(job.error ?? job.status);

  const result = await fetch(`${BASE}/jobs/${job.file}`);
  await writeFile("document.doclang", Buffer.from(await result.arrayBuffer()));
  ```
</CodeGroup>

## Endpoints

Every error response is JSON with an `error` field that describes the problem. Some carry a `code` or `status` field as well, noted below.

### POST /parse

Submit one document. Send it as `multipart/form-data` with a `file` field holding the document and an `outputType` field set to `doclang`, `json`, or `txt`. See [Output formats](/fine-parser/basics/output-formats).

A successful submission returns `202 Accepted`, a `Location` header pointing at the job, and a body with the job ID and the filename FineParser recorded.

```json theme={null}
{"jobId":"6c4f1f0e-...","file":"document.pdf"}
```

| Status | Cause                                                                                                                                                        |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `202`  | Accepted. Poll `/jobs/{id}`.                                                                                                                                 |
| `400`  | `outputType` is missing or not one of `doclang`, `json`, or `txt`, or the request does not contain exactly one part named `file`.                            |
| `402`  | Your plan has no pages left. The body carries `code: out_of_credits`. See [Reaching your limit](/fine-parser/basics/licenses-and-plans#reaching-your-limit). |
| `403`  | The container has no license or the license is not provisioned. The body carries `code: no_instance` or `code: not_provisioned`.                             |
| `413`  | The request body is larger than the upload limit, 1 GiB by default. See [Configuration](/fine-parser/basics/configuration).                                  |
| `503`  | The license server cannot be reached or asked for a retry. The body carries `code: server_unreachable`, `rate_limited`, or `unknown`. Try again later.       |

The licensing check happens before any of the upload is read, so an unlicensed container refuses without spending bandwidth or disk on the request.

### GET /jobs/\{id}

Report a job's state.

```json theme={null}
{"status":"successful","file":"6c4f1f0e-.../document.pdf.doclang"}
```

| `status`      | Meaning                                                                                           |
| ------------- | ------------------------------------------------------------------------------------------------- |
| `pending`     | Queued, waiting for the worker.                                                                   |
| `in-progress` | Being recognized.                                                                                 |
| `successful`  | Finished. `file` is present while the result is on disk.                                          |
| `failed`      | Did not finish. `error` carries the reason.                                                       |
| `cancelled`   | The container shut down while recognizing this document. Terminal. Resubmit if you still want it. |

`file` is the result's path in the form `{jobId}/{name}`. Prefix it with `/jobs/` to get the download URL. The name is the uploaded filename with the output type appended, so `document.pdf` parsed to DocLang becomes `document.pdf.doclang`. The field is absent until the job succeeds, and disappears again if the result is removed from disk.

A job can fail for licensing reasons after it was accepted, because the page count is only known once the document is loaded. In that case `status` is `failed` and `error` says so. Nothing is charged for a failed job.

Returns `404` if there is no such job.

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

Download a result. Use the `file` value from the status response as the path.

The result streams back with a `Content-Type` matching the output format and a `Content-Disposition` attachment header naming the file, so `curl -OJ` saves it under the right name. Ranged requests are not served.

| Status | Cause                                                                                |
| ------ | ------------------------------------------------------------------------------------ |
| `200`  | The result.                                                                          |
| `404`  | No such job, or the name is not this job's result.                                   |
| `409`  | The job has not succeeded. The body carries `status`, and `error` if the job failed. |
| `410`  | The job succeeded but its result is no longer on disk.                               |

### DELETE /jobs/\{id}

Remove a job and its result from disk.

| Status | Cause                                                                               |
| ------ | ----------------------------------------------------------------------------------- |
| `204`  | Deleted.                                                                            |
| `404`  | No such job.                                                                        |
| `409`  | The job is being recognized right now and cannot be deleted. Wait for it to settle. |

### GET /healthz

Report whether the container is licensed and ready to accept documents. Use it as the readiness probe in Kubernetes or your orchestrator. The container image also runs it as its own Docker `HEALTHCHECK`.

```bash theme={null}
curl -s localhost:8080/healthz
# {"code":"ok","ready":true,"telemetry":true}
```

A ready container returns `200`. An unready one returns `503` with `ready: false`, a `code` explaining why, and usually a `detail` message. Readiness reflects licensing only. Telemetry is reported alongside it and never affects it. See [Confirming the license](/fine-parser/basics/configuration#confirming-the-license).

## Results on disk

Every result is written under `/app/output` as `{jobId}/{name}`, next to a small job database. The layout is stable, so you can also collect results straight from the mounted volume instead of downloading them over HTTP.

By default a result stays until you `DELETE` the job or clear it from the volume yourself. Nothing expires by age. If you would rather have FineParser clean up, start the container with `DELETE_ON_DOWNLOAD=true` and each result is removed, along with its job record, as soon as it has been downloaded in full. After that both the status and the download return `404`. An interrupted download leaves the result in place for a retry.

Uploads are held in a separate scratch directory and deleted as soon as recognition finishes, whichever way it went.

## Restarts and shutdown

Jobs are recorded in the job database on the output volume, so they survive a restart as long as the volume does. Pending jobs are picked up again. A job that was in progress when the process died is marked `failed`, because the process cannot know how much of the result was written.

On a clean stop, FineParser stops accepting requests, lets in-flight requests finish, and exits without waiting for the document being recognized. Recognition cannot be interrupted, and waiting for it could hold the shutdown open for minutes. That job is marked `cancelled` and its partial result is removed. Resubmit it if you still want it.

Give the container a stop timeout of 60 seconds, with `--stop-timeout 60` in Docker or `terminationGracePeriodSeconds: 60` in Kubernetes. Docker's default of 10 seconds cuts the drain short and loses the usage telemetry for everything since the last export.

Only one container can use a given output directory at a time. The job database is locked while open, so a second container on the same volume refuses to start. Two containers need two volumes.
