Skip to main content

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.
Node.js
const token = 'your_jwt_token';
const headers = {
  'Authorization': `Bearer ${token}`,
  'Content-Type': 'application/json'
};
Python
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:
LevelAssetIdProductIdCoverage
Asset-levelnullAsset across all product contexts
Product-levelnullEntire product (all tracks)
Product-AssetSpecific 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)
Example
{
  "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.
Valid Split
{
  "split": [
    { "user": "user-uuid-1", "share": 60 },
    { "user": "user-uuid-2", "share": 40 }
  ]
}
// Total: 100% ✓
Invalid Split
{
  "split": [
    { "user": "user-uuid-1", "share": 60 },
    { "user": "user-uuid-2", "share": 30 }
  ]
}
// Total: 90% ✗ - Will be rejected

Quick Start: Creating Your First Split

1

Prepare Asset or Product

Identify the asset or product you want to split revenue for.
Get Assets
curl -X GET 'https://api.royalti.io/asset' \
  -H 'Authorization: Bearer YOUR_TOKEN'
2

Identify Users

Get the UUIDs of users who will receive revenue shares.
Get Users
curl -X GET 'https://api.royalti.io/user' \
  -H 'Authorization: Bearer YOUR_TOKEN'
3

Create the Split

Submit a POST request to create the split configuration.
Node.js
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);
Python
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)
4

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.
Node.js
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.
Node.js
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

Node.js
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

Node.js
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

Node.js
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.
Node.js
{
  conditions: [
    {
      mode: 'exclude',
      territories: ['CN', 'KP'],
      memo: 'Exclude China and North Korea'
    }
  ]
}

Updating Splits

Update existing splits while maintaining validation rules.
Node.js
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

Node.js
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.
Node.js
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:
Node.js
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 TypeRevenue TypeUse 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.
1

Priority Hierarchy

The system applies splits in this order:
  1. Manual splits (provided in request body) - HIGHEST
  2. Artist default splits (fallback from artist settings)
  3. Error (if neither available)
2

Create with Manual Splits

Override artist defaults by providing splits explicitly.
Node.js
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"
3

Create with Artist Defaults

Omit the split array to use artist’s default configuration.
Node.js
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

FeatureManual CreationAutomated from Defaults
FlexibilityFull control over sharesInherits artist configuration
SpeedRequires manual inputInstant application
ConsistencyVaries per creationConsistent across catalog
OverrideN/ACan override with manual splits
Use CaseUnique agreementsStandard label/artist deals

Best Practices for Defaults

  1. Set Up Early: Configure artist defaults before creating catalog items
  2. Type-Specific: Define different splits for publishing vs streaming vs live
  3. Validation: Ensure each type totals exactly 100%
  4. Documentation: Use memo fields to explain split rationale
  5. Review Regularly: Update defaults as contracts change

Split Conditions & Matching

Understanding Condition Logic

Split conditions use a two-phase evaluation system:
  1. Exclusions checked first (any match = reject split)
  2. Inclusions checked next (at least one must match)
  3. No conditions = always match
Evaluation Flow
// 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.
Node.js
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`);
});
Python
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

DimensionMatch Logic
territoriesContext territory must be in array
dspsContext DSP must be in array
usageTypesContext usage type must be in array
customDimensionContext custom field must match values
Multiple dimensionsALL must match (AND operation)

Custom Dimensions

Add custom filtering criteria beyond standard dimensions.
Node.js
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).
Node.js
{
  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.
Node.js
{
  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 Object
{
  "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.
Checking for Overlaps
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).
Example Timeline
// 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 TypeDescriptionStart DateEnd Date
beforeBefore split startsnull (infinite past)Split start date
afterAfter split endsSplit end datenull (infinite future)
betweenBetween two splitsPrevious split endNext split start
infinitePermanent gapsnullnull
Handling Gaps
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.
No Gaps with Default
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:
Node.js - Q1 Split
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

  1. Use Exclusive End Dates: Remember end dates are exclusive, so start next split on end date of previous
  2. Check Coverage Before: Use coverage analysis to verify no gaps
  3. Maintain Type Consistency: Keep the same revenue type across succession
  4. Document Changes: Use memo field to explain why split terms changed
  5. Plan Ahead: Create future splits in advance to avoid coverage gaps

Advanced Features

Retrieving Splits with Filters

The GET endpoint supports extensive filtering:
Node.js - Filtered Query
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:
Node.js
const response = await fetch(
  'https://api.royalti.io/split?q=Nigeria&include=count',
  {
    method: 'GET',
    headers: headers
  }
);

Performance Considerations

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:
Don't Do This ✗
// Inefficient: Multiple individual deletes
for (const splitId of splitIds) {
  await fetch(`https://api.royalti.io/split/${splitId}`, {
    method: 'DELETE',
    headers: headers
  });
}
Do This ✓
// 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:
Node.js
// 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:
Node.js
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:
Node.js
// 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:
Node.js
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:
Node.js - Pre-Recoupment
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:
Node.js
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:
  1. Fetches applicable splits for each revenue entry
  2. Matches conditions (territory, DSP, usage type, date)
  3. Applies split percentages to revenue amount
  4. Creates user earnings records for each split party
  5. Aggregates totals for dashboard display
Calculation Example
// 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.

Performance Impact

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:
Good 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"
Bad Names ✗
"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”

Problem
{
  "split": [
    { "user": "uuid-1", "share": 60 },
    { "user": "uuid-2", "share": 30 }
  ]
}
// Total: 90% ✗
Solution
{
  "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.
Problem
// 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:
Solution
// 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.
Problem
{
  "conditions": [
    {
      "mode": "include",
      "memo": "Some condition"
      // No territories, dsps, usageTypes, or customDimension
    }
  ]
}
Solution
{
  "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.
Add Default 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.
Fix Gap
// 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 }
    ]
  })
});

Performance Issues

Slow Split Matching

Symptom: /split/match endpoint takes > 2 seconds. Causes:
  • Too many splits in tenant
  • Complex condition logic
  • Large split shares arrays
Solutions:
  1. Add filters to reduce search space
  2. Simplify conditions where possible
  3. Use pagination for bulk operations
  4. 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:
  1. Wait 30-60 seconds for cache to clear
  2. Check background job queue status
  3. Verify split changes were saved correctly
  4. 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.