Skip to main content

Overview

Upload royalty files and let the API automatically detect the source (Spotify, Apple Music, etc.), accounting period, and file schema. The system uses intelligent pattern matching to identify files and provides confidence scores so you know when to review manually versus auto-process.

Key Features

  • Automatic source detection from filename and content patterns
  • Confidence scoring (0-1 scale) for detection accuracy
  • Period extraction from filenames like spotify_2024-01.csv
  • Manual confirmation workflow for low-confidence matches
  • Auto-processing for trusted sources with high confidence
  • Real-time status polling to track detection progress

Prerequisites

Authentication

All requests require a valid API token:
const headers = {
  'Authorization': `Bearer ${YOUR_API_TOKEN}`,
  'Content-Type': 'application/json'
};

Quick Start

1

Request Upload URL

Get a pre-signed URL for uploading your file with detection enabled.
Node.js
const response = await fetch('https://api.royalti.io/file/upload-url', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${YOUR_API_TOKEN}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    fileName: 'spotify_2024-01.csv',
    fileSize: 1048576,
    uploadType: 'royalty',
    enableDetection: true  // Enable automatic detection
  })
});

const { signedUrl, sessionId, fileId } = await response.json();
Save the sessionId - you’ll need it to check detection status.
2

Upload File

Upload your file to the signed URL using a PUT request.
Node.js
const file = fs.readFileSync('./spotify_2024-01.csv');

await fetch(signedUrl, {
  method: 'PUT',
  headers: { 'Content-Type': 'application/octet-stream' },
  body: file
});
The detection process starts automatically after upload completes.
3

Check Detection Status

Poll the detection endpoint to get results.
Node.js
const detectionResponse = await fetch(
  `https://api.royalti.io/royalty/detection/${sessionId}`,
  { headers: { 'Authorization': `Bearer ${YOUR_API_TOKEN}` } }
);

const detection = await detectionResponse.json();

console.log('Detected source:', detection.data.detectedValues.source);
console.log('Confidence:', detection.data.confidence.source);
// Output: Detected source: spotify
// Output: Confidence: 0.95
Files with confidence scores above 0.8 can be auto-processed if you enable that feature. See Auto-Processing Configuration below.

Understanding Confidence Scores

The API returns three confidence scores for each file:
Score TypeWhat It MeasuresThreshold for Auto-Process
sourceHow certain the source detection is> 0.8
periodHow certain the period extraction is> 0.7
schemaHow well the file structure matches> 0.8

Confidence Levels

What it means: Detection is highly reliableAction: File can be auto-processedExample:
{
  "confidence": {
    "source": 0.95,
    "period": 0.89,
    "schema": 0.92
  }
}

Dual-Period Sources

Some royalty sources (like Merlin aggregator sources) require both a sale period and an accounting period. These are called “dual-period sources.”

How to Identify Dual-Period Sources

When you upload a file, check the detection response for requiresAccountingPeriod:
{
  "data": {
    "detectedValues": {
      "source": "merlin_snap",
      "detectedSalePeriod": "2024-06-01",
      "requiresAccountingPeriod": true
    }
  }
}
When requiresAccountingPeriod: true, you must provide both accountingPeriod and salePeriod when confirming. The system will block auto-processing until both periods are provided.

Common Dual-Period Sources

SourceTable FormatExample Table Name
merlin_snapmerlin_snap_SALPER_ACCPERmerlin_snap_202406_202406
merlin_facebookmerlin_facebook_SALPER_ACCPERmerlin_facebook_202406_202407
merlin_tiktokmerlin_tiktok_SALPER_ACCPERmerlin_tiktok_202406_202406

Understanding Sale Period vs Accounting Period

Period TypeDescriptionSource
Sale PeriodWhen the streams/sales occurredUsually from filename (e.g., _202406_)
Accounting PeriodWhen the royalties are being reportedUsually from file content (e.g., Start_Date column)
For most files, these periods are the same. But some distributors report sales with a delay, so the accounting period may be different from the sale period.

Detection Workflow

Polling for Detection Results

Detection typically completes within 2-10 seconds. Poll every 2 seconds until status is detected:
async function waitForDetection(sessionId) {
  for (let attempt = 0; attempt < 30; attempt++) {
    const response = await fetch(
      `https://api.royalti.io/royalty/detection/${sessionId}`,
      { headers: { 'Authorization': `Bearer ${YOUR_API_TOKEN}` } }
    );

    const detection = await response.json();
    const status = detection.data.status;

    if (status === 'detected' || status === 'auto_confirmed') {
      return detection.data;
    }

    if (status === 'expired') {
      throw new Error('Detection session expired after 24 hours');
    }

    // Wait 2 seconds before next attempt
    await new Promise(resolve => setTimeout(resolve, 2000));
  }

  throw new Error('Detection timeout after 60 seconds');
}

