Skip to main content
This guide provides comprehensive documentation for integrating the Releases API into your application, including authentication, data models, endpoints, and workflow management.

Overview

The Releases API enables developers to create and manage music releases through a complete workflow system. The API supports release creation, track management, media uploads, review workflows, feedback systems, and automatic catalog integration.

Key Features

Release Management

Create, update, and manage music releases with multiple tracks and comprehensive metadata

Media Management

Upload files or submit external links for tracks and release artwork

Workflow System

Complete approval workflow from draft to automatic catalog integration

Feedback System

User and admin feedback with internal notes and review communication

Authentication

All API endpoints require Bearer token authentication. Include your JWT token in the Authorization header.
Authorization: Bearer <your-jwt-token>

Data Models

Release Model

interface Release {
  // Core Fields
  id: string;
  TenantId: number;
  TenantUserId: string;
  title: string;
  format: 'Single' | 'EP' | 'Album' | 'Mixtape';
  type: 'Audio' | 'Video' | 'Audiovisual';
  status: 'draft' | 'submitted' | 'under_review' | 'approved' | 'rejected' | 'completed';
  
  // Dates
  submittedAt?: Date;
  reviewedById?: string;
  reviewedAt?: Date;
  completedAt?: Date;
  releaseDate?: Date;
  preReleaseDate?: Date;
  
  // Metadata
  version?: string;
  label?: string;
  copyright?: string;
  displayArtist: string;
  artists: Record<string, 'primary' | 'featuring'>;
  mainGenre?: string[];
  subGenre?: string[];
  contributors?: Record<string, string[]>;
  description?: string;
  metadata?: Record<string, any>;
  media?: IMedia[];
  explicit: 'explicit' | 'clean' | null;
  
  // Auto-creation Status
  autoCreationStatus?: 'pending' | 'success' | 'failed' | null;
  autoCreationError?: string;
  createdProductId?: string;
  createdAssetIds?: string[];
  
  // Relations
  owner?: User;
  reviewer?: User;
  tracks?: ReleaseAsset[];
  feedbacks?: ReleaseFeedback[];
}

Release Asset (Track) Model

interface ReleaseAsset {
  id: string;
  TenantId: number;
  ReleaseId: string;
  trackNumber: number;
  title: string;
  version?: string;
  isrc?: string;
  iswc?: string;
  duration?: number; // in seconds
  lyrics?: string;
  language: string; // default: 'en'
  displayArtist: string;
  artists: Record<string, 'primary' | 'featuring'>;
  mainGenre?: string[];
  subGenre?: string[];
  contributors?: Record<string, string[]>;
  media?: IMedia[];
  metadata?: Record<string, any>;
  explicit: 'explicit' | 'clean' | null;
  assetId?: string; // Link to created Asset after approval
}

Media Model

interface IMedia {
  cloudId: string;           // Unique identifier for the media
  cloudUrl: string;          // Public URL for accessing the media
  type: 'audio' | 'video' | 'image' | 'document';
  name: string;              // File name or link title
  isLink: boolean;           // true for external links, false for uploaded files
  releasePath?: string;      // Internal storage path (for files only)
  metadata?: {
    duration?: number;       // Duration in seconds (audio/video)
    bitrate?: string;        // Audio bitrate
    sampleRate?: string;     // Audio sample rate
    channels?: string;       // Audio channels
    codec?: string;          // Codec information
    fileSize?: number;       // File size in bytes
    mimeType?: string;       // MIME type
    processedAt?: Date;      // Processing timestamp
    linkValidated?: boolean; // For links - validation status
    [key: string]: any;
  };
}

Release Feedback Model

interface ReleaseFeedback {
  id: string;
  TenantId: number;
  ReleaseId: string;
  TenantUserId: string;      // User who created the feedback
  comment: string;
  status: string;            // Status when feedback was created
  createdAt: Date;
  updatedAt: Date;
}

Release Workflow

1

Draft Creation

User creates a release in draft status with basic information and tracks. Multiple drafts can be saved and edited.
2

Media Upload

User uploads audio files, artwork, and other media or submits external links (WeTransfer, Google Drive, etc.)
3

Submission

User submits the completed release for admin review. Release must have complete metadata and at least one track.
4

Admin Review

Admin reviews the submission and can approve, reject, or request changes with detailed feedback.
5

Auto-Creation

Upon approval, the system automatically creates catalog products and assets with proper metadata and file associations.
6

Completion

Release is marked as completed and all content is available in the main catalog system.

API Endpoints

Create Release

