Overview
The Royalti.io Splits system enables you to distribute revenue among multiple parties based on configurable rules. This guide covers manual split creation, automated splits from artist defaults, temporal split management, and advanced features like coverage analysis and conditional splits.
What Are Splits?
Splits define how revenue from music assets and products is distributed among collaborators, rights holders, and other parties. Each split configuration specifies:
- Who receives revenue (users and their share percentages)
- What generates the revenue (asset, product, or both)
- When the split is active (date ranges or permanent)
- Where it applies (territories, DSPs, usage types)
Key Benefits
- Flexible Configuration: Asset-level, product-level, or combined scopes
- Temporal Management: Different splits for different time periods
- Conditional Splits: Territory, DSP, and usage-type specific distributions
- Automated Creation: Auto-apply splits from artist defaults
- Coverage Analysis: Identify gaps and overlaps in temporal coverage
- 100% Validation: Ensures shares always total exactly 100%
Prerequisites
Required Permissions
- Users: Can view splits they’re included in
- Admins: Full access to create, update, and delete all splits
Authentication
All split endpoints require authentication with a Bearer token.
const token = 'your_jwt_token';
const headers = {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
};
import requests
token = 'your_jwt_token'
headers = {
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'
}
Core Concepts
Split Configuration Levels
Splits can be configured at three different scopes:
| Level | AssetId | ProductId | Coverage |
| Asset-level | ✓ | null | Asset across all product contexts |
| Product-level | null | ✓ | Entire product (all tracks) |
| Product-Asset | ✓ | ✓ | Specific asset within specific product only |
Only splits with the exact same configuration (AssetId + ProductId + type) can overlap or conflict. Different configurations are independent.
Revenue Types
Splits can be categorized by revenue stream type:
' ' (empty string) - Default/general revenue
'Publishing' - Publishing and mechanical rights
'YouTube' - YouTube-specific revenue
'Live' - Live performance revenue
Different revenue types for the same asset/product are independent. Publishing and YouTube splits don’t conflict with each other.
Date Range Behavior
Temporal splits use exclusive end dates following PostgreSQL DATERANGE format [startDate, endDate):
- startDate: INCLUSIVE (period starts on this date)
- endDate: EXCLUSIVE (period ends before this date)
{
"startDate": "2025-01-01", // Starts January 1, 2025
"endDate": "2025-03-31" // Ends March 30, 2025 (March 31 is excluded)
}
// This split covers: Jan 1 through Mar 30
// Next split can start: Mar 31 (no gap, no overlap)
Share Validation
All split shares must total exactly 100%. The system validates this before creation and will reject splits that don’t meet this requirement.
{
"split": [
{ "user": "user-uuid-1", "share": 60 },
{ "user": "user-uuid-2", "share": 40 }
]
}
// Total: 100% ✓
{
"split": [
{ "user": "user-uuid-1", "share": 60 },
{ "user": "user-uuid-2", "share": 30 }
]
}
// Total: 90% ✗ - Will be rejected
Quick Start: Creating Your First Split
Prepare Asset or Product
Identify the asset or product you want to split revenue for.curl -X GET 'https://api.royalti.io/asset' \
-H 'Authorization: Bearer YOUR_TOKEN'
Identify Users
Get the UUIDs of users who will receive revenue shares.curl -X GET 'https://api.royalti.io/user' \
-H 'Authorization: Bearer YOUR_TOKEN'
Create the Split
Submit a POST request to create the split configuration.const response = await fetch('https://api.royalti.io/split', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
asset: 'asset-uuid-here',
product: null, // Asset-level split
type: 'Publishing',
name: 'Publishing Split - Song Title',
split: [
{ user: 'user-uuid-1', share: 60 },
{ user: 'user-uuid-2', share: 40 }
]
})
});
const data = await response.json();
console.log('Split created:', data);
import requests
response = requests.post(
'https://api.royalti.io/split',
headers=headers,
json={
'asset': 'asset-uuid-here',
'product': None, # Asset-level split
'type': 'Publishing',
'name': 'Publishing Split - Song Title',
'split': [
{'user': 'user-uuid-1', 'share': 60},
{'user': 'user-uuid-2', 'share': 40}
]
}
)
data = response.json()
print('Split created:', data)
Verify Creation
Retrieve the split to confirm it was created successfully.curl -X GET 'https://api.royalti.io/split/SPLIT_ID' \
-H 'Authorization: Bearer YOUR_TOKEN'
Manual Split Workflows
Creating Permanent Splits
Permanent splits have no date restrictions and apply indefinitely.
const response = await fetch('https://api.royalti.io/split', {
method: 'POST',
headers: headers,
body: JSON.stringify({
asset: 'asset-uuid',
product: null,
type: 'Publishing',
name: 'Permanent Publishing Split',
split: [
{ user: 'songwriter-uuid', share: 50 },
{ user: 'producer-uuid', share: 30 },
{ user: 'publisher-uuid', share: 20 }
],
memo: 'Standard publishing agreement'
})
});
Permanent splits (without dates) serve as fallback coverage when no temporal splits match a given period.
Creating Temporal Splits
Temporal splits are active only during specified date ranges.
const response = await fetch('https://api.royalti.io/split', {
method: 'POST',
headers: headers,
body: JSON.stringify({
asset: 'asset-uuid',
product: null,
type: 'Publishing',
name: 'Q1 2025 Publishing Split',
startDate: '2025-01-01',
endDate: '2025-04-01', // Covers through March 31
split: [
{ user: 'user-uuid-1', share: 70 },
{ user: 'user-uuid-2', share: 30 }
]
})
});
Adding Split Conditions
Split conditions allow you to apply splits only when specific criteria are met.
Territory-Specific Splits
const response = await fetch('https://api.royalti.io/split', {
method: 'POST',
headers: headers,
body: JSON.stringify({
asset: 'asset-uuid',
product: null,
type: 'Publishing',
name: 'African Markets Split',
split: [
{ user: 'local-partner-uuid', share: 50 },
{ user: 'main-artist-uuid', share: 50 }
],
conditions: [
{
mode: 'include',
territories: ['NG', 'KE', 'ZA', 'GH'],
memo: 'African territories only'
}
]
})
});
DSP-Specific Splits
const response = await fetch('https://api.royalti.io/split', {
method: 'POST',
headers: headers,
body: JSON.stringify({
asset: 'asset-uuid',
product: null,
type: 'YouTube',
name: 'YouTube Exclusive Split',
split: [
{ user: 'content-creator-uuid', share: 60 },
{ user: 'rights-holder-uuid', share: 40 }
],
conditions: [
{
mode: 'include',
dsps: ['youtube'],
memo: 'YouTube revenue only'
}
]
})
});
Multi-Dimensional Conditions
const response = await fetch('https://api.royalti.io/split', {
method: 'POST',
headers: headers,
body: JSON.stringify({
asset: 'asset-uuid',
product: null,
type: 'Publishing',
name: 'North America Streaming Split',
split: [
{ user: 'user-uuid-1', share: 60 },
{ user: 'user-uuid-2', share: 40 }
],
conditions: [
{
mode: 'include',
territories: ['US', 'CA'],
dsps: ['spotify', 'apple'],
usageTypes: ['stream'],
memo: 'US/Canada streaming on Spotify and Apple Music only'
}
]
})
});
All specified dimensions in a condition must match for the condition to pass. This is an AND operation, not OR.
Exclude Mode
Use mode: 'exclude' to prevent splits from applying in certain contexts.
{
conditions: [
{
mode: 'exclude',
territories: ['CN', 'KP'],
memo: 'Exclude China and North Korea'
}
]
}
Updating Splits
Update existing splits while maintaining validation rules.
const response = await fetch(`https://api.royalti.io/split/${splitId}`, {
method: 'PUT',
headers: headers,
body: JSON.stringify({
asset: 'asset-uuid',
product: null,
type: 'Publishing',
name: 'Updated Split Name',
startDate: '2025-01-01',
endDate: '2025-04-01',
split: [
{ user: 'user-uuid-1', share: 65 }, // Changed from 60%
{ user: 'user-uuid-2', share: 35 } // Changed from 40%
]
})
});
When you update split shares, the system automatically notifies users who were added or removed from the split.
Deleting Splits
const response = await fetch(`https://api.royalti.io/split/${splitId}`, {
method: 'DELETE',
headers: headers
});
console.log(await response.json());
// { "message": "Split was deleted successfully." }
Bulk Operations
Bulk Delete Splits
Delete multiple splits at once by providing an array of split IDs.
const response = await fetch('https://api.royalti.io/split/bulk/delete', {
method: 'POST',
headers: headers,
body: JSON.stringify({
ids: [
'split-uuid-1',
'split-uuid-2',
'split-uuid-3'
]
})
});
const data = await response.json();
console.log(`Deleted ${data.deletedCount} splits`);
Bulk Delete Catalog Splits
Delete all splits associated with specific assets or products.
Node.js - Delete Asset Splits
const response = await fetch('https://api.royalti.io/split/bulk/catalog-splits', {
method: 'DELETE',
headers: headers,
body: JSON.stringify({
type: 'asset',
ids: [
'asset-uuid-1',
'asset-uuid-2',
'asset-uuid-3'
]
})
});
const data = await response.json();
console.log(`Deleted ${data.totalSplitsDeleted} splits across ${data.totalProcessed} assets`);
Node.js - Delete Product Splits
const response = await fetch('https://api.royalti.io/split/bulk/catalog-splits', {
method: 'DELETE',
headers: headers,
body: JSON.stringify({
type: 'product',
ids: [
'product-uuid-1',
'product-uuid-2'
]
})
});
- Asset type: Deletes only asset-level splits (where ProductId is null)
- Product type: Deletes ALL splits associated with the product
Automated Splits Creation
Artist Default Splits
Artist default splits allow you to define standard revenue distributions that automatically apply to new assets and products.
Setting Up Artist Defaults
When creating or updating an artist, you can define default splits for different revenue types:
const response = await fetch('https://api.royalti.io/artist', {
method: 'POST',
headers: headers,
body: JSON.stringify({
artistName: 'The Wave Collective',
split: {
// Default split for general revenue
default: [
{ user: 'artist-uuid', share: 70 },
{ user: 'manager-uuid', share: 20 },
{ user: 'label-uuid', share: 10 }
],
// Publishing-specific split
publishing: [
{ user: 'songwriter-uuid', share: 50 },
{ user: 'producer-uuid', share: 30 },
{ user: 'publisher-uuid', share: 20 }
],
// YouTube-specific split
video: [
{ user: 'content-creator-uuid', share: 60 },
{ user: 'rights-holder-uuid', share: 40 }
],
// Live performance split
gig: [
{ user: 'performer-uuid', share: 80 },
{ user: 'venue-uuid', share: 20 }
]
}
})
});
Each revenue type must total exactly 100%. You can define splits for some types and omit others.
Revenue Type Mapping
| Split Type | Revenue Type | Use Case |
default | ' ' (empty) | General revenue, fallback |
publishing | 'Publishing' | Publishing and mechanical rights |
video | 'YouTube' | YouTube and video platform revenue |
gig | 'Live' | Live performance and touring revenue |
Auto-Creation Workflow
When you create assets or products for an artist with default splits, the system can automatically create split configurations.
Using Default Splits Endpoint
The /split/default endpoint provides flexible split creation with automatic fallback to artist defaults.
Priority Hierarchy
The system applies splits in this order:
- Manual splits (provided in request body) - HIGHEST
- Artist default splits (fallback from artist settings)
- Error (if neither available)
Create with Manual Splits
Override artist defaults by providing splits explicitly.const response = await fetch('https://api.royalti.io/split/default', {
method: 'POST',
headers: headers,
body: JSON.stringify({
assetId: 'asset-uuid',
type: 'Publishing',
split: [
{ user: 'user-uuid-1', share: 60 },
{ user: 'user-uuid-2', share: 40 }
]
})
});
const data = await response.json();
console.log('Source:', data.source); // "manual"
Create with Artist Defaults
Omit the split array to use artist’s default configuration.const response = await fetch('https://api.royalti.io/split/default', {
method: 'POST',
headers: headers,
body: JSON.stringify({
productId: 'product-uuid',
type: 'video'
})
});
const data = await response.json();
console.log('Source:', data.source); // "artist_default"
console.log('Applied split:', data.split);
Auto-Creation During Asset/Product Creation
The system can automatically create splits when assets or products are created:
Node.js - Asset Creation with Auto-Split
const response = await fetch('https://api.royalti.io/asset', {
method: 'POST',
headers: headers,
body: JSON.stringify({
title: 'Summer Nights',
displayArtist: 'The Wave Collective',
isrc: 'USCM51500001',
ArtistId: 'artist-uuid-with-defaults',
// ... other asset fields
})
});
// System checks artist defaults and auto-creates splits if:
// 1. Artist has default splits configured
// 2. Default splits total 100%
// 3. Split type matches context (e.g., 'video' for video assets)
Auto-creation is triggered automatically during asset/product creation if artist defaults are properly configured.
Manual vs Automated Split Creation
| Feature | Manual Creation | Automated from Defaults |
| Flexibility | Full control over shares | Inherits artist configuration |
| Speed | Requires manual input | Instant application |
| Consistency | Varies per creation | Consistent across catalog |
| Override | N/A | Can override with manual splits |
| Use Case | Unique agreements | Standard label/artist deals |
Best Practices for Defaults
- Set Up Early: Configure artist defaults before creating catalog items
- Type-Specific: Define different splits for publishing vs streaming vs live
- Validation: Ensure each type totals exactly 100%
- Documentation: Use memo fields to explain split rationale
- Review Regularly: Update defaults as contracts change
Split Conditions & Matching
Understanding Condition Logic
Split conditions use a two-phase evaluation system:
- Exclusions checked first (any match = reject split)
- Inclusions checked next (at least one must match)
- No conditions = always match
// Phase 1: Check exclusions
for (const condition of excludeConditions) {
if (matchesCondition(condition, context)) {
return false; // Reject immediately
}
}
// Phase 2: Check inclusions
if (includeConditions.length > 0) {
return includeConditions.some(condition =>
matchesCondition(condition, context)
);
}
// Phase 3: No conditions = match
return true;
Matching Splits
Use the /split/match endpoint to find splits that apply to specific revenue contexts.
const response = await fetch('https://api.royalti.io/split/match', {
method: 'POST',
headers: headers,
body: JSON.stringify({
territory: 'NG',
dsp: 'spotify',
usageType: 'stream',
date: '2025-02-15'
})
});
const data = await response.json();
console.log(`Found ${data.count} matching splits`);
data.splits.forEach(split => {
console.log(`- ${split.name}: ${split.SplitShares.length} parties`);
});
response = requests.post(
'https://api.royalti.io/split/match',
headers=headers,
json={
'territory': 'NG',
'dsp': 'spotify',
'usageType': 'stream',
'date': '2025-02-15'
}
)
data = response.json()
print(f"Found {data['count']} matching splits")
Condition Matching Rules
| Dimension | Match Logic |
| territories | Context territory must be in array |
| dsps | Context DSP must be in array |
| usageTypes | Context usage type must be in array |
| customDimension | Context custom field must match values |
| Multiple dimensions | ALL must match (AND operation) |
Custom Dimensions
Add custom filtering criteria beyond standard dimensions.
const response = await fetch('https://api.royalti.io/split', {
method: 'POST',
headers: headers,
body: JSON.stringify({
asset: 'asset-uuid',
product: null,
type: 'Publishing',
name: 'Premium Tier Split',
split: [
{ user: 'user-uuid-1', share: 60 },
{ user: 'user-uuid-2', share: 40 }
],
conditions: [
{
mode: 'include',
customDimension: 'subscription_tier',
customValues: ['premium', 'platinum'],
memo: 'Premium subscription tiers only'
}
]
})
});
Complex Condition Scenarios
Multiple Include Conditions
Split matches if any include condition passes (OR logic between conditions).
{
conditions: [
{
mode: 'include',
territories: ['US', 'CA'],
memo: 'North America'
},
{
mode: 'include',
territories: ['GB', 'DE', 'FR'],
memo: 'Major European markets'
}
]
}
// Matches: US, CA, GB, DE, or FR
Combining Include and Exclude
Exclude conditions always take precedence.
{
conditions: [
{
mode: 'include',
territories: ['US', 'CA', 'MX'],
memo: 'All of North America'
},
{
mode: 'exclude',
territories: ['MX'],
memo: 'Except Mexico (separate agreement)'
}
]
}
// Matches: US and CA only (MX is excluded despite being included)
Temporal Splits & Coverage Analysis
Understanding Temporal Coverage
Temporal coverage analysis helps you understand how splits cover different time periods and identify gaps in coverage.
Requesting Coverage Analysis
Add includeCoverage=true to GET requests:
Get Single Split with Coverage
curl -X GET 'https://api.royalti.io/split/SPLIT_ID?includeCoverage=true' \
-H 'Authorization: Bearer YOUR_TOKEN'
Get All Splits with Coverage
curl -X GET 'https://api.royalti.io/split?includeCoverage=true' \
-H 'Authorization: Bearer YOUR_TOKEN'
Coverage Response Structure
{
"coverage": {
"overlapping": [
{
"id": "overlap-split-uuid",
"name": "Conflicting Split",
"type": "Publishing",
"startDate": "2025-02-01",
"endDate": "2025-04-01"
}
],
"adjacent": {
"predecessor": {
"id": "previous-split-uuid",
"name": "Q4 2024 Split",
"type": "Publishing",
"endDate": "2025-01-01"
},
"successor": {
"id": "next-split-uuid",
"name": "Q2 2025 Split",
"type": "Publishing",
"startDate": "2025-04-01"
}
},
"gaps": [
{
"type": "before",
"description": "No coverage before split start date",
"startDate": null,
"endDate": "2025-01-01"
},
{
"type": "after",
"description": "No coverage after split end date",
"startDate": "2025-04-01",
"endDate": null
}
],
"defaultSplits": [
{
"id": "default-split-uuid",
"name": "Permanent Publishing Split",
"type": "Publishing"
}
],
"summary": {
"hasOverlaps": false,
"hasPredecessor": true,
"hasSuccessor": true,
"hasGaps": false,
"isOpenEnded": false,
"hasDefaultSplit": true
}
}
}
Coverage Analysis Components
1. Overlapping Splits
Identifies splits with conflicting date ranges.
Overlapping splits indicate a configuration error. The system prevents creating overlaps, but existing data may have them from legacy imports.
if (split.coverage.summary.hasOverlaps) {
console.log('Warning: This split overlaps with:');
split.coverage.overlapping.forEach(overlap => {
console.log(`- ${overlap.name} (${overlap.startDate} to ${overlap.endDate})`);
});
}
2. Adjacent Splits (Succession)
Identifies splits that start when another ends (no gap, no overlap).
// Predecessor: 2024-10-01 to 2025-01-01
// Current: 2025-01-01 to 2025-04-01 ← Starts when predecessor ends
// Successor: 2025-04-01 to 2025-07-01 ← Starts when current ends
Adjacent splits are the recommended pattern for changing split terms over time.
3. Gap Detection
Identifies periods without split coverage.
| Gap Type | Description | Start Date | End Date |
| before | Before split starts | null (infinite past) | Split start date |
| after | After split ends | Split end date | null (infinite future) |
| between | Between two splits | Previous split end | Next split start |
| infinite | Permanent gaps | null | null |
if (split.coverage.summary.hasGaps) {
split.coverage.gaps.forEach(gap => {
if (gap.type === 'before') {
console.log(`No coverage before ${gap.endDate}`);
} else if (gap.type === 'after') {
console.log(`No coverage after ${gap.startDate}`);
}
});
}
4. Default Splits (Fallback)
If default splits (no dates) exist, they provide fallback coverage for gaps.
if (split.coverage.summary.hasDefaultSplit) {
console.log('Default split provides coverage for any gaps');
console.log('No periods will be uncovered');
}
Planning Split Succession
Creating Successor Splits
Build chains of temporal splits with no gaps:
const q1Split = await fetch('https://api.royalti.io/split', {
method: 'POST',
headers: headers,
body: JSON.stringify({
asset: 'asset-uuid',
product: null,
type: 'Publishing',
name: 'Q1 2025 Publishing',
startDate: '2025-01-01',
endDate: '2025-04-01', // Ends March 31
split: [
{ user: 'user-uuid-1', share: 60 },
{ user: 'user-uuid-2', share: 40 }
]
})
});
Node.js - Q2 Split (Successor)
const q2Split = await fetch('https://api.royalti.io/split', {
method: 'POST',
headers: headers,
body: JSON.stringify({
asset: 'asset-uuid',
product: null,
type: 'Publishing',
name: 'Q2 2025 Publishing',
startDate: '2025-04-01', // Starts exactly when Q1 ends
endDate: '2025-07-01', // Ends June 30
split: [
{ user: 'user-uuid-1', share: 50 }, // Changed terms
{ user: 'user-uuid-2', share: 30 },
{ user: 'user-uuid-3', share: 20 } // New party added
]
})
});
Best Practices for Succession
- Use Exclusive End Dates: Remember end dates are exclusive, so start next split on end date of previous
- Check Coverage Before: Use coverage analysis to verify no gaps
- Maintain Type Consistency: Keep the same revenue type across succession
- Document Changes: Use memo field to explain why split terms changed
- Plan Ahead: Create future splits in advance to avoid coverage gaps
Advanced Features
Retrieving Splits with Filters
The GET endpoint supports extensive filtering:
const params = new URLSearchParams({
asset: 'asset-uuid',
type: 'Publishing',
user: 'user-uuid', // Splits where this user has a share
page: 1,
size: 20,
sort: 'createdAt',
order: 'desc',
includeCoverage: true
});
const response = await fetch(`https://api.royalti.io/split?${params}`, {
method: 'GET',
headers: headers
});
const data = await response.json();
console.log(`Total: ${data.totalItems}`);
console.log(`Showing: ${data.Splits.length}`);
Search Functionality
Use the q parameter for text search across split names:
const response = await fetch(
'https://api.royalti.io/split?q=Nigeria&include=count',
{
method: 'GET',
headers: headers
}
);
Batch Processing
When working with many splits, use pagination and filtering to improve performance:
Node.js - Efficient Pagination
async function getAllSplitsForAsset(assetId) {
const allSplits = [];
let page = 1;
const size = 100; // Larger page size for fewer requests
let hasMore = true;
while (hasMore) {
const params = new URLSearchParams({
asset: assetId,
page: page,
size: size,
include: 'count'
});
const response = await fetch(`https://api.royalti.io/split?${params}`, {
method: 'GET',
headers: headers
});
const data = await response.json();
allSplits.push(...data.Splits);
hasMore = data.Splits.length === size;
page++;
}
return allSplits;
}
Bulk Operations
Always use bulk endpoints for multiple operations:
// Inefficient: Multiple individual deletes
for (const splitId of splitIds) {
await fetch(`https://api.royalti.io/split/${splitId}`, {
method: 'DELETE',
headers: headers
});
}
// Efficient: Single bulk delete
await fetch('https://api.royalti.io/split/bulk/delete', {
method: 'POST',
headers: headers,
body: JSON.stringify({ ids: splitIds })
});
Common Use Cases
1. Artist 360 Deal
Artist gets percentage of all revenue types, manager gets cut from everything:
// Set up artist defaults for 360 deal
const artist = await fetch('https://api.royalti.io/artist', {
method: 'POST',
headers: headers,
body: JSON.stringify({
artistName: 'Rising Star',
split: {
default: [
{ user: 'artist-uuid', share: 80 },
{ user: 'manager-uuid', share: 20 }
],
publishing: [
{ user: 'artist-uuid', share: 80 },
{ user: 'manager-uuid', share: 20 }
],
video: [
{ user: 'artist-uuid', share: 80 },
{ user: 'manager-uuid', share: 20 }
],
gig: [
{ user: 'artist-uuid', share: 80 },
{ user: 'manager-uuid', share: 20 }
]
}
})
});
// All new assets/products automatically get these splits
2. Co-Write Publishing Split
Multiple songwriters split publishing revenue:
await fetch('https://api.royalti.io/split', {
method: 'POST',
headers: headers,
body: JSON.stringify({
asset: 'song-uuid',
product: null,
type: 'Publishing',
name: 'Co-Write Publishing - Song Title',
split: [
{ user: 'songwriter-1-uuid', share: 40 },
{ user: 'songwriter-2-uuid', share: 40 },
{ user: 'producer-uuid', share: 20 }
],
memo: 'Standard co-write agreement, producer gets 20% for beat'
})
});
3. Territory-Based Licensing Deal
Different partners for different territories:
// North America
await fetch('https://api.royalti.io/split', {
method: 'POST',
headers: headers,
body: JSON.stringify({
asset: 'asset-uuid',
product: null,
type: 'Publishing',
name: 'North America License',
split: [
{ user: 'us-partner-uuid', share: 50 },
{ user: 'artist-uuid', share: 50 }
],
conditions: [
{
mode: 'include',
territories: ['US', 'CA', 'MX']
}
]
})
});
// Europe
await fetch('https://api.royalti.io/split', {
method: 'POST',
headers: headers,
body: JSON.stringify({
asset: 'asset-uuid',
product: null,
type: 'Publishing',
name: 'European License',
split: [
{ user: 'eu-partner-uuid', share: 50 },
{ user: 'artist-uuid', share: 50 }
],
conditions: [
{
mode: 'include',
territories: ['GB', 'DE', 'FR', 'ES', 'IT']
}
]
})
});
4. Temporary Feature Split
Featuring artist gets share for limited time:
await fetch('https://api.royalti.io/split', {
method: 'POST',
headers: headers,
body: JSON.stringify({
asset: 'collab-track-uuid',
product: null,
type: 'Publishing',
name: 'Feature Artist Split - 12 Months',
startDate: '2025-01-01',
endDate: '2026-01-01', // Exactly one year
split: [
{ user: 'main-artist-uuid', share: 60 },
{ user: 'featured-artist-uuid', share: 40 }
],
memo: 'Feature artist gets 40% for first year, reverts to main artist after'
})
});
// Create fallback split for after feature period
await fetch('https://api.royalti.io/split', {
method: 'POST',
headers: headers,
body: JSON.stringify({
asset: 'collab-track-uuid',
product: null,
type: 'Publishing',
name: 'Post-Feature Split',
startDate: '2026-01-01', // Starts when feature ends
split: [
{ user: 'main-artist-uuid', share: 100 }
]
})
});
5. Label Partnership with Recoupment
Label gets higher percentage until recoupment, then artist gets more:
await fetch('https://api.royalti.io/split', {
method: 'POST',
headers: headers,
body: JSON.stringify({
product: 'album-uuid',
asset: null,
type: 'Publishing',
name: 'Pre-Recoupment Split',
startDate: '2025-01-01',
endDate: '2026-01-01', // Expected recoupment date
split: [
{ user: 'label-uuid', share: 70 },
{ user: 'artist-uuid', share: 30 }
],
memo: 'Label 70% until $100,000 recouped'
})
});
Node.js - Post-Recoupment
await fetch('https://api.royalti.io/split', {
method: 'POST',
headers: headers,
body: JSON.stringify({
product: 'album-uuid',
asset: null,
type: 'Publishing',
name: 'Post-Recoupment Split',
startDate: '2026-01-01',
split: [
{ user: 'artist-uuid', share: 80 },
{ user: 'label-uuid', share: 20 }
],
memo: 'Artist 80% after recoupment reached'
})
});
6. Sample Clearance Split
Original artist gets share when their work is sampled:
await fetch('https://api.royalti.io/split', {
method: 'POST',
headers: headers,
body: JSON.stringify({
asset: 'new-track-with-sample-uuid',
product: null,
type: 'Publishing',
name: 'Sample Clearance Split',
split: [
{ user: 'new-artist-uuid', share: 60 },
{ user: 'original-artist-uuid', share: 30 }, // Sample clearance
{ user: 'publisher-uuid', share: 10 }
],
memo: 'Contains sample from [Original Song] - 30% clearance fee'
})
});
Integration with Accounting
How Splits Affect Earnings
When royalty revenue is processed, the accounting system:
- Fetches applicable splits for each revenue entry
- Matches conditions (territory, DSP, usage type, date)
- Applies split percentages to revenue amount
- Creates user earnings records for each split party
- Aggregates totals for dashboard display
// Revenue entry: $1000 from Spotify Nigeria on 2025-02-15
// Matched split: 60% user A, 40% user B
const revenue = 1000;
const matchedSplit = {
SplitShares: [
{ TenantUserId: 'user-a-uuid', Share: 60 },
{ TenantUserId: 'user-b-uuid', Share: 40 }
]
};
// Calculate earnings
matchedSplit.SplitShares.forEach(share => {
const earnings = (revenue * share.Share) / 100;
console.log(`User ${share.TenantUserId}: $${earnings}`);
});
// Result:
// User user-a-uuid: $600
// User user-b-uuid: $400
Cache Invalidation
The system automatically invalidates accounting caches when splits change:
- Create: Invalidates cache for all users in split
- Update: Invalidates cache for old and new users
- Delete: Invalidates cache for all users in split
Cache invalidation ensures accounting calculations always use the latest split configurations.
Split changes trigger accounting recalculation:
- Small catalogs (< 1000 assets): Instant recalculation
- Medium catalogs (1000-10,000): 1-5 seconds
- Large catalogs (> 10,000): Background job (5-30 seconds)
Avoid frequent bulk split changes as they trigger expensive recalculations. Plan changes and execute them together.
Best Practices
1. Planning Temporal Splits
✓ Do:
- Plan entire succession chain before creating first split
- Use exclusive end dates to create adjacent splits
- Create default split as fallback for gaps
- Document reason for split changes in memo field
- Use coverage analysis to verify no gaps
✗ Don’t:
- Create random date ranges without planning
- Leave gaps between split periods
- Forget to create default split for permanent coverage
- Change split terms without creating new temporal split
2. Naming Conventions
Use clear, descriptive names:
"Q1 2025 Publishing Split - Artist Name"
"YouTube Revenue Split - Territory: Nigeria"
"Feature Split - Main Artist ft. Guest (Jan-Dec 2025)"
"Post-Recoupment Label Deal - Album Title"
"Split 1"
"New split"
"test"
"Publishing"
3. Condition Design
✓ Do:
- Use specific territories when licensing to partners
- Use DSP conditions for platform-specific deals
- Document condition logic in memo field
- Test conditions with
/split/match endpoint
- Keep conditions simple and understandable
✗ Don’t:
- Create overly complex condition combinations
- Use conditions when simple permanent split works
- Forget that all dimensions must match (AND logic)
- Rely on exclude mode when include would be clearer
4. Managing Split Changes
✓ Do:
- Create new temporal split instead of updating existing
- Maintain audit trail with memo fields
- Notify all parties when splits change
- Use coverage analysis to verify changes
- Plan transition dates carefully
✗ Don’t:
- Update historical splits (breaks accounting)
- Delete splits with revenue history
- Change split terms without user notification
- Forget about outstanding royalty periods
5. Documentation
✓ Do:
- Use memo field to explain split rationale
- Document contract references
- Note important dates or milestones
- Keep track of recoupment status
- Link to external agreements
✗ Don’t:
- Leave memo fields empty
- Use cryptic abbreviations
- Forget to update documentation when terms change
Troubleshooting
Common Validation Errors
Error: “Split must equal 100”
{
"split": [
{ "user": "uuid-1", "share": 60 },
{ "user": "uuid-2", "share": 30 }
]
}
// Total: 90% ✗
{
"split": [
{ "user": "uuid-1", "share": 60 },
{ "user": "uuid-2", "share": 40 }
]
}
// Total: 100% ✓
Error: “Split already exists with same parameters”
Cause: Attempting to create duplicate split configuration.
Solution: Either update existing split or create with different parameters (dates, type, or scope).
Error: “Temporal overlap detected”
Cause: New split’s date range overlaps with existing split of same configuration.
// Existing: 2025-01-01 to 2025-06-01
// New: 2025-04-01 to 2025-09-01
// Overlap: 2025-04-01 to 2025-06-01 ✗
Solution: Adjust dates to avoid overlap:
// Existing: 2025-01-01 to 2025-06-01
// New: 2025-06-01 to 2025-09-01
// Adjacent, no overlap ✓
Error: “At least one condition dimension must be specified”
Cause: Empty conditions array or condition with no dimensions.
{
"conditions": [
{
"mode": "include",
"memo": "Some condition"
// No territories, dsps, usageTypes, or customDimension
}
]
}
{
"conditions": [
{
"mode": "include",
"territories": ["US", "CA"],
"memo": "North America"
}
]
}
Coverage Warnings
Warning: “Gap detected before split start”
Meaning: No coverage before this split’s start date.
Solution: Create default split (no dates) or earlier temporal split.
await fetch('https://api.royalti.io/split', {
method: 'POST',
headers: headers,
body: JSON.stringify({
asset: 'asset-uuid',
product: null,
type: 'Publishing',
name: 'Default Publishing Split',
// No startDate or endDate = permanent
split: [
{ user: 'artist-uuid', share: 100 }
]
})
});
Warning: “Gap detected between splits”
Meaning: Time period with no coverage between two temporal splits.
Solution: Adjust split dates to be adjacent or create intermediate split.
// Gap: 2025-04-01 to 2025-06-01
await fetch('https://api.royalti.io/split', {
method: 'POST',
headers: headers,
body: JSON.stringify({
asset: 'asset-uuid',
product: null,
type: 'Publishing',
name: 'Interim Split',
startDate: '2025-04-01',
endDate: '2025-06-01',
split: [
{ user: 'user-uuid', share: 100 }
]
})
});
Slow Split Matching
Symptom: /split/match endpoint takes > 2 seconds.
Causes:
- Too many splits in tenant
- Complex condition logic
- Large split shares arrays
Solutions:
- Add filters to reduce search space
- Simplify conditions where possible
- Use pagination for bulk operations
- Cache match results when appropriate
Accounting Recalculation Delays
Symptom: Earnings not updated after split change.
Cause: Cache invalidation in progress or background job queued.
Solutions:
- Wait 30-60 seconds for cache to clear
- Check background job queue status
- Verify split changes were saved correctly
- Contact support if delay > 5 minutes
API Reference
For detailed API documentation, see:
Summary
The Royalti.io Splits system provides comprehensive revenue distribution management with:
✓ Flexible Configuration: Asset, product, or combined scopes
✓ Temporal Management: Date ranges with coverage analysis
✓ Conditional Splits: Territory, DSP, and usage type filtering
✓ Automated Creation: Inherit from artist defaults
✓ Validation: 100% share totals and overlap prevention
✓ Integration: Seamless accounting calculation updates
By following the workflows and best practices in this guide, you can create robust revenue distribution configurations that accurately reflect your agreements and automatically calculate correct payouts.
For additional assistance, contact Royalti.io support or refer to the API documentation.