// Usage
const result = await waitForDetection(sessionId);
console.log('Detection complete:', result);

Detection Response Format

{
  "status": "success",
  "data": {
    "id": "123e4567-e89b-12d3-a456-426614174000",
    "status": "detected",
    "fileName": "merlin_snap_202406_Monthly-Sales.csv",
    "fileSize": 1048576,
    "detectedValues": {
      "source": "merlin_snap",
      "period": "2024-06-01",
      "schema": "Service:STRING,Start_Date:DATE,...",
      "detectedSalePeriod": "2024-06-01",
      "detectedAccountingPeriod": null,
      "requiresAccountingPeriod": true,
      "suggestedPeriodMapping": "salePeriod",
      "periodSource": "filename"
    },
    "confidence": {
      "source": 1.0,
      "period": 0.85,
      "schema": 1.0
    },
    "completedSteps": ["upload", "analysis", "pattern_matching"],
    "expiresAt": "2025-01-16T12:00:00Z"
  }
}

Key Response Fields

FieldDescription
detectedSalePeriodDetected sale period (from filename or content)
detectedAccountingPeriodDetected accounting period (if found in content)
requiresAccountingPeriodtrue if this source needs both periods
suggestedPeriodMappingWhether detected period should be salePeriod or accountingPeriod
periodSourceWhere period was detected: filename or content

Confirming Detection

When to Confirm Manually

Manual confirmation is required when:
  • Any confidence score < 0.8
  • Auto-processing is disabled
  • You want to review before processing
  • Detection identified an unexpected source
  • Dual-period sources require both accountingPeriod and salePeriod

Confirmation Endpoints

There are two endpoints for confirming detection:
EndpointUse CaseSource Input
POST /file/detection/:sessionIdQuick confirmation with UUIDroyaltySourceId (UUID)
POST /file/confirm-detection/:sessionIdFull flow with learningroyaltySource (name)

Option 1: Quick Confirmation (by Source ID)

Use this when you have the source UUID and want simple confirmation.
async function confirmDetection(sessionId, sourceId, accountingPeriod, salePeriod) {
  const response = await fetch(
    `https://api.royalti.io/file/detection/${sessionId}`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${YOUR_API_TOKEN}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        royaltySourceId: sourceId,
        accountingPeriod: accountingPeriod,  // Format: YYYY-MM-DD
        salePeriod: salePeriod,              // For dual-period sources
        confirm: true                         // Required to trigger processing
      })
    }
  );

  const result = await response.json();
  return result.data.jobId;
}

// Usage - Single period source
const jobId = await confirmDetection(
  sessionId,
  '456e7890-e89b-12d3-a456-426614174001',
  '2024-01-01',
  null
);

// Usage - Dual period source (e.g., Merlin sources)
const jobId = await confirmDetection(
  sessionId,
  '59339018-d21e-4882-b81a-2d5bce37600e',
  '2024-06-01',  // Accounting period
  '2024-06-01'   // Sale period
);

console.log('Processing started. Job ID:', jobId);
The confirm: true flag is required to trigger processing. Without it, the file is marked for manual review but not processed.

Option 2: Full Confirmation (by Source Name)

Use this for the complete workflow with correction tracking and learning.
async function confirmDetectionFull(sessionId, sourceName, accountingPeriod, salePeriod) {
  const response = await fetch(
    `https://api.royalti.io/file/confirm-detection/${sessionId}`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${YOUR_API_TOKEN}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        royaltySource: sourceName,           // Source NAME, not UUID
        accountingPeriod: accountingPeriod,
        salePeriod: salePeriod               // For dual-period sources
      })
    }
  );

  const result = await response.json();
  return result.data;
}

// Usage
const result = await confirmDetectionFull(
  sessionId,
  'merlin_snap',     // Source name
  '2024-06-01',      // Accounting period
  '2024-06-01'       // Sale period
);
This endpoint also:
  • Records corrections if you change detected values (for learning)
  • Tracks period mapping corrections (salePeriod vs accountingPeriod)
  • Updates cloud storage metadata
  • Supports ZIP file confirmations

Getting Source ID

You need the source ID to confirm detection. Retrieve it from the sources endpoint:
Node.js
const sourcesResponse = await fetch(
  'https://api.royalti.io/royalty/sources',
  { headers: { 'Authorization': `Bearer ${YOUR_API_TOKEN}` } }
);

