ScoreFlow API Documentation

Sign in to get API access

Overview

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)

ParameterTypeDescription
pdfFilePDF file containing sheet music (max 10MB)
typeStringConversion 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:

  1. Submit your PDF: Send a POST request to /api/convert. The server responds with a 202 Accepted status and a JSON body containing a conversionId.
  2. Check status: Periodically send a GET request to /api/convert/status/{conversionId}. Poll this endpoint until the status field in the response is completed (or failed).
  3. 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.