OMR Interactive Jobs API
The Interactive Jobs API exposes Flat's Optical Music Recognition (OMR) pipeline directly through the /omr/jobs endpoints. Use it when you need more than a one-shot import: live progress, an optional review step to confirm the detected instruments, incremental capture (add pages one by one, ideal on mobile), or MusicXML output without creating a Library score.
If you just want a score in the Library from a single PDF, the Auto simple import is fewer calls. New to OMR? Start with the OMR overview for scopes, credits, and error codes.
Job lifecycle
A job is a small state machine. You create it, start it, optionally answer interactive steps while it runs, and read the result when it is done.
status | Meaning |
|---|---|
draft | Created; files can still be added. Not started or charged yet. |
processing | The pipeline is running. See progress. |
awaitingInput | Paused on currentStep, waiting for you to submit it. |
done | Finished. See result. |
error | Failed. See errorCode and errorMessage. |
canceled | Canceled by the client. |
In this page:
- Prerequisites
- Draft vs one-shot
- Step 1. Create a draft job
- Step 2. Add files
- Step 3. Start the job
- Step 4. Follow progress
- Step 5. Review detected instruments
- Step 6. Get the result
- One-shot import
- Exports
- List and resume jobs
- Cancel a job
- Errors
Prerequisites
Every request needs the omr scope (with a Personal Access Token or an OAuth2 token). With the default output: library, the job creates a score, so an OAuth2 token also needs the scores scope. output: musicxml only needs omr. See Authentication and scopes.
The examples below use these variables:
export TOKEN='<my_api_personal_access_token>'
export BASE='https://api.flat.io/v2'const TOKEN = '<my_api_personal_access_token>'
const BASE = 'https://api.flat.io/v2'
const auth = { Authorization: `Bearer ${TOKEN}` }import base64, time, requests
TOKEN = "<my_api_personal_access_token>"
BASE = "https://api.flat.io/v2"
auth = {"Authorization": f"Bearer {TOKEN}"}Draft vs one-shot
There are two ways to create a job:
- Draft (this walkthrough): create an empty job, add files one at a time, then start it. Best for multiple images or incremental mobile capture, where pages arrive over time.
- One-shot: send the files inline with
autoStart: truein a single request. Best for a single PDF or a straightforward third-party integration.
Both produce the same kind of job. Pick whichever fits how your client captures files.
Step 1. Create a draft job
Create the job with POST /omr/jobs. Omit files to get a draft. Declare the interactive steps your client can render in interactiveSteps (here, details); the pipeline pauses only at the steps you list and auto-resolves the rest, so future steps never break an older client.
curl -X POST "$BASE/omr/jobs" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"output": "musicxml",
"interactiveSteps": ["details"],
"locales": ["en"]
}'let res = await fetch(`${BASE}/omr/jobs`, {
method: 'POST',
headers: { ...auth, 'Content-Type': 'application/json' },
body: JSON.stringify({
output: 'musicxml',
interactiveSteps: ['details'],
locales: ['en'],
}),
})
let job = await res.json()
const jobId = job.idjob = requests.post(
f"{BASE}/omr/jobs",
headers=auth,
json={"output": "musicxml", "interactiveSteps": ["details"], "locales": ["en"]},
).json()
job_id = job["id"]The response is a job in draft:
{
"id": "665f3a1c9b2e4a0012ab34cd",
"status": "draft",
"output": "musicxml",
"interactiveSteps": ["details"],
"locales": ["en"],
"creationDate": "2026-07-08T09:24:00.000Z"
}This example uses output: "musicxml", which only needs the omr scope and lets you download the result directly. To import the result into the Flat Library instead, set output: "library" (the default), add the scores scope, and use collection to target a specific collection. Pass idempotencyKey to make retries safe (a repeated key returns the existing job).
Step 2. Add files
Add one image or PDF per call with POST /omr/jobs/{job}/files. Files keep their upload order. The file is Base64-encoded in file.
curl -X POST "$BASE/omr/jobs/$JOB/files" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d "{ \"file\": \"$(base64 < page1.jpg | tr -d '\n')\", \"filename\": \"page1.jpg\" }"import { readFileSync } from 'node:fs'
res = await fetch(`${BASE}/omr/jobs/${jobId}/files`, {
method: 'POST',
headers: { ...auth, 'Content-Type': 'application/json' },
body: JSON.stringify({
file: readFileSync('page1.jpg').toString('base64'),
filename: 'page1.jpg',
}),
})
const { fileCount } = await res.json()with open("page1.jpg", "rb") as f:
payload = {"file": base64.b64encode(f.read()).decode(), "filename": "page1.jpg"}
added = requests.post(f"{BASE}/omr/jobs/{job_id}/files", headers=auth, json=payload).json()The response reports the assigned index and running count: { "fileIndex": 0, "fileCount": 1 }. Provide either a filename with a recognizable extension or an explicit mimeType; if the type cannot be determined from either, the call returns 400 (OMR_MIME_REQUIRED). Repeat for each page.
Step 3. Start the job
Start processing with POST /omr/jobs/{job}/start. This validates the files, runs the permission, quota, and credit checks, charges the page-based credits, and queues the job.
curl -X POST "$BASE/omr/jobs/$JOB/start" -H "Authorization: Bearer $TOKEN"res = await fetch(`${BASE}/omr/jobs/${jobId}/start`, { method: 'POST', headers: auth })
job = await res.json() // status: "processing", estimatedCredits: Njob = requests.post(f"{BASE}/omr/jobs/{job_id}/start", headers=auth).json()The job returns as processing with estimatedCredits set to what was charged. A start with no files returns 400; insufficient credits or quota returns 402.
Step 4. Follow progress
Poll GET /omr/jobs/{job} to track the job. Optionally pass wait (0 to 25 seconds) to long-poll: the call returns as soon as the state changes (or live progress advances), then you call again. This keeps progress live without a tight loop.
While processing, the job carries a progress object with a percent, a localized text, and a stable key (for example OMR_QUEUED, OMR_DETECTING_STAVES, OMR_READING_NOTES) you can map to your own stepper UI.
# Returns as soon as the state changes, or after 25s.
curl -H "Authorization: Bearer $TOKEN" "$BASE/omr/jobs/$JOB?wait=25"async function pollUntilSettled(jobId) {
while (true) {
const res = await fetch(`${BASE}/omr/jobs/${jobId}?wait=25`, { headers: auth })
const job = await res.json()
if (job.status !== 'processing') return job // awaitingInput, done, error, canceled
console.log(job.progress?.percent, job.progress?.key)
}
}def poll_until_settled(job_id):
while True:
job = requests.get(f"{BASE}/omr/jobs/{job_id}", params={"wait": 25}, headers=auth).json()
if job["status"] != "processing":
return job # awaitingInput, done, error, canceled
print(job.get("progress", {}).get("percent"), job.get("progress", {}).get("key"))The job settles into awaitingInput (if a detection needs your review), done, or error.
Step 5. Review detected instruments
If you declared details in interactiveSteps, the job may pause at awaitingInput after recognition, with currentStep: "details", so you can review low-confidence detections. It may also resolve automatically and continue straight to done, for example when every part is recognized with high confidence. Always handle both outcomes: after processing, a job can be awaitingInput or already done.
When it does pause, pendingStep.data holds the detected title and instruments for review (the "check your score details" screen):
{
"id": "665f3a1c9b2e4a0012ab34cd",
"status": "awaitingInput",
"currentStep": "details",
"estimatedCredits": 3,
"pendingStep": {
"step": "details",
"data": {
"step": "details",
"title": "Sonata in C",
"instruments": [
{ "index": 0, "partName": "Violin I", "instrumentId": "strings.violin", "instrumentName": "Violin", "midiProgram": 40, "resolvedConfidence": "high" },
{ "index": 1, "partName": "Alto", "instrumentId": "strings.violin", "instrumentName": "Violin", "midiProgram": 40, "resolvedConfidence": "low" }
]
}
}
}To show the pages being reviewed, fetch each input image by index with GET /omr/jobs/{job}/files/{index} (0-based; returns the input page as an image).
Present these to the user, then resume the pipeline by submitting your corrections to POST /omr/jobs/{job}/steps/details. Send only what you change: omitted fields keep the detected values. Here we fix the second part and tidy the title:
curl -X POST "$BASE/omr/jobs/$JOB/steps/details" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"step": "details",
"title": "Sonata in C major",
"instruments": [
{ "index": 1, "instrumentId": "strings.viola" }
]
}'res = await fetch(`${BASE}/omr/jobs/${jobId}/steps/details`, {
method: 'POST',
headers: { ...auth, 'Content-Type': 'application/json' },
body: JSON.stringify({
step: 'details',
title: 'Sonata in C major',
instruments: [{ index: 1, instrumentId: 'strings.viola' }],
}),
})
job = await res.json() // status back to "processing"job = requests.post(
f"{BASE}/omr/jobs/{job_id}/steps/details",
headers=auth,
json={
"step": "details",
"title": "Sonata in C major",
"instruments": [{"index": 1, "instrumentId": "strings.viola"}],
},
).json()The job resumes and re-runs only the cheap assembly and finalize stages. Poll again (step 4) until it is done.
Correcting a part
Each override in instruments[] is matched to a detected part by its index. To set the instrument, use either of these (you do not need both):
instrumentId: a Flat instrument ID such asbrass.horn. Browse the Instrument IDs catalog, or resolve IDs programmatically with the@flat/instrumentspackage.midiProgram: a General MIDI program number (0 to 127), resolved server-side. Handy when you would rather not map Flat instrument IDs.
If both are sent, instrumentId wins. You can also set partName and transposeKey (a pitch class like F or Bb, for transposing instruments) independently. At the submission level, title and mainLanguage (BCP 47, overrides the job locale for lyric and text reading) apply to the whole score.
Step 6. Get the result
When the job is done, result tells you what is available:
{
"status": "done",
"result": {
"exports": ["musicxml", "mxl", "midi"]
}
}- With
output: musicxml(used above), the result stays out of the Flat Library. You download it directly as a file (MusicXML, MIDI, and more) from the exports endpoint. Use this when your integration keeps files in its own system and does not need a Flat score. - With
output: library(the default), the result is imported into the user's Flat Library as a score.result.scoreholds the new score ID. Fetch it withGET /scores/{score}, and it behaves like any other Flat score. You can still download the raw files from the exports endpoint.
One-shot import
To skip the draft sequence, send the files inline with autoStart: true. The job is created, started, and charged in one call, returning 202 with a processing job. This is the simplest path for a single PDF when you do not need to add pages incrementally.
curl -X POST "$BASE/omr/jobs" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d "{
\"output\": \"musicxml\",
\"autoStart\": true,
\"files\": [ { \"file\": \"$(base64 < score.pdf | tr -d '\n')\", \"filename\": \"score.pdf\" } ]
}"res = await fetch(`${BASE}/omr/jobs`, {
method: 'POST',
headers: { ...auth, 'Content-Type': 'application/json' },
body: JSON.stringify({
output: 'musicxml',
autoStart: true,
files: [{ file: readFileSync('score.pdf').toString('base64'), filename: 'score.pdf' }],
}),
})
job = await res.json()with open("score.pdf", "rb") as f:
files = [{"file": base64.b64encode(f.read()).decode(), "filename": "score.pdf"}]
job = requests.post(
f"{BASE}/omr/jobs",
headers=auth,
json={"output": "musicxml", "autoStart": True, "files": files},
).json()From here, follow progress (step 4) as usual. If you also declared interactiveSteps, the one-shot job still pauses for review.
Exports
Once a job is done, download the result in any available format with GET /omr/jobs/{job}/exports/{format}. The Content-Disposition header carries a filename derived from the resolved work title.
format | File | Content type |
|---|---|---|
musicxml | Uncompressed MusicXML (.xml) | application/vnd.recordare.musicxml+xml |
mxl | Compressed MusicXML (.mxl) | application/vnd.recordare.musicxml |
midi | Standard MIDI (.mid) | audio/midi |
thumbnail.png | PNG preview of the first page | image/png |
curl -H "Authorization: Bearer $TOKEN" \
"$BASE/omr/jobs/$JOB/exports/musicxml" -o score.xmlNew formats may be added over time, so request the format your client supports. result.exports lists what is available for a given job.
List and resume jobs
GET /omr/jobs lists the caller's jobs, newest first, with an optional status filter and cursor pagination via the Link response header. Use it to resume an interrupted job or clean up abandoned drafts.
curl -H "Authorization: Bearer $TOKEN" "$BASE/omr/jobs?status=draft"Cancel a job
POST /omr/jobs/{job}/cancel cancels a job that is not being actively recognized: a draft, a queued job, or a job paused at an interactive step (awaitingInput). Any credits charged at start are reversed.
curl -X POST "$BASE/omr/jobs/$JOB/cancel" -H "Authorization: Bearer $TOKEN"Only a job the worker is actively recognizing cannot be canceled; that returns 409 (OMR_JOB_NOT_CANCELLABLE), so poll and retry if needed. Canceling a job that is already done, error, or canceled is a harmless no-op.
Errors
See the OMR overview for the shared errorCode list and the common HTTP statuses. The job lifecycle adds these request errors:
| Situation | Status | Code |
|---|---|---|
Adding a file to a job that is no longer a draft | 400 | OMR_JOB_NOT_DRAFT |
| Adding a file whose type cannot be determined | 400 | OMR_MIME_REQUIRED |
| Submitting a step whose body and path disagree | 400 | OMR_STEP_MISMATCH |
| Submitting a step the job is not awaiting | 409 | OMR_NOT_AWAITING_STEP |
| Submitting a step for a job whose task can no longer resume (for example a concurrent cancel) | 409 | OMR_JOB_NOT_RESUMABLE |
Downloading an export before the job is done | 409 | OMR_JOB_NOT_DONE |
| Canceling a job the worker is actively processing | 409 | OMR_JOB_NOT_CANCELLABLE |
Related
- OMR overview - scopes, credits, capabilities, and error codes.
- Auto simple import - the fewest-calls path to a Library score.
- Instrument IDs - the catalog used to correct detected parts.
- API Reference: OMR.