const sources = await sourcesResponse.json();
const spotify = sources.data.sources.find(s => s.name === 'spotify');
const sourceId = spotify.id;

Monitoring Processing

After confirmation, track processing progress:
async function checkProcessingStatus(jobId) {
  const response = await fetch(
    `https://api.royalti.io/royalty/processing/${jobId}`,
    { headers: { 'Authorization': `Bearer ${YOUR_API_TOKEN}` } }
  );

  const result = await response.json();
  const job = result.data;

  console.log(`Status: ${job.status}`);
  console.log(`Progress: ${job.progress}%`);
  console.log(`Rows processed: ${job.rowsProcessed}/${job.rowsTotal}`);

  return job;
}

// Poll every 3 seconds
async function waitForProcessing(jobId) {
  while (true) {
    const job = await checkProcessingStatus(jobId);

    if (job.status === 'completed') {
      console.log('✓ Processing complete!');
      return job;
    }

    if (job.status === 'failed') {
      console.error('✗ Processing failed:', job.errors);
      throw new Error('Processing failed');
    }

    await new Promise(resolve => setTimeout(resolve, 3000));
  }
}

Processing Status Values

StatusDescription
waitingJob is queued, waiting to start
activeCurrently processing rows
completedSuccessfully finished
failedProcessing encountered an error
delayedTemporarily delayed, will retry

Auto-Processing

Enable auto-processing to automatically process files with high confidence scores.

Enable Auto-Processing

const response = await fetch(
  'https://api.royalti.io/file/enable-auto-processing',
  {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${YOUR_API_TOKEN}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      enabled: true,
      confidenceThresholds: {
        source: 0.85,   // Minimum source confidence
        period: 0.75,   // Minimum period confidence
        schema: 0.85    // Minimum schema confidence
      }
    })
  }
);

console.log('Auto-processing enabled');
Set thresholds carefully. Higher thresholds (0.9+) are safer but require more manual confirmations. Start with 0.85 and adjust based on accuracy.

Check Auto-Processing Config

Node.js
const response = await fetch(
  'https://api.royalti.io/file/auto-processing-config',
  { headers: { 'Authorization': `Bearer ${YOUR_API_TOKEN}` } }
);

const config = await response.json();

console.log('Enabled:', config.data.enabled);
console.log('Thresholds:', config.data.confidenceThresholds);
console.log('Success rate:', config.data.statistics.successRate, '%');

Complete Example

Here’s a full workflow from upload to processing:

Best Practices

File Naming

Use consistent naming for better detection accuracy:
spotify_2024-01.csv
apple_music_2024-Q1.xlsx
youtube_january_2024.csv
Pattern: {source}_{period}.{ext}

Error Handling

Always handle errors gracefully:
Node.js
try {
  const detection = await waitForDetection(sessionId);

  if (detection.confidence.source < 0.6) {
    console.warn('Low confidence - manual review recommended');
  }

  // Continue with confirmation...
} catch (error) {
  if (error.message.includes('expired')) {
    console.error('Session expired - please re-upload');
  } else if (error.message.includes('timeout')) {
    console.error('Detection took too long - check file format');
  } else {
    console.error('Unexpected error:', error);
  }
}

Timeout Handling

Don’t poll indefinitely:
Node.js
const MAX_DETECTION_TIME = 60000; // 60 seconds
const POLL_INTERVAL = 2000; // 2 seconds
const MAX_ATTEMPTS = MAX_DETECTION_TIME / POLL_INTERVAL;

for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
  // Poll detection...
}

Troubleshooting

Problem: Detection confidence is consistently below 0.8Solutions:
  • Use standard filename format: {source}_{period}.{ext}
  • Ensure column headers match expected schema
  • Check file isn’t corrupted or empty
  • Contact support to create custom patterns for your files
Problem: Error “Detection session expired after 24 hours”Solutions:
  • Process files within 24 hours of upload
  • Enable auto-processing for faster handling
  • Re-upload the file to create a new session
Problem: System detects incorrect sourceSolutions:
  • Check filename doesn’t contain misleading keywords
  • Manually confirm with correct source ID
  • Contact support to improve detection patterns for your file format
Problem: Job never progresses from waiting to activeSolutions:
  • Check queue health (if you have admin access)
  • Wait up to 5 minutes during high load
  • Contact support if stuck longer than 10 minutes

API Reference

Related endpoints:

Next Steps


Support

Need help? Contact us: