Overview
The Royalti.io API provides comprehensive functionality for delivering music products to Digital Service Providers (DSPs) like Spotify, Apple Music, YouTube Music, and more. This guide covers the complete delivery workflow, from preparation to monitoring.
Key Features
Multi-Provider Delivery - Deliver to multiple DSPs simultaneously
Real-Time Validation - Pre-flight checks before delivery
Batch Operations - Process up to 100 products at once
Status Monitoring - Track delivery progress and logs
Automatic Retry - Built-in retry logic for failed deliveries
Legacy Support - Backward-compatible endpoints maintained
Delivery Methods
The API provides two sets of delivery endpoints:
New RESTful API (Recommended)
Modern, REST-compliant endpoints for product delivery with full CRUD operations:
/product/delivery-providers - Get available providers
/product/{id}/deliveries - Manage deliveries
/product/batch-delivery - Batch operations
Legacy API (Deprecated)
Maintained for backward compatibility:
/product/{id}/delivery - Trigger delivery
/product/{id}/delivery/status - Get status
New integrations should use the RESTful API (/deliveries). Legacy endpoints will be deprecated in a future release.
Prerequisites
Before delivering a product, ensure:
Product has a valid UPC - Required for all deliveries
Assets have ISRCs - Required for track-level metadata
Metadata is complete - Provider-specific requirements vary
Provider is configured - Tenant must have access to the provider
Quick Start
Check Available Providers
Retrieve the list of providers available to your workspace: curl -X GET https://api.royalti.io/product/delivery-providers \
-H "Authorization: Bearer YOUR_API_TOKEN"
const response = await fetch ( 'https://api.royalti.io/product/delivery-providers' , {
headers: {
'Authorization' : `Bearer ${ API_TOKEN } `
}
});
const { data } = await response . json ();
console . log ( 'Available providers:' , data );
import requests
response = requests.get(
'https://api.royalti.io/product/delivery-providers' ,
headers = { 'Authorization' : f 'Bearer { API_TOKEN } ' }
)
providers = response.json()[ 'data' ]
print ( f 'Available providers: { providers } ' )
Note the requiredFields for each provider - you’ll need these for validation.
Validate Your Product
Before delivering, validate your product meets provider requirements: curl -X POST https://api.royalti.io/product/prod-123/deliveries/validate \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"providers": ["spotify-ddex-sftp", "apple-ddex-api"]
}'
const validation = await fetch ( 'https://api.royalti.io/product/prod-123/deliveries/validate' , {
method: 'POST' ,
headers: {
'Authorization' : `Bearer ${ API_TOKEN } ` ,
'Content-Type' : 'application/json'
},
body: JSON . stringify ({
providers: [ 'spotify-ddex-sftp' , 'apple-ddex-api' ]
})
});
const result = await validation . json ();
const isValid = result . data . validations . every ( v => v . isValid );
if ( ! isValid ) {
console . error ( 'Validation failed:' , result . data . validations );
}
response = requests.post(
'https://api.royalti.io/product/prod-123/deliveries/validate' ,
headers = {
'Authorization' : f 'Bearer { API_TOKEN } ' ,
'Content-Type' : 'application/json'
},
json = {
'providers' : [ 'spotify-ddex-sftp' , 'apple-ddex-api' ]
}
)
result = response.json()
is_valid = all (v[ 'isValid' ] for v in result[ 'data' ][ 'validations' ])
if not is_valid:
print ( f "Validation errors: { result[ 'data' ][ 'validations' ] } " )
Validation is a dry-run - it checks requirements without actually delivering. Fix any errors before proceeding.
Initiate Delivery
Once validation passes, initiate delivery to one or more DSPs: curl -X POST https://api.royalti.io/product/prod-123/deliveries \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"providers": ["spotify-ddex-sftp", "apple-ddex-api"],
"autoDeliver": true,
"settings": {
"releaseType": "Album"
}
}'
const delivery = await fetch ( 'https://api.royalti.io/product/prod-123/deliveries' , {
method: 'POST' ,
headers: {
'Authorization' : `Bearer ${ API_TOKEN } ` ,
'Content-Type' : 'application/json'
},
body: JSON . stringify ({
providers: [ 'spotify-ddex-sftp' , 'apple-ddex-api' ],
autoDeliver: true ,
settings: {
releaseType: 'Album'
}
})
});
const { data } = await delivery . json ();
console . log ( 'Deliveries initiated:' , data . deliveries );
response = requests.post(
'https://api.royalti.io/product/prod-123/deliveries' ,
headers = {
'Authorization' : f 'Bearer { API_TOKEN } ' ,
'Content-Type' : 'application/json'
},
json = {
'providers' : [ 'spotify-ddex-sftp' , 'apple-ddex-api' ],
'autoDeliver' : True ,
'settings' : {
'releaseType' : 'Album'
}
}
)
data = response.json()[ 'data' ]
print ( f "Deliveries initiated: { data[ 'deliveries' ] } " )
Parameters:
providers (required): Array of provider IDs or single provider string
autoDeliver (optional): Set to false for validation only (default: true)
settings (optional): Custom DDEX settings to override product defaults
Monitor Delivery Progress
Track delivery status and progress: curl -X GET "https://api.royalti.io/product/prod-123/deliveries?status=processing" \
-H "Authorization: Bearer YOUR_API_TOKEN"
const status = await fetch ( 'https://api.royalti.io/product/prod-123/deliveries?status=processing' , {
headers: {
'Authorization' : `Bearer ${ API_TOKEN } `
}
});
const { data } = await status . json ();
data . forEach ( delivery => {
console . log ( ` ${ delivery . provider } : ${ delivery . status } ( ${ delivery . progress } %)` );
});
response = requests.get(
'https://api.royalti.io/product/prod-123/deliveries' ,
headers = { 'Authorization' : f 'Bearer { API_TOKEN } ' },
params = { 'status' : 'processing' }
)
deliveries = response.json()[ 'data' ]
for delivery in deliveries:
print ( f " { delivery[ 'provider' ] } : { delivery[ 'status' ] } ( { delivery[ 'progress' ] } %)" )
Query Parameters:
provider - Filter by specific provider
status - Filter by delivery status
deliveryId - Get specific delivery
Understanding Delivery Providers
Each provider has specific requirements and capabilities. Use the /product/delivery-providers endpoint to discover what’s available.
Response Example:
{
"status" : "success" ,
"message" : "Available delivery providers retrieved from database" ,
"data" : [
{
"id" : "spotify-ddex-sftp" ,
"name" : "Spotify (DDEX ERN)" ,
"messageType" : "ERN" ,
"deliveryMethod" : "SFTP" ,
"requiredFields" : {
"product" : [ "title" , "upc" , "displayArtist" , "releaseDate" ],
"asset" : [ "title" , "isrc" , "displayArtist" ]
},
"requiredAssets" : {
"minimum" : 1 ,
"fields" : []
}
}
]
}
id : Unique identifier for API calls
name : Human-readable display name
messageType : Format used (ERN, MEAD, CSV)
deliveryMethod : Transport protocol (SFTP, FTP, API, HTTP)
requiredFields : Mandatory metadata fields
requiredAssets : Minimum asset requirements
Delivery Status Lifecycle
Deliveries progress through the following statuses:
Status Definitions:
pending - Queued, not yet started
processing - Actively being delivered
delivered - Successfully completed
failed - Delivery failed, can be retried
error - System error occurred
rejected - Provider rejected the delivery
cancelled - Manually cancelled
retry - Scheduled for retry
Monitoring Delivery Status
Track delivery progress with detailed logs and status information:
# Get all deliveries for a product
curl -X GET https://api.royalti.io/product/prod-123/deliveries \
-H "Authorization: Bearer YOUR_API_TOKEN"
# Get specific delivery details
curl -X GET https://api.royalti.io/product/prod-123/deliveries/delivery-456 \
-H "Authorization: Bearer YOUR_API_TOKEN"
// Get all deliveries
const response = await fetch ( `https://api.royalti.io/product/ ${ productId } /deliveries` , {
headers: { 'Authorization' : `Bearer ${ API_TOKEN } ` }
});
const { data } = await response . json ();
// Check delivery logs
data . forEach ( delivery => {
console . log ( ` \n Delivery ${ delivery . id } :` );
console . log ( ` Provider: ${ delivery . providerName } ` );
console . log ( ` Status: ${ delivery . status } ` );
console . log ( ` Progress: ${ delivery . progress } %` );
console . log ( ` Attempts: ${ delivery . attemptCount } / ${ delivery . maxAttempts } ` );
if ( delivery . deliveryLog ) {
console . log ( ' Recent events:' );
delivery . deliveryLog . slice ( - 3 ). forEach ( log => {
console . log ( ` - ${ log . event } ( ${ log . status } )` );
});
}
});
# Get all deliveries
response = requests.get(
f 'https://api.royalti.io/product/ { product_id } /deliveries' ,
headers = { 'Authorization' : f 'Bearer { API_TOKEN } ' }
)
deliveries = response.json()[ 'data' ]
# Check delivery logs
for delivery in deliveries:
print ( f " \n Delivery { delivery[ 'id' ] } :" )
print ( f " Provider: { delivery[ 'providerName' ] } " )
print ( f " Status: { delivery[ 'status' ] } " )
print ( f " Progress: { delivery[ 'progress' ] } %" )
print ( f " Attempts: { delivery[ 'attemptCount' ] } / { delivery[ 'maxAttempts' ] } " )
if delivery.get( 'deliveryLog' ):
print ( " Recent events:" )
for log in delivery[ 'deliveryLog' ][ - 3 :]:
print ( f " - { log[ 'event' ] } ( { log[ 'status' ] } )" )
Handling Failed Deliveries
If a delivery fails, you can retry it with the retry endpoint:
curl -X PUT https://api.royalti.io/product/prod-123/deliveries/delivery-456/retry \
-H "Authorization: Bearer YOUR_API_TOKEN"
const retry = await fetch (
`https://api.royalti.io/product/ ${ productId } /deliveries/ ${ deliveryId } /retry` ,
{
method: 'PUT' ,
headers: { 'Authorization' : `Bearer ${ API_TOKEN } ` }
}
);
if ( retry . ok ) {
const { data } = await retry . json ();
console . log ( `Retry initiated. Attempt ${ data . attemptCount } / ${ data . maxAttempts } ` );
}
response = requests.put(
f 'https://api.royalti.io/product/ { product_id } /deliveries/ { delivery_id } /retry' ,
headers = { 'Authorization' : f 'Bearer { API_TOKEN } ' }
)
if response.ok:
data = response.json()[ 'data' ]
print ( f "Retry initiated. Attempt { data[ 'attemptCount' ] } / { data[ 'maxAttempts' ] } " )
Retry Requirements:
Delivery must be in retryable status (failed, error, rejected, cancelled)
Cannot exceed maximum retry attempts (default: 3)
Cancelling Deliveries
Cancel a pending or in-progress delivery:
curl -X DELETE https://api.royalti.io/product/prod-123/deliveries/delivery-456 \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"reason": "Product metadata needs updating"}'
const cancel = await fetch (
`https://api.royalti.io/product/ ${ productId } /deliveries/ ${ deliveryId } ` ,
{
method: 'DELETE' ,
headers: {
'Authorization' : `Bearer ${ API_TOKEN } ` ,
'Content-Type' : 'application/json'
},
body: JSON . stringify ({
reason: 'Product metadata needs updating'
})
}
);
const { data } = await cancel . json ();
console . log ( `Delivery cancelled: ${ data . status } ` );
response = requests.delete(
f 'https://api.royalti.io/product/ { product_id } /deliveries/ { delivery_id } ' ,
headers = {
'Authorization' : f 'Bearer { API_TOKEN } ' ,
'Content-Type' : 'application/json'
},
json = { 'reason' : 'Product metadata needs updating' }
)
data = response.json()[ 'data' ]
print ( f "Delivery cancelled: { data[ 'status' ] } " )
Cancellable Statuses:
pending - Not yet started
retry - Scheduled for retry
processing - Currently in progress
Cancelling a delivery that’s already completed on the provider side may require manual intervention with the DSP.
Batch Delivery
Deliver multiple products to the same provider efficiently:
curl -X POST https://api.royalti.io/product/batch-delivery \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"productIds": ["prod-123", "prod-456", "prod-789"],
"provider": "spotify-ddex-sftp",
"priority": "normal",
"settings": {
"releaseType": "Album"
}
}'
const batch = await fetch ( 'https://api.royalti.io/product/batch-delivery' , {
method: 'POST' ,
headers: {
'Authorization' : `Bearer ${ API_TOKEN } ` ,
'Content-Type' : 'application/json'
},
body: JSON . stringify ({
productIds: [ 'prod-123' , 'prod-456' , 'prod-789' ],
provider: 'spotify-ddex-sftp' ,
priority: 'normal' ,
settings: {
releaseType: 'Album'
}
})
});
const { data } = await batch . json ();
console . log ( `Batch ${ data . batchId } : ${ data . successfulDeliveries } / ${ data . totalProducts } successful` );
response = requests.post(
'https://api.royalti.io/product/batch-delivery' ,
headers = {
'Authorization' : f 'Bearer { API_TOKEN } ' ,
'Content-Type' : 'application/json'
},
json = {
'productIds' : [ 'prod-123' , 'prod-456' , 'prod-789' ],
'provider' : 'spotify-ddex-sftp' ,
'priority' : 'normal' ,
'settings' : {
'releaseType' : 'Album'
}
}
)
data = response.json()[ 'data' ]
print ( f "Batch { data[ 'batchId' ] } : { data[ 'successfulDeliveries' ] } / { data[ 'totalProducts' ] } successful" )
Limitations:
Maximum 100 products per batch
All products must have UPC codes
All products delivered to same provider
Response:
{
"status" : "success" ,
"message" : "Batch delivery initiated successfully" ,
"data" : {
"batchId" : "batch_1234567890" ,
"totalProducts" : 3 ,
"foundProducts" : 3 ,
"successfulDeliveries" : 3 ,
"failedDeliveries" : 0 ,
"provider" : "spotify-ddex-sftp" ,
"deliveries" : [
{
"productId" : "prod-123" ,
"upc" : "123456789012" ,
"status" : "queued" ,
"deliveryId" : "delivery-001"
}
]
}
}
Common Use Cases
Single Product, Multiple Providers
Deliver one product to multiple DSPs:
POST /product/prod-123/deliveries
{
"providers" : [
"spotify-ddex-sftp" ,
"apple-ddex-api" ,
"youtube-ddex-api"
]
}
Validation Only (Dry-Run)
Check if product is ready without delivering:
POST /product/prod-123/deliveries
{
"providers" : [ "spotify-ddex-sftp" ],
"autoDeliver" : false
}
Custom DDEX Settings
Override default settings for specific delivery:
POST /product/prod-123/deliveries
{
"providers" : [ "spotify-ddex-sftp" ],
"settings" : {
"releaseType" : "Single",
"recordLabel" : "Custom Label",
"copyrightYear" : 2024
}
}
Troubleshooting
Product must have a UPC to be delivered
Error: 400 Bad Request - Product must have a UPC to be deliveredCause: The product doesn’t have a UPC assigned, which is required for all DSP deliveries.Solution: Add a UPC to your product before attempting delivery:curl -X PUT https://api.royalti.io/product/prod-123 \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"upc": "123456789012"}'
Provider not available for tenant
Error: Provider 'xyz-provider' not available for tenantCause: The provider you’re trying to use isn’t configured for your workspace, or the provider ID is incorrect.Solution:
Check available providers for your workspace:
GET /product/delivery-providers
Verify you’re using the correct provider ID from the response
Contact support if you need access to a specific provider
Missing required field errors
Error: Validation fails with missing field messagesExample: {
"provider" : "spotify-ddex-sftp" ,
"isValid" : false ,
"errors" : [ "Missing required field: releaseDate" ],
"suggestions" : {
"releaseDate" : "2024-09-01"
}
}
Solution: The validation response includes specific field suggestions. Update your product with the missing data:curl -X PUT https://api.royalti.io/product/prod-123 \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"releaseDate": "2024-09-01"}'
Error: 404 Not Found - Product not foundCause: The product ID doesn’t exist or doesn’t belong to your workspace.Solution:
Verify the product ID is correct
Check the product exists in your workspace:
Ensure you’re using the correct API token for your workspace
Delivery stuck in processing
Issue: Delivery status remains “processing” for extended periodCause: Large files, slow provider connections, or provider-side delays.Solution:
Check delivery logs for detailed progress:
GET /product/prod-123/deliveries/delivery-456
Typical delivery times:
Small releases (1-5 tracks): 5-15 minutes
Albums (6-20 tracks): 15-45 minutes
Large catalogs: 1-3 hours
If stuck for more than expected time, contact support with the delivery ID
Maximum retry attempts reached
Error: 400 Bad Request - Delivery has reached maximum retry attempts (3)Cause: The delivery has failed 3 times and can’t be retried automatically.Solution:
Review delivery logs to understand why it’s failing:
GET /product/prod-123/deliveries/delivery-456
Fix the underlying issue (usually metadata or file problems)
Create a new delivery instead of retrying:
POST /product/prod-123/deliveries
Best Practices
Always Validate First Run validation before delivery to catch issues early and avoid failed deliveries. This saves time and API quota.
Monitor Progress Poll delivery status regularly to track progress and handle failures promptly. Set up webhooks for real-time notifications.
Use Batch Delivery For multiple products, use batch delivery to reduce API calls and improve efficiency. Process up to 100 products at once.
Handle Retries Implement automatic retry logic for transient failures with exponential backoff. Don’t retry immediately on failure.
Complete Integration Example
Here’s a full workflow example with error handling and monitoring:
const axios = require ( 'axios' );
async function deliverProduct ( productId , providers ) {
const baseURL = 'https://api.royalti.io' ;
const headers = {
'Authorization' : `Bearer ${ API_TOKEN } ` ,
'Content-Type' : 'application/json'
};
try {
// Step 1: Validate
console . log ( 'Validating product...' );
const validation = await axios . post (
` ${ baseURL } /product/ ${ productId } /deliveries/validate` ,
{ providers },
{ headers }
);
if ( ! validation . data . data . validations . every ( v => v . isValid )) {
console . error ( 'Validation failed:' , validation . data );
return ;
}
// Step 2: Deliver
console . log ( 'Initiating delivery...' );
const delivery = await axios . post (
` ${ baseURL } /product/ ${ productId } /deliveries` ,
{ providers , autoDeliver: true },
{ headers }
);
const deliveryIds = delivery . data . data . deliveries . map ( d => d . deliveryId );
console . log ( 'Deliveries initiated:' , deliveryIds );
// Step 3: Monitor
console . log ( 'Monitoring delivery status...' );
for ( const deliveryId of deliveryIds ) {
await monitorDelivery ( productId , deliveryId );
}
} catch ( error ) {
console . error ( 'Delivery error:' , error . response ?. data || error . message );
}
}
async function monitorDelivery ( productId , deliveryId ) {
const baseURL = 'https://api.royalti.io' ;
const headers = { 'Authorization' : `Bearer ${ API_TOKEN } ` };
let attempts = 0 ;
const maxAttempts = 60 ; // 5 minutes with 5-second intervals
while ( attempts < maxAttempts ) {
const response = await axios . get (
` ${ baseURL } /product/ ${ productId } /deliveries/ ${ deliveryId } ` ,
{ headers }
);
const { status , progress } = response . data . data ;
console . log ( `Delivery ${ deliveryId } : ${ status } ( ${ progress } %)` );
if ( status === 'delivered' ) {
console . log ( 'Delivery completed successfully!' );
return ;
}
if ([ 'failed' , 'error' , 'rejected' ]. includes ( status )) {
console . error ( 'Delivery failed:' , response . data . data );
return ;
}
await new Promise ( resolve => setTimeout ( resolve , 5000 ));
attempts ++ ;
}
console . warn ( 'Monitoring timeout reached' );
}
// Usage
deliverProduct ( 'prod-123' , [ 'spotify-ddex-sftp' , 'apple-ddex-api' ]);
import requests
import time
def deliver_product ( product_id , providers ):
base_url = 'https://api.royalti.io'
headers = {
'Authorization' : f 'Bearer { API_TOKEN } ' ,
'Content-Type' : 'application/json'
}
try :
# Step 1: Validate
print ( 'Validating product...' )
validation = requests.post(
f ' { base_url } /product/ { product_id } /deliveries/validate' ,
headers = headers,
json = { 'providers' : providers}
)
validation.raise_for_status()
if not all (v[ 'isValid' ] for v in validation.json()[ 'data' ][ 'validations' ]):
print ( f 'Validation failed: { validation.json() } ' )
return
# Step 2: Deliver
print ( 'Initiating delivery...' )
delivery = requests.post(
f ' { base_url } /product/ { product_id } /deliveries' ,
headers = headers,
json = { 'providers' : providers, 'autoDeliver' : True }
)
delivery.raise_for_status()
delivery_ids = [d[ 'deliveryId' ] for d in delivery.json()[ 'data' ][ 'deliveries' ]]
print ( f 'Deliveries initiated: { delivery_ids } ' )
# Step 3: Monitor
print ( 'Monitoring delivery status...' )
for delivery_id in delivery_ids:
monitor_delivery(product_id, delivery_id, headers, base_url)
except requests.exceptions.RequestException as error:
print ( f 'Delivery error: { error.response.json() if error.response else str (error) } ' )
def monitor_delivery ( product_id , delivery_id , headers , base_url ):
attempts = 0
max_attempts = 60 # 5 minutes with 5-second intervals
while attempts < max_attempts:
response = requests.get(
f ' { base_url } /product/ { product_id } /deliveries/ { delivery_id } ' ,
headers = headers
)
response.raise_for_status()
data = response.json()[ 'data' ]
status = data[ 'status' ]
progress = data[ 'progress' ]
print ( f "Delivery { delivery_id } : { status } ( { progress } %)" )
if status == 'delivered' :
print ( 'Delivery completed successfully!' )
return
if status in [ 'failed' , 'error' , 'rejected' ]:
print ( f 'Delivery failed: { data } ' )
return
time.sleep( 5 )
attempts += 1
print ( 'Monitoring timeout reached' )
# Usage
deliver_product( 'prod-123' , [ 'spotify-ddex-sftp' , 'apple-ddex-api' ])
Support
Need help with product delivery?