curl -X POST https://api.royalti.io/releases \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "My Awesome Single",
    "displayArtist": "Artist Name",
    "artists": {
      "550e8400-e29b-41d4-a716-446655440000": "primary"
    },
    "format": "Single",
    "type": "Audio",
    "explicit": null,
    "releaseDate": "2024-12-01",
    "tracks": [
      {
        "title": "Track Title",
        "displayArtist": "Artist Name",
        "artists": {
          "550e8400-e29b-41d4-a716-446655440000": "primary"
        },
        "duration": 180,
        "language": "en"
      }
    ]
  }'
{
  title: string;                    // Required
  displayArtist: string;           // Required
  artists: Record<string, 'primary' | 'featuring'>; // Required, must have at least one primary
  format?: 'Single' | 'EP' | 'Album' | 'Mixtape'; // Default: 'Single'
  type?: 'Audio' | 'Video' | 'Audiovisual'; // Default: 'Audio'
  version?: string;
  label?: string;
  copyright?: string;
  mainGenre?: string[];
  subGenre?: string[];
  contributors?: Record<string, string[]>;
  description?: string;
  metadata?: Record<string, any>;
  explicit?: 'explicit' | 'clean' | null; // Default: null
  releaseDate?: string;            // Format: YYYY-MM-DD
  preReleaseDate?: string;         // Format: YYYY-MM-DD
  ownerId?: string;                // Admin only: create for another user
  tracks: TrackInput[];            // Required, at least 1 track
}

Upload Media Files

curl -X POST https://api.royalti.io/releases/{id}/media/files \
  -H "Authorization: Bearer <token>" \
  -F "files=@artwork.jpg" \
  -F "files=@liner_notes.pdf"
curl -X POST https://api.royalti.io/releases/{id}/media/links \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "links": [
      {
        "url": "https://wetransfer.com/downloads/abc123",
        "name": "Album Artwork",
        "type": "image"
      },
      {
        "url": "https://drive.google.com/file/d/xyz789",
        "name": "Press Kit",
        "type": "document"
      }
    ]
  }'

Upload Track Media

curl -X POST https://api.royalti.io/releases/{id}/tracks/{trackId}/media/file \
  -H "Authorization: Bearer <token>" \
  -F "file=@track.wav"

Get Releases with Filtering

curl -X GET "https://api.royalti.io/releases?page=1&limit=20&status=draft&search=awesome&sortBy=updatedAt&sortOrder=desc" \
  -H "Authorization: Bearer <token>"

Submit Release for Review

Only releases in ‘draft’ or ‘rejected’ status can be submitted for review. Release must have at least one track with complete metadata.
curl -X POST https://api.royalti.io/releases/{id}/submit \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{}'

Review Release (Admin Only)

This endpoint requires admin privileges. Only releases in ‘submitted’ status can be reviewed.
curl -X POST https://api.royalti.io/releases/{id}/review \
  -H "Authorization: Bearer <admin-token>" \
  -H "Content-Type: application/json" \
  -d '{
    "action": "approve",
    "feedback": "Excellent release! All requirements met. Processing for catalog creation."
  }'

Add Feedback

curl -X POST https://api.royalti.io/releases/{id}/feedback \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "message": "Great track! Consider adjusting the mix on the bridge section.",
    "isInternal": false
  }'
curl -X POST https://api.royalti.io/releases/{id}/tracks/link-asset \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "assetId": "550e8400-e29b-41d4-a716-446655440000",
    "trackNumber": 2,
    "overrides": {
      "title": "My Song (Radio Edit)",
      "explicit": "clean"
    }
  }'

Reorder Tracks

curl -X POST https://api.royalti.io/releases/{id}/tracks/reorder \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "trackOrder": [
      "track2-uuid",
      "track1-uuid",
      "track3-uuid"
    ]
  }'

Error Handling

Common Error Codes

{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed",
    "details": [
      "Title is required",
      "At least one primary artist is required",
      "Tracks array cannot be empty"
    ]
  }
}
{
  "success": false,
  "error": {
    "code": "UNAUTHORIZED", 
    "message": "Invalid or missing authentication token"
  }
}
{
  "success": false,
  "error": {
    "code": "FORBIDDEN",
    "message": "You do not have permission to perform this action"
  }
}
{
  "success": false,
  "error": {
    "code": "NOT_FOUND",
    "message": "Release not found"
  }
}

Status Workflow & Permissions

Understanding the release status workflow and permission system is crucial for proper integration.

Status Transitions

Current StatusAvailable ActionsNext StatusWho Can Perform
draftEdit, Delete, SubmitsubmittedOwner, Admin
submittedReview, Add Feedbackunder_review, approved, rejectedAdmin
under_reviewReview, Add Feedbackapproved, rejectedAdmin
approvedRevert Statussubmitted, completedAdmin
rejectedEdit, SubmitsubmittedOwner, Admin
completedView Only-All

