Skip to content

Auto simple PDF import

The simplest way to run OMR is the regular score import: send a PDF to POST /scores 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:

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

Prerequisites

  • Authentication. A Personal Access Token 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, providing the Base64-encoded PDF and supportsTasks: true:

bash
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
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
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.

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} until it completes:

bash
curl -H 'Authorization: Bearer <my_api_personal_access_token>' \
  https://api.flat.io/v2/tasks/5e0a59c0f1e6b8000868e0c1
js
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 (task.state === 'done' || task.state === 'error') return task
    await new Promise((r) => setTimeout(r, 3000)) // poll every few seconds
  }
}
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"):
            return task
        time.sleep(3)  # poll every few seconds

The task moves through the following states:

stateMeaning
createdThe task is queued and waiting to be processed.
doingOMR processing is in progress (see progress.percent).
doneThe score has been imported. The score field is set.
errorProcessing failed. See result.error.

The fields you care about while polling:

FieldDescription
idUnique identifier of the task.
typeimport-omr for an OMR import.
stateCurrent state of the task (see table above).
progress.percentProgression of the task, from 0 to 100.
scoreThe unique identifier of the imported score (set once state is done).
result.errorA 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.

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}:

bash
curl -H 'Authorization: Bearer <my_api_personal_access_token>' \
  https://api.flat.io/v2/scores/5e0a5b2cf1e6b8000868e0d4
js
const res = await fetch(`https://api.flat.io/v2/scores/${task.score}`, {
  headers: { Authorization: `Bearer ${token}` },
})
const score = await res.json()
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. Exporting follows the same asynchronous task pattern as the import: create the export task, poll GET /tasks/{task}, then download the result from the task's result.url once it is done.

Error handling

StatusWhen it happens
400Bad request, for example a missing supportsTasks: true, or an invalid or password-protected PDF.
402The account is over quota, the OMR feature is not included in the plan, or there are not enough credits.
403The 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 page for the general error format, and the OMR overview for the full list of recognition failure codes.

Usage and billing

OMR imports consume the credits available on the user's Flat account, charged per page. 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.

Copyright © Tutteo Limited