ScoreFlow API Documentation
Sign in to get API accessOverview
ScoreFlow's API allows you to programmatically convert sheet music from PDF format to MIDI or MusicXML. All API conversions follow an asynchronous workflow: submit your file, poll for status, and then download the result. This ensures reliable processing for scores of any complexity.
API Pricing: Credits cost €0.05 each, with 1 credit allowing conversion of up to 5 pages.
You'll need to purchase credits and generate an API key in your account settings to use the API.
Asynchronous Workflow: All conversions use our asynchronous workflow with status polling.
This is the standard procedure for all API requests. See the "Asynchronous Conversion" section below for implementation details.
Authentication
API requests are authenticated using API keys. These keys should be included in the Authorization header of your HTTP requests.
Authorization: Bearer YOUR_API_KEY
API Endpoints
Convert PDF to MIDI/MusicXML
POST https://scoreflow.app/api/convert
Initiates the conversion of a PDF sheet music file to MIDI or MusicXML format. This endpoint returns a `conversionId` which is then used to check status and download the file.
Request Headers
Authorization: Bearer YOUR_API_KEY
Content-Type: multipart/form-data
Request Body (multipart/form-data)
Parameter | Type | Description |
---|---|---|
File | PDF file containing sheet music (max 10MB) | |
type | String | Conversion type: "midi" or "musicxml" |
Successful Initial Response (202 Accepted)
This response indicates the conversion has been successfully queued. Use the `conversionId` to poll for status.
{
"success": true,
"conversionId": "api_xxxxxxxx_yyyyy",
"status": "queued",
"queueStatus": {
"position": 2, // Example queue position
"estimatedWait": 5 // Example estimated wait in minutes
},
"message": "Conversion initiated. Use the status endpoint to check progress.",
"apiCreditsUsed": 1,
"apiCreditsRemaining": 19,
"timestamp": "2025-05-14T15:42:31.123Z"
}
Error Responses
401 Unauthorized
{
"error": "Invalid or disabled API key"
}
402 Payment Required
{
"error": "Insufficient credits. Required: 2, Available: 1"
}
400 Bad Request
{
"error": "No PDF file provided"
// Or other validation errors like "Invalid conversion type"
}
503 Service Unavailable
{
"error": "All servers are busy. Please try again later."
}
Check Conversion Status
GET https://scoreflow.app/api/convert/status/{conversionId}
Check the status of a queued conversion using the `conversionId` received from the POST request.
Request Headers
Authorization: Bearer YOUR_API_KEY
Possible Status Responses (200 OK)
Status: queued
{
"status": "queued",
"queueStatus": {
"position": 1,
"estimatedWait": 2
},
"message": "Your conversion is in the queue."
}
Status: processing
{
"status": "processing",
// "progress": 45, // Progress may or may not be available
"message": "Your conversion is being processed."
// Potentially more detailed message e.g. "Processing page 3 of 7"
}
Status: completed
{
"status": "completed",
"message": "Conversion completed successfully. Ready for download."
}
Status: failed
{
"status": "failed",
"error": "Conversion process encountered an error.",
// "details": "Optional more specific error details"
}
Download Converted File
GET https://scoreflow.app/api/convert/download/{conversionId}
Download a completed conversion. Only call this endpoint after the status is "completed".
Request Headers
Authorization: Bearer YOUR_API_KEY
Response
Binary file data with appropriate Content-Type header (e.g., `audio/midi` or `application/vnd.recordare.musicxml+xml`) and Content-Disposition header.
Error Responses
404 Not Found
{
"error": "Conversion not found"
}
400 Bad Request
{
"error": "Conversion not yet completed",
"currentStatus": "processing" // or "queued", "failed"
}
Asynchronous Conversion Workflow
All API conversions utilize an asynchronous workflow to handle scores of any complexity:
- Submit your PDF: Send a POST request to
/api/convert
. The server responds with a202 Accepted
status and a JSON body containing aconversionId
. - Check status: Periodically send a GET request to
/api/convert/status/{conversionId}
. Poll this endpoint until thestatus
field in the response iscompleted
(orfailed
). - Download the result: Once the status is
completed
, send a GET request to/api/convert/download/{conversionId}
to retrieve the converted file.
This approach ensures robust handling of conversions, irrespective of their processing time, as it doesn't rely on a single HTTP request remaining open.
Code Examples
JavaScript/Browser (Asynchronous)
// Asynchronous conversion with status polling
async function convertPdfAsync(pdfFile, type = 'midi') {
// Step 1: Submit the PDF and start conversion
const formData = new FormData();
formData.append('pdf', pdfFile);
formData.append('type', type);
const response = await fetch('https://scoreflow.app/api/convert', {
method: 'POST',
headers: {
'Authorization': 'Bearer YOUR_API_KEY'
// Content-Type is automatically set by browser for FormData
},
body: formData
});
const data = await response.json();
if (response.status === 202 && data.conversionId) {
// Conversion successfully queued, proceed to polling
console.log('Conversion queued:', data);
return await pollUntilComplete(data.conversionId);
} else if (!response.ok) {
// Handle other errors (400, 401, 402, 500, 503 etc.)
throw new Error(data.error || `Failed to start conversion. Status: ${response.status}`);
} else {
// Unexpected successful response that isn't 202
throw new Error(`Unexpected response from server. Status: ${response.status}`);
}
}
async function pollUntilComplete(conversionId) {
// Step 2: Poll the status endpoint
const MAX_ATTEMPTS = 120; // Example: poll for up to 2 hours if interval is 1 min
let attempts = 0;
console.log(`Polling for conversion ID: ${conversionId}`);
while (attempts < MAX_ATTEMPTS) {
attempts++;
// Implement an adaptive polling interval (e.g., exponential backoff with jitter)
// Start with a short interval, then increase it.
// Example: 5s, 10s, 15s, then cap at 30s or 60s.
const interval = Math.min(5000 * attempts, 60000);
await new Promise(resolve => setTimeout(resolve, interval));
console.log(`Checking status for ${conversionId} (Attempt ${attempts})`);
const statusResponse = await fetch(
`https://scoreflow.app/api/convert/status/${conversionId}`,
{
headers: {
'Authorization': 'Bearer YOUR_API_KEY'
}
}
);
if (!statusResponse.ok) {
// Handle non-OK status check responses (e.g., 404 if ID is wrong, or server error)
const errorData = await statusResponse.json().catch(() => ({ error: 'Failed to parse status error' }));
throw new Error(errorData.error || `Failed to check conversion status. Status: ${statusResponse.status}`);
}
const statusData = await statusResponse.json();
console.log('Current status:', statusData);
if (statusData.status === 'completed') {
// Step 3: Conversion is complete, prepare to download
console.log('Conversion completed!');
return {
success: true,
conversionId,
download: async function() {
console.log(`Downloading file for ${conversionId}...`);
const downloadUrl = `https://scoreflow.app/api/convert/download/${conversionId}`;
const fileResponse = await fetch(downloadUrl, {
headers: {
'Authorization': 'Bearer YOUR_API_KEY'
}
});
if (!fileResponse.ok) {
const errorData = await fileResponse.json().catch(() => ({ error: 'Failed to parse download error' }));
throw new Error(errorData.error || `Failed to download converted file. Status: ${fileResponse.status}`);
}
// Determine filename from Content-Disposition or generate one
const contentDisposition = fileResponse.headers.get('Content-Disposition');
let filename = `${conversionId}.data`; // Default filename
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename="?(.+)"?/i);
if (filenameMatch && filenameMatch[1]) {
filename = filenameMatch[1];
}
}
console.log(`Downloaded file: ${filename}`);
return { blob: await fileResponse.blob(), filename };
}
};
} else if (statusData.status === 'failed') {
throw new Error(statusData.error || statusData.message || 'Conversion failed');
}
// Continue polling if status is 'queued' or 'processing'
console.log(`Conversion status for ${conversionId}: ${statusData.status}. Polling again in ${interval/1000}s...`);
}
throw new Error(`Conversion ${conversionId} timed out after ${MAX_ATTEMPTS} attempts.`);
}
// Usage example (ensure you have an input element with id="pdfFileInput")
// document.getElementById('yourConvertButton').addEventListener('click', async () => {
// const fileInput = document.getElementById('pdfFileInput');
// if (!fileInput || !fileInput.files || fileInput.files.length === 0) {
// console.error('No file selected');
// alert('Please select a PDF file first.');
// return;
// }
// const file = fileInput.files[0];
// try {
// // Show some loading indicator to the user
// console.log('Starting conversion process...');
// const result = await convertPdfAsync(file, 'midi'); // or 'musicxml'
// console.log('Process finished, ready to download:', result);
// // Download the file
// const { blob, filename } = await result.download();
// const url = URL.createObjectURL(blob);
// const a = document.createElement('a');
// a.href = url;
// a.download = filename; // Use the server-provided or generated filename
// document.body.appendChild(a);
// a.click();
// document.body.removeChild(a);
// URL.revokeObjectURL(url);
// console.log('File download initiated.');
// // Hide loading indicator
// } catch (error) {
// console.error('Conversion error:', error.message);
// alert(`Conversion Error: ${error.message}`);
// // Hide loading indicator
// }
// });
Python (Asynchronous)
import requests
import time
import os
API_KEY = 'YOUR_API_KEY' # Replace with your actual API key
BASE_URL = 'https://scoreflow.app/api'
def convert_pdf_async(pdf_file_path, conversion_type='midi'):
"""
Convert a PDF to MIDI or MusicXML using the ScoreFlow API (asynchronous workflow).
"""
headers = {'Authorization': f'Bearer {API_KEY}'}
# Step 1: Submit the conversion request
print(f"Starting conversion for {pdf_file_path} to {conversion_type}...")
with open(pdf_file_path, 'rb') as f:
files = {'pdf': (os.path.basename(pdf_file_path), f, 'application/pdf')}
data = {'type': conversion_type}
try:
response = requests.post(
f'{BASE_URL}/convert',
headers=headers,
files=files,
data=data,
timeout=30 # Timeout for the initial request
)
response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx)
except requests.exceptions.RequestException as e:
raise Exception(f"Failed to submit conversion request: {e}")
if response.status_code == 202:
result = response.json()
conversion_id = result.get('conversionId')
if not conversion_id:
raise Exception("API error: 202 Accepted but no conversionId received.")
print(f"Conversion successfully queued. Conversion ID: {conversion_id}")
print(f"Credits used: {result.get('apiCreditsUsed', 'N/A')}, Remaining: {result.get('apiCreditsRemaining', 'N/A')}")
# Step 2: Poll for completion
return poll_until_complete(conversion_id, headers, pdf_file_path, conversion_type)
else:
# This case should ideally be caught by response.raise_for_status()
# but as a fallback:
error_message = "Unknown error during submission"
try:
error_message = response.json().get('error', error_message)
except requests.exceptions.JSONDecodeError:
error_message = response.text
raise Exception(f"API error (Status {response.status_code}): {error_message}")
def poll_until_complete(conversion_id, headers, original_pdf_path, conversion_type):
"""Poll the API until conversion is complete or failed."""
max_attempts = 120 # Example: poll for up to 2 hours (adjust as needed)
attempts = 0
status_url = f'{BASE_URL}/convert/status/{conversion_id}'
while attempts < max_attempts:
attempts += 1
# Adaptive polling interval (e.g., 5s, then 10s, up to 60s)
interval = min(5 * attempts, 60)
print(f"Checking status for {conversion_id} (Attempt {attempts})... Next check in {interval}s.")
time.sleep(interval)
try:
response = requests.get(status_url, headers=headers, timeout=15)
response.raise_for_status() # Check for 4xx/5xx errors
except requests.exceptions.RequestException as e:
print(f"Warning: Failed to check status on attempt {attempts}: {e}")
if attempts == max_attempts: # If last attempt also failed to connect
raise Exception(f"Failed to get status after {max_attempts} attempts. Last error: {e}")
continue # Try again if not the last attempt
status_data = response.json()
current_status = status_data.get('status', 'unknown').lower()
print(f"Current status for {conversion_id}: {current_status}")
if current_status == 'completed':
print("Conversion completed! Proceeding to download...")
# Step 3: Download the file
download_url = f'{BASE_URL}/convert/download/{conversion_id}'
return download_file(download_url, headers, original_pdf_path, conversion_type)
elif current_status == 'failed':
error_msg = status_data.get('error', status_data.get('message', 'Conversion process failed.'))
raise Exception(f"Conversion failed for {conversion_id}: {error_msg}")
elif current_status not in ['queued', 'processing']:
raise Exception(f"Unexpected status received for {conversion_id}: {current_status}")
raise Exception(f"Conversion {conversion_id} timed out after {max_attempts} attempts.")
def download_file(url, headers, original_pdf_path, conversion_type):
"""Download the converted file and save it."""
print(f"Downloading file from: {url}")
try:
response = requests.get(url, headers=headers, stream=True, timeout=300) # 5 min timeout for download
response.raise_for_status()
except requests.exceptions.RequestException as e:
raise Exception(f"Failed to download file: {e}")
# Determine output filename
filename_from_header = None
content_disposition = response.headers.get('Content-Disposition')
if content_disposition:
# Basic parsing, more robust parsing might be needed
parts = content_disposition.split('filename=')
if len(parts) > 1:
filename_from_header = parts[1].strip('"')
if filename_from_header:
output_file = filename_from_header
else:
base_name, _ = os.path.splitext(os.path.basename(original_pdf_path))
output_extension = '.mid' if conversion_type == 'midi' else '.musicxml'
output_file = f"{base_name}_converted{output_extension}"
# Save the file
try:
with open(output_file, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
print(f"File successfully downloaded and saved to: {output_file}")
return output_file
except IOError as e:
raise Exception(f"Failed to save downloaded file {output_file}: {e}")
# --- Example Usage ---
if __name__ == '__main__':
# Create a dummy PDF file for testing if it doesn't exist
dummy_pdf_path = 'test_sheet_music.pdf'
if not os.path.exists(dummy_pdf_path):
try:
with open(dummy_pdf_path, 'w') as f:
f.write("%PDF-1.4\n%Dummy PDF for testing ScoreFlow API\n%%EOF")
print(f"Created dummy PDF: {dummy_pdf_path}")
except IOError:
print(f"Could not create dummy PDF at {dummy_pdf_path}. Please create it manually or use a real PDF.")
# exit(1) # Uncomment if you want to stop if dummy PDF cannot be created
# Replace 'test_sheet_music.pdf' with the actual path to your PDF file
# Ensure API_KEY at the top is set correctly.
pdf_to_convert = dummy_pdf_path # or your actual PDF path
if not API_KEY or API_KEY == 'YOUR_API_KEY':
print("ERROR: Please set your API_KEY at the top of the script.")
elif not os.path.exists(pdf_to_convert):
print(f"ERROR: PDF file not found at {pdf_to_convert}")
else:
try:
print("--- Starting MIDI Conversion ---")
midi_output_path = convert_pdf_async(pdf_to_convert, 'midi')
print(f"MIDI conversion successful! File saved to: {midi_output_path}")
# print("\n--- Starting MusicXML Conversion ---")
# musicxml_output_path = convert_pdf_async(pdf_to_convert, 'musicxml')
# print(f"MusicXML conversion successful! File saved to: {musicxml_output_path}")
except Exception as e:
print(f"An error occurred: {e}")
Rate Limits & Policies
Rate Limits
The API is rate-limited to protect our services from abuse:
- Maximum 60 requests per minute for submitting new conversions (`/api/convert`).
- Polling requests (`/api/convert/status/*`) may have more lenient limits but should still be reasonable (e.g., avoid polling faster than every 5 seconds).
- Overall daily request limits may apply.
Exceeding rate limits will result in a 429 Too Many Requests
error.
File Limitations
- Maximum file size: 10MB
- Only PDF files are accepted
- For optimal conversion results, ensure the PDF contains clear, high-quality sheet music images (not handwritten or poor scans).
Credits
- 1 credit permits conversion of up to 5 pages.
- Credit usage is determined by the number of pages in the PDF and is charged upon successful queueing of the conversion.
- Credits are rounded up (e.g., a 6-page PDF will use 2 credits).
- Credits can be purchased from your account settings at €0.05 per credit.