---
url: https://flat.io/developers/docs/api/omr/import.md
description: >-
  Import a PDF of sheet music into Flat with Optical Music Recognition (OMR)
  through a single unified endpoint. Send the file to POST /scores, poll the
  task, and get an editable score.
---

# Auto simple PDF import

The simplest way to run OMR is the regular score import: send a PDF to
[`POST /scores`](https://flat.io/developers/docs/api/reference#tag/Score/operation/createScore) and
Flat imports it as an editable score. This is the **same endpoint** you use for MusicXML and MIDI, so
one code path handles every format. It is the right choice when you just want a score in the Library
and do not need progress updates or a review step.

Unlike MusicXML or MIDI (which import synchronously), OMR takes some time, so a PDF import is handled
as an **asynchronous task**:

1. You start the import with `POST /scores`. The API replies immediately with a **task**.
2. You **poll the task** until it is done.
3. Once done, you retrieve (and optionally export) the newly created score.

In this page:

* [Prerequisites](#prerequisites)
* [Step 1. Start the import](#step-1-start-the-import)
* [Step 2. Poll the task](#step-2-poll-the-task)
* [Step 3. Retrieve the imported score](#step-3-retrieve-the-imported-score)
* [Error handling](#error-handling)
* [Usage and billing](#usage-and-billing)
* [Related](#related)

> Need live progress, an instrument-correction review step, MusicXML-only output, or incremental
> (mobile) capture? Use the [Interactive Jobs API](/api/omr/jobs) instead. New to OMR? Start with the
> [OMR overview](/api/omr/).

## Prerequisites

* **Authentication.** A [Personal Access Token](/api/authentication#personal-access-tokens) works
  nicely to get started quickly. If you are building an OAuth2 app, request the `scores` scope (to
  create the score), the `omr` scope (to run OMR), and the `tasks.readonly` scope (to poll the task).
* **File format.** Only **PDF** (`application/pdf`) files go through OMR.
* **Encoding.** Binary PDF data must be Base64-encoded and sent in the `data` property, with
  `dataEncoding` set to `base64`.
* **Opt in to tasks.** You must set `supportsTasks: true` in the request. Without it, a PDF import is
  rejected.

## Step 1. Start the import

Create the score with
[`POST /scores`](https://flat.io/developers/docs/api/reference#tag/Score/operation/createScore),
providing the Base64-encoded PDF and `supportsTasks: true`:

::: code-group

```bash [curl]
curl -X POST 'https://api.flat.io/v2/scores' \
  -H 'Authorization: Bearer <my_api_personal_access_token>' \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "My imported score",
    "filename": "my-sheet-music.pdf",
    "data": "<base64-encoded-pdf>",
    "dataEncoding": "base64",
    "supportsTasks": true
  }'
```

```js [JavaScript]
import { readFileSync } from 'node:fs';

const res = await fetch('https://api.flat.io/v2/scores', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer <my_api_personal_access_token>',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    title: 'My imported score',
    filename: 'my-sheet-music.pdf',
    data: readFileSync('my-sheet-music.pdf').toString('base64'),
    dataEncoding: 'base64',
    supportsTasks: true,
  }),
});

// A PDF import returns 202 with a task; MusicXML/MIDI return 200 with the score.
const task = await res.json();
```

```python [Python]
import base64, requests

with open("my-sheet-music.pdf", "rb") as f:
    data = base64.b64encode(f.read()).decode()

res = requests.post(
    "https://api.flat.io/v2/scores",
    headers={"Authorization": "Bearer <my_api_personal_access_token>"},
    json={
        "title": "My imported score",
        "filename": "my-sheet-music.pdf",
        "data": data,
        "dataEncoding": "base64",
        "supportsTasks": True,
    },
)

# A PDF import returns 202 with a task; MusicXML/MIDI return 200 with the score.
task = res.json()
```

:::

Because the PDF requires OMR, the API responds with **`202 Accepted`** and a task instead of the
score:

```json
{
  "id": "5e0a59c0f1e6b8000868e0c1",
  "type": "import-omr",
  "state": "created",
  "progress": {
    "percent": 0
  },
  "creationDate": "2026-06-16T09:24:00.000Z"
}
```

Keep the task `id`. You will use it to follow the import.

::: tip Branch on the HTTP status
MusicXML and MIDI imports are processed synchronously and still return **`200 OK`** with the full
score. A PDF import returns **`202 Accepted`** with a task. Have your client branch on the status
code: `200` means the score is ready, `202` means follow the task flow below.
:::

## Step 2. Poll the task

Fetch the task with
[`GET /tasks/{task}`](https://flat.io/developers/docs/api/reference#tag/Task/operation/getTask) until
it completes:

::: code-group

```bash [curl]
curl -H 'Authorization: Bearer <my_api_personal_access_token>' \
  https://api.flat.io/v2/tasks/5e0a59c0f1e6b8000868e0c1
```

```js [JavaScript]
async function waitForTask(taskId, token) {
  while (true) {
    const res = await fetch(`https://api.flat.io/v2/tasks/${taskId}`, {
      headers: { Authorization: `Bearer ${token}` },
    });
    const task = await res.json();
    if (['done', 'error', 'canceled'].includes(task.state)) return task;
    await new Promise((r) => setTimeout(r, 3000)); // poll every few seconds
  }
}
```

```python [Python]
import time, requests

def wait_for_task(task_id, token):
    while True:
        task = requests.get(
            f"https://api.flat.io/v2/tasks/{task_id}",
            headers={"Authorization": f"Bearer {token}"},
        ).json()
        if task["state"] in ("done", "error", "canceled"):
            return task
        time.sleep(3)  # poll every few seconds
```

:::

The task moves through the following states:

| `state`    | Meaning                                                        |
|:-----------|:---------------------------------------------------------------|
| `created`  | The task is queued and waiting to be processed.                |
| `blocked`  | The task is waiting on something else before it can run.       |
| `doing`    | OMR processing is in progress (see `progress.percent`).        |
| `done`     | The score has been imported. The `score` field is set.         |
| `canceled` | The task was canceled and will not complete.                   |
| `error`    | Processing failed. See `result.error`.                         |

`created`, `blocked`, and `doing` are transient; `done`, `canceled`, and `error` are terminal. Stop
polling on any of the three terminal states, not just `done` and `error`, or a canceled task will
loop forever.

The fields you care about while polling:

| Field             | Description                                                              |
|:------------------|:------------------------------------------------------------------------|
| `id`              | Unique identifier of the task.                                           |
| `type`            | `import-omr` for an OMR import.                                          |
| `state`           | Current state of the task (see table above).                            |
| `progress.percent`| Progression of the task, from `0` to `100`.                             |
| `score`           | The unique identifier of the imported score (set once `state` is `done`).|
| `result.error`    | A human-readable error message when `state` is `error`.                 |

A completed task looks like:

```json
{
  "id": "5e0a59c0f1e6b8000868e0c1",
  "type": "import-omr",
  "state": "done",
  "score": "5e0a5b2cf1e6b8000868e0d4",
  "revision": "5e0a5b2cf1e6b8000868e0d5",
  "progress": {
    "percent": 100
  },
  "creationDate": "2026-06-16T09:24:00.000Z",
  "doneDate": "2026-06-16T09:25:12.000Z"
}
```

Poll every few seconds rather than in a tight loop, and respect our [rate limits](/api/rate-limits).

## Step 3. Retrieve the imported score

Once the task is `done`, read the `score` field for the new score identifier, then fetch the full
score details with
[`GET /scores/{score}`](https://flat.io/developers/docs/api/reference#tag/Score/operation/getScore):

::: code-group

```bash [curl]
curl -H 'Authorization: Bearer <my_api_personal_access_token>' \
  https://api.flat.io/v2/scores/5e0a5b2cf1e6b8000868e0d4
```

```js [JavaScript]
const res = await fetch(`https://api.flat.io/v2/scores/${task.score}`, {
  headers: { Authorization: `Bearer ${token}` },
});
const score = await res.json();
```

```python [Python]
score = requests.get(
    f"https://api.flat.io/v2/scores/{task['score']}",
    headers={"Authorization": f"Bearer {token}"},
).json()
```

:::

From there, the score behaves like any other Flat score. You can also **export** it to other formats,
for example **MusicXML**, **MP3**, or **MIDI**, using the
[score export endpoints](https://flat.io/developers/docs/api/reference#tag/Score/operation/createScoreRevisionExport).
Exporting follows the **same asynchronous task pattern** as the import: create the export task, poll
[`GET /tasks/{task}`](https://flat.io/developers/docs/api/reference#tag/Task/operation/getTask), then
download the result from the task's `result.url` once it is `done`.

## Error handling

| Status | When it happens                                                                                       |
|:-------|:-----------------------------------------------------------------------------------------------------|
| `400`  | Bad request, for example a missing `supportsTasks: true`, or an invalid or password-protected PDF.    |
| `402`  | The account is over quota, the OMR feature is not included in the plan, or there are not enough credits. See [Usage and billing](#usage-and-billing).|
| `403`  | The OAuth2 token is missing the `omr` scope (`OMR_SCOPE_REQUIRED`), or the `scores` scope needed to create the score. |

In addition, a task may finish in the `error` state. When that happens, inspect `result.error` for a
description of what went wrong. See the [Errors](/api/errors) page for the general error format, and
the [OMR overview](/api/omr/#errors) for the full list of recognition failure codes.

## Usage and billing

OMR imports consume the **credits of the Flat account that owns the token**, charged per page:
**1 credit is 1 page**, drawn from the same pool as the Flat web, desktop, and mobile apps. With a
Personal Access Token that is your own account; with OAuth2, the account of the user who authorized
your app. Credits come from the account's Flat or Flat for Education subscription allowance first,
then from credit packs bought on
[flat.io/settings/ai-credits](https://flat.io/settings/ai-credits?ref=dev-docs-omr). Out of credits
means a `402`. See [Capabilities and credits](/api/omr/#capabilities-and-credits) for the full
picture, including how to read `remainingCredits` before you start an import, and the
[credits and billing FAQ](/api/omr/faq/credits) for refunds, packs, and volume pricing.

If you need higher volumes or have specific requirements, reach out to us at
<developers@flat.io>. We are happy to discuss the best setup for your use
case.

## Related

* [OMR overview](/api/omr/) - scopes, credits, capabilities, and error codes.
* [Interactive Jobs API](/api/omr/jobs) - progress, the instrument-review step, and MusicXML output.
* [FAQ](/api/omr/faq/) - credits and billing, limits, recognition quality, and commercial use.
* [API Reference: `createScore`](https://flat.io/developers/docs/api/reference#tag/Score/operation/createScore)
  and [`getTask`](https://flat.io/developers/docs/api/reference#tag/Task/operation/getTask).
* [Authentication](/api/authentication).
