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:
- You start the import with
POST /scores. The API replies immediately with a task. - You poll the task until it is done.
- Once done, you retrieve (and optionally export) the newly created score.
In this page:
- Prerequisites
- Step 1. Start the import
- Step 2. Poll the task
- Step 3. Retrieve the imported score
- Error handling
- Usage and billing
- Related
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
scoresscope (to create the score), theomrscope (to run OMR), and thetasks.readonlyscope (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
dataproperty, withdataEncodingset tobase64. - Opt in to tasks. You must set
supportsTasks: truein 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:
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
}'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()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:
{
"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:
curl -H 'Authorization: Bearer <my_api_personal_access_token>' \
https://api.flat.io/v2/tasks/5e0a59c0f1e6b8000868e0c1async 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
}
}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 secondsThe task moves through the following states:
state | Meaning |
|---|---|
created | The task is queued and waiting to be processed. |
doing | OMR processing is in progress (see progress.percent). |
done | The score has been imported. The score field is set. |
error | Processing failed. See result.error. |
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:
{
"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}:
curl -H 'Authorization: Bearer <my_api_personal_access_token>' \
https://api.flat.io/v2/scores/5e0a5b2cf1e6b8000868e0d4const res = await fetch(`https://api.flat.io/v2/scores/${task.score}`, {
headers: { Authorization: `Bearer ${token}` },
})
const score = await res.json()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
| 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. |
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 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.
Related
- OMR overview - scopes, credits, capabilities, and error codes.
- Interactive Jobs API - progress, the instrument-review step, and MusicXML output.
- API Reference:
createScoreandgetTask. - Authentication.