Permission Matrix

Regular Users

  • Create own releases
  • Edit own draft/rejected releases
  • Submit releases for review
  • Add public feedback
  • Delete own draft releases
  • View own releases

Admin Users

  • All regular user permissions
  • View all releases
  • Review any submission
  • Approve/reject releases
  • Add internal feedback
  • Delete any release
  • Revert release status
  • Create releases for other users

Auto-Creation Process

When a release is approved, the system automatically:
  1. Creates Products: Main release product in catalog
  2. Creates Assets: Individual track assets with metadata
  3. Transfers Media: Moves files from temporary to permanent storage
  4. Links Relationships: Establishes proper product-asset associations
  5. Updates Status: Marks release as completed
  6. Sends Notifications: Notifies relevant users

Monitoring Auto-Creation

curl -X GET https://api.royalti.io/releases/{id} \
  -H "Authorization: Bearer <token>"

Handling Auto-Creation Failures

If auto-creation fails (autoCreationStatus: 'failed'), admins can:
  1. Review Error Details: Check autoCreationError field
  2. Revert Status: Use revert endpoint to send back for corrections
  3. Manual Processing: Create catalog items manually if needed
curl -X POST https://api.royalti.io/releases/{id}/revert-status \
  -H "Authorization: Bearer <admin-token>" \
  -H "Content-Type: application/json" \
  -d '{
    "targetStatus": "submitted",
    "reason": "Auto-creation failed due to missing artist metadata. Reverting for re-review."
  }'

Best Practices

  • Always validate required fields before submission
  • Ensure at least one primary artist is specified
  • Include comprehensive track information and metadata
  • Use proper ISRC/ISWC codes when available
  • Set appropriate explicit content flags
  • Upload high-quality media files or valid external links
  • Test audio files before submission
  • Use lossless audio formats (WAV/FLAC) when possible
  • Minimum 320kbps MP3 for compressed audio
  • Upload artwork at minimum 3000x3000px resolution
  • Use descriptive file names
  • Validate external links before submission
  • Consider file size limits for uploads
  • Implement retry logic for network failures
  • Handle validation errors gracefully with specific user feedback
  • Show meaningful error messages based on API response details
  • Log errors with sufficient context for debugging
  • Handle auto-creation failures with appropriate user messaging
  • Use pagination for release lists with appropriate page sizes
  • Implement caching for frequently accessed release data
  • Debounce search inputs to reduce API calls
  • Lazy load track details and media when needed
  • Use background processing for large file uploads
  • Always validate JWT tokens and handle expired tokens
  • Never expose sensitive information in client-side code
  • Implement proper permission checks before API calls
  • Sanitize all user inputs before sending to API
  • Use HTTPS for all API communications
  • Validate file types and sizes before upload

Complete Integration Example

import { useState, useEffect } from 'react';

// Enhanced API Service
class ReleasesAPI {
  constructor(baseURL, token) {
    this.baseURL = baseURL;
    this.token = token;
  }

  async request(endpoint, options = {}) {
    const url = `${this.baseURL}${endpoint}`;
    const config = {
      headers: {
        'Authorization': `Bearer ${this.token}`,
        ...options.headers,
      },
      ...options,
    };

    const response = await fetch(url, config);
    
    if (!response.ok) {
      const error = await response.json();
      throw new Error(error.message || `HTTP error! status: ${response.status}`);
    }

    return response.json();
  }

  async createRelease(releaseData) {
    return this.request('/releases', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(releaseData)
    });
  }

  async getReleases(filters = {}) {
    const params = new URLSearchParams(filters);
    return this.request(`/releases?${params}`);
  }

  async getRelease(releaseId) {
    return this.request(`/releases/${releaseId}`);
  }

  async updateRelease(releaseId, updateData) {
    return this.request(`/releases/${releaseId}`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(updateData)
    });
  }

  async submitRelease(releaseId) {
    return this.request(`/releases/${releaseId}/submit`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({})
    });
  }

  async reviewRelease(releaseId, action, feedback) {
    return this.request(`/releases/${releaseId}/review`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ action, feedback })
    });
  }

  async uploadMedia(releaseId, files) {
    const formData = new FormData();
    files.forEach(file => formData.append('files', file));

    return this.request(`/releases/${releaseId}/media/files`, {
      method: 'POST',
      body: formData
    });
  }

  async submitLinks(releaseId, links) {
    return this.request(`/releases/${releaseId}/media/links`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ links })
    });
  }

  async uploadTrackMedia(releaseId, trackId, file) {
    const formData = new FormData();
    formData.append('file', file);

    return this.request(`/releases/${releaseId}/tracks/${trackId}/media/file`, {
      method: 'POST',
      body: formData
    });
  }

  async addFeedback(releaseId, message, isInternal = false) {
    return this.request(`/releases/${releaseId}/feedback`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ message, isInternal })
    });
  }
}

// Enhanced React Component
function ReleaseDashboard() {
  const [releases, setReleases] = useState([]);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [filters, setFilters] = useState({
    page: 1,
    limit: 20,
    sortBy: 'updatedAt',
    sortOrder: 'desc'
  });

  const api = new ReleasesAPI('https://api.royalti.io', 'your-jwt-token');

  useEffect(() => {
    loadReleases();
  }, [filters]);

  const loadReleases = async () => {
    try {
      setLoading(true);
      const data = await api.getReleases(filters);
      setReleases(data.data.releases);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };

  const handleSubmitRelease = async (releaseId) => {
    try {
      await api.submitRelease(releaseId);
      loadReleases(); // Refresh list
    } catch (err) {
      setError(err.message);
    }
  };

  const handleStatusFilter = (status) => {
    setFilters(prev => ({ ...prev, status, page: 1 }));
  };

  const handleSearch = (search) => {
    setFilters(prev => ({ ...prev, search, page: 1 }));
  };

  if (loading) return <div>Loading releases...</div>;
  if (error) return <div>Error: {error}</div>;

  return (
    <div className="release-dashboard">
      <div className="header">
        <h1>My Releases</h1>
        <button onClick={() => window.location.href = '/releases/create'}>
          Create New Release
        </button>
      </div>

      <div className="filters">
        <input
          type="text"
          placeholder="Search releases..."
          onChange={(e) => handleSearch(e.target.value)}
        />
        <select onChange={(e) => handleStatusFilter(e.target.value)}>
          <option value="">All Statuses</option>
          <option value="draft">Draft</option>
          <option value="submitted">Submitted</option>
          <option value="approved">Approved</option>
          <option value="rejected">Rejected</option>
          <option value="completed">Completed</option>
        </select>
      </div>

      <div className="releases-grid">
        {releases.map(release => (
          <div key={release.id} className="release-card">
            <h3>{release.title}</h3>
            <p>Artist: {release.displayArtist}</p>
            <p>Format: {release.format}</p>
            <span className={`status status-${release.status}`}>
              {release.status}
            </span>
            
            <div className="actions">
              <button onClick={() => window.location.href = `/releases/${release.id}`}>
                View Details
              </button>
              
              {release.status === 'draft' && (
                <>
                  <button onClick={() => window.location.href = `/releases/edit/${release.id}`}>
                    Edit
                  </button>
                  <button onClick={() => handleSubmitRelease(release.id)}>
                    Submit for Review
                  </button>
                </>
              )}
              
              {release.status === 'rejected' && (
                <button onClick={() => window.location.href = `/releases/edit/${release.id}`}>
                  Fix & Resubmit
                </button>
              )}
            </div>

            {release.autoCreationStatus === 'failed' && (
              <div className="error-notice">
                Auto-creation failed. Please contact support.
              </div>
            )}
          </div>
        ))}
      </div>

      {releases.length === 0 && (
        <div className="empty-state">
          <p>No releases found. Create your first release to get started!</p>
        </div>
      )}
    </div>
  );
}

export default ReleaseDashboard;

Webhooks & Notifications

The API includes a comprehensive notification system that sends real-time updates for release events.

Webhook Events

EventDescriptionPayload
release.createdRelease createdRelease object
release.submittedRelease submitted for reviewRelease object
release.approvedRelease approved by adminRelease object + reviewer info
release.rejectedRelease rejected by adminRelease object + feedback
release.completedAuto-creation completed successfullyRelease object + created assets
release.auto_creation_failedAuto-creation process failedRelease object + error details
release.feedback_addedNew feedback addedFeedback object + release info

Setting Up Webhooks

const express = require('express');
const app = express();

app.use(express.json());

app.post('/webhooks/releases', (req, res) => {
  const { event, data, timestamp } = req.body;
  
  console.log(`Received webhook: ${event} at ${timestamp}`);
  
  switch (event) {
    case 'release.approved':
      handleReleaseApproved(data);
      break;
      
    case 'release.rejected':
      handleReleaseRejected(data);
      break;
      
    case 'release.completed':
      handleReleaseCompleted(data);
      break;
      
    case 'release.auto_creation_failed':
      handleAutoCreationFailed(data);
      break;
      
    case 'release.feedback_added':
      handleFeedbackAdded(data);
      break;
      
    default:
      console.log(`Unhandled event: ${event}`);
  }
  
  res.status(200).send('OK');
});

function handleReleaseApproved(data) {
  const { release, reviewer } = data;
  
  // Send notification to release owner
  sendNotification(release.TenantUserId, {
    title: 'Release Approved! 🎉',
    message: `Your release "${release.title}" has been approved and is being processed.`,
    type: 'success',
    releaseId: release.id
  });
}

function handleReleaseCompleted(data) {
  const { release, createdAssets } = data;
  
  // Send success notification
  sendNotification(release.TenantUserId, {
    title: 'Release Live! 🚀',
    message: `"${release.title}" is now live in your catalog with ${createdAssets.length} tracks.`,
    type: 'success',
    releaseId: release.id,
    actionUrl: `/catalog/products/${release.createdProductId}`
  });
}

app.listen(3000, () => {
  console.log('Webhook server listening on port 3000');
});

Testing & Validation

Test Environment Setup

// Jest test example for Releases API integration
const ReleasesAPI = require('./releases-api');

describe('Releases API Integration', () => {
  let api;
  let testReleaseId;

  beforeAll(() => {
    api = new ReleasesAPI('https://api-staging.royalti.io', process.env.TEST_TOKEN);
  });

  describe('Release Creation', () => {
    test('should create a valid single release', async () => {
      const releaseData = {
        title: 'Test Single',
        displayArtist: 'Test Artist',
        artists: {
          '550e8400-e29b-41d4-a716-446655440000': 'primary'
        },
        format: 'Single',
        type: 'Audio',
        tracks: [
          {
            title: 'Test Track',
            displayArtist: 'Test Artist',
            artists: {
              '550e8400-e29b-41d4-a716-446655440000': 'primary'
            },
            duration: 180,
            language: 'en'
          }
        ]
      };

      const response = await api.createRelease(releaseData);
      
      expect(response.success).toBe(true);
      expect(response.data.title).toBe(releaseData.title);
      expect(response.data.status).toBe('draft');
      expect(response.data.tracks).toHaveLength(1);
      
      testReleaseId = response.data.id;
    });

    test('should reject release with missing required fields', async () => {
      const invalidData = {
        title: 'Test Single'
        // Missing required fields
      };

      await expect(api.createRelease(invalidData)).rejects.toThrow();
    });
  });

  describe('Release Workflow', () => {
    test('should submit draft release for review', async () => {
      const response = await api.submitRelease(testReleaseId);
      
      expect(response.success).toBe(true);
      expect(response.data.status).toBe('submitted');
    });
  });

  describe('Media Management', () => {
    test('should upload media files successfully', async () => {
      const testFile = new File(['test audio data'], 'test.mp3', { type: 'audio/mpeg' });
      
      const response = await api.uploadTrackMedia(testReleaseId, trackId, testFile);
      
      expect(response.success).toBe(true);
      expect(response.data.type).toBe('audio');
      expect(response.data.name).toBe('test.mp3');
    });

    test('should submit external links successfully', async () => {
      const links = [
        {
          url: 'https://wetransfer.com/downloads/test123',
          name: 'Test Audio File',
          type: 'audio'
        }
      ];

      const response = await api.submitLinks(testReleaseId, links);
      
      expect(response.success).toBe(true);
      expect(response.data).toHaveLength(1);
      expect(response.data[0].isLink).toBe(true);
    });
  });

  afterAll(async () => {
    // Cleanup test data
    if (testReleaseId) {
      try {
        await api.deleteRelease(testReleaseId);
      } catch (error) {
        console.log('Cleanup error:', error.message);
      }
    }
  });
});

Conclusion

The Releases API provides a comprehensive solution for managing music submissions and catalog creation. By following this guide, you can integrate a complete release workflow into your application, from initial submission through approval and automatic catalog creation.

Key Takeaways

  1. Workflow-Centric Design: The API is built around a clear submission-to-publication workflow
  2. Comprehensive Media Support: Handle both file uploads and external links seamlessly
  3. Robust Error Handling: Implement proper error handling for all API interactions
  4. Performance Considerations: Use pagination, filtering, and caching appropriately
  5. Security Best Practices: Always validate permissions and sanitize inputs

Next Steps

  • Implement webhook handling for real-time updates
  • Set up monitoring and analytics for release workflows
  • Customize the UI/UX to match your application’s design
  • Consider implementing additional features like bulk operations or advanced analytics
For additional support or questions about the Releases API, please refer to our support documentation or contact the development team.
I