Skip to main content

Overview

Royalti.io provides real-time WebSocket events so your application can receive instant updates about file processing status. Instead of polling the job status endpoint, you can subscribe to events and get immediate notifications when processing starts, progresses, completes, or fails.

Why Use WebSocket Events?

  • Instant Updates: Receive status changes immediately without polling
  • Progress Tracking: Get granular progress updates (every 10%) during processing
  • User-Scoped: Events are sent only to the user who uploaded the file
  • Efficient: Reduces API calls and improves user experience
  • Reliable: Built on Socket.io with automatic reconnection

Prerequisites

Authentication

You need a valid JWT access token to connect to the WebSocket server. Use the same token you use for REST API calls.

Socket.io Client

Install the Socket.io client for your platform:
npm install socket.io-client

Quick Start

1

Connect to WebSocket

Establish a WebSocket connection with your JWT token:
Node.js
import { io } from 'socket.io-client';

const socket = io('https://api.royalti.io', {
  auth: { token: 'your-jwt-access-token' },
  path: '/socket.io'
});

socket.on('connect', () => {
  console.log('Connected to Royalti.io WebSocket');
});

socket.on('connect_error', (error) => {
  console.error('Connection failed:', error.message);
});
2

Subscribe to File Processing Events

Listen for file processing status updates:
Node.js
// Processing started
socket.on('file:processing:started', (data) => {
  console.log(`Processing started: ${data.fileName}`);
  // Update UI to show processing state
});

// Progress updates (every ~10%)
socket.on('file:processing:progress', (data) => {
  console.log(`Progress: ${data.progress}% - ${data.message}`);
  // Update progress bar
});

// Processing completed
socket.on('file:processing:completed', (data) => {
  console.log(`Completed: ${data.fileName} in ${data.metadata.processingTimeMs}ms`);
  // Show success notification
});

// Processing failed
socket.on('file:processing:failed', (data) => {
  console.error(`Failed: ${data.error}`);
  // Show error notification
});
3

Upload a File

Upload a royalty file - events will automatically be sent to your WebSocket connection:
Node.js
// Request upload URL
const response = await fetch('https://api.royalti.io/file/get-upload-url', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    autoDetect: true,
    fileMetaData: {
      originalname: 'spotify_royalties_2024_01.csv',
      mimetype: 'text/csv'
    }
  })
});

// Upload will trigger WebSocket events automatically

Event Types

Royalti.io emits four types of file processing events:
EventWhen EmittedContains
file:processing:startedProcessing beginsFile ID, name, initial status
file:processing:progressEvery ~10% progressProgress percentage, status message
file:processing:completedProcessing succeedsFinal stats, processing time
file:processing:failedProcessing failsError message, failure details

Event Payload Structure

All events share a common payload structure:
interface FileProcessingEvent {
  fileId: string;          // UUID of the file
  fileName: string;        // Original filename
  status: 'started' | 'progress' | 'completed' | 'failed';
  progress?: number;       // 0-100 percentage
  message?: string;        // Human-readable status
  error?: string;          // Error message (failed only)
  metadata?: {
    rowsProcessed?: number;
    source?: string;
    accountingPeriod?: string;
    salePeriod?: string;
    processingTimeMs?: number;
  };
}

Progress Stages

Progress updates are emitted at key processing stages. The exact stages depend on the file type:

CSV/Text Files

ProgressStage
0%Processing started
20%Configuration loaded
30-40%Table ready, validation complete
50%BigQuery upload starting
80%BigQuery upload complete
100%Processing complete

ZIP Files

ProgressStage
0%Processing started
10%Downloading ZIP
30%ZIP downloaded
50%Extracting files
80%Files queued for processing
100%Complete

Excel Files

ProgressStage
0%Processing started
20%Creating sheet processor
50%Sheets discovered
100%Complete

Code Examples

Complete Node.js Example

Node.js
import { io } from 'socket.io-client';

class RoyaltiWebSocket {
  constructor(accessToken) {
    this.socket = io('https://api.royalti.io', {
      auth: { token: accessToken },
      path: '/socket.io',
      reconnection: true,
      reconnectionAttempts: 5,
      reconnectionDelay: 1000
    });

    this.setupEventHandlers();
  }

  setupEventHandlers() {
    this.socket.on('connect', () => {
      console.log('Connected to Royalti.io');
    });

    this.socket.on('disconnect', (reason) => {
      console.log('Disconnected:', reason);
    });

    this.socket.on('file:processing:started', (data) => {
      this.onProcessingStarted(data);
    });

    this.socket.on('file:processing:progress', (data) => {
      this.onProcessingProgress(data);
    });

    this.socket.on('file:processing:completed', (data) => {
      this.onProcessingCompleted(data);
    });

    this.socket.on('file:processing:failed', (data) => {
      this.onProcessingFailed(data);
    });
  }

  onProcessingStarted(data) {
    console.log(`[${data.fileId}] Started: ${data.fileName}`);
  }

  onProcessingProgress(data) {
    console.log(`[${data.fileId}] ${data.progress}% - ${data.message}`);
  }

  onProcessingCompleted(data) {
    console.log(`[${data.fileId}] Completed in ${data.metadata?.processingTimeMs}ms`);
    console.log(`  Rows processed: ${data.metadata?.rowsProcessed}`);
  }

  onProcessingFailed(data) {
    console.error(`[${data.fileId}] Failed: ${data.error}`);
  }

  disconnect() {
    this.socket.disconnect();
  }
}

// Usage
const ws = new RoyaltiWebSocket('your-jwt-token');

Python Example

Python
import socketio

sio = socketio.Client()

@sio.event
def connect():
    print('Connected to Royalti.io')

@sio.event
def disconnect():
    print('Disconnected')

@sio.on('file:processing:started')
def on_started(data):
    print(f"Started: {data['fileName']}")

@sio.on('file:processing:progress')
def on_progress(data):
    print(f"Progress: {data['progress']}% - {data.get('message', '')}")

@sio.on('file:processing:completed')
def on_completed(data):
    print(f"Completed: {data['fileName']}")
    if 'metadata' in data:
        print(f"  Processing time: {data['metadata'].get('processingTimeMs')}ms")

@sio.on('file:processing:failed')
def on_failed(data):
    print(f"Failed: {data.get('error', 'Unknown error')}")

# Connect with authentication
sio.connect(
    'https://api.royalti.io',
    auth={'token': 'your-jwt-token'},
    socketio_path='/socket.io'
)

# Keep connection alive
sio.wait()

React Hook Example

React
import { useEffect, useState, useCallback } from 'react';
import { io } from 'socket.io-client';

export function useFileProcessing(accessToken) {
  const [files, setFiles] = useState({});
  const [connected, setConnected] = useState(false);

  useEffect(() => {
    if (!accessToken) return;

    const socket = io('https://api.royalti.io', {
      auth: { token: accessToken },
      path: '/socket.io'
    });

    socket.on('connect', () => setConnected(true));
    socket.on('disconnect', () => setConnected(false));

    socket.on('file:processing:started', (data) => {
      setFiles(prev => ({
        ...prev,
        [data.fileId]: { ...data, status: 'processing' }
      }));
    });

    socket.on('file:processing:progress', (data) => {
      setFiles(prev => ({
        ...prev,
        [data.fileId]: { ...prev[data.fileId], ...data }
      }));
    });

    socket.on('file:processing:completed', (data) => {
      setFiles(prev => ({
        ...prev,
        [data.fileId]: { ...data, status: 'completed' }
      }));
    });

    socket.on('file:processing:failed', (data) => {
      setFiles(prev => ({
        ...prev,
        [data.fileId]: { ...data, status: 'failed' }
      }));
    });

    return () => socket.disconnect();
  }, [accessToken]);

  return { files, connected };
}

// Usage in component
function FileUploader() {
  const { files, connected } = useFileProcessing(accessToken);

  return (
    <div>
      <div>Status: {connected ? 'Connected' : 'Disconnected'}</div>
      {Object.values(files).map(file => (
        <div key={file.fileId}>
          {file.fileName}: {file.progress}% - {file.message}
        </div>
      ))}
    </div>
  );
}

Best Practices

Error Handling

Always handle connection errors and implement reconnection logic:
Node.js
const socket = io('https://api.royalti.io', {
  auth: { token: accessToken },
  path: '/socket.io',
  reconnection: true,
  reconnectionAttempts: 5,
  reconnectionDelay: 1000,
  reconnectionDelayMax: 5000
});

socket.on('connect_error', (error) => {
  if (error.message === 'Authentication failed') {
    // Token may be expired - refresh it
    refreshToken().then(newToken => {
      socket.auth.token = newToken;
      socket.connect();
    });
  }
});

Token Refresh

Update your token when it expires without losing the connection:
Node.js
socket.on('disconnect', (reason) => {
  if (reason === 'io server disconnect') {
    // Server disconnected us - likely auth issue
    refreshToken().then(newToken => {
      socket.auth.token = newToken;
      socket.connect();
    });
  }
  // Otherwise Socket.io will automatically try to reconnect
});

Cleanup

Always disconnect when your component unmounts or user logs out:
Node.js
// On logout or component unmount
socket.disconnect();

Combining with Notifications

WebSocket events provide real-time progress, while the notification system provides persistent records. Use both for a complete solution:
Node.js
// Real-time progress via WebSocket
socket.on('file:processing:progress', (data) => {
  updateProgressBar(data.fileId, data.progress);
});

// Persistent notification on completion
socket.on('notification', (notification) => {
  if (notification.type === 'ROYALTY_FILE_PROCESSED') {
    addToNotificationCenter(notification);
  }
});

Troubleshooting

Cause: Your JWT token is invalid or expired.Solution: Ensure you’re using a valid access token. If using refresh tokens, exchange for a new access token before connecting.
Cause: WebSocket connection may have dropped or file was uploaded by a different user.Solution:
  1. Verify your WebSocket is connected (socket.connected)
  2. Events are user-scoped - only the uploader receives them
  3. Check that you’re listening to the correct event names
Cause: Multiple WebSocket connections or event handlers registered.Solution: Ensure you only create one socket connection per session and clean up event handlers when disconnecting.
Cause: File processing completed very quickly (small file) or progress events are being batched.Solution: This is normal for small files. For larger files, you should see intermediate progress updates.

API Reference

For REST API endpoints used with file uploads, see:

Support

If you encounter issues with WebSocket events: