# ChapterTwo Documentation > ChapterTwo is a music royalty management platform. This document contains the complete developer documentation for integrating with ChapterTwo via the API, SDK, CLI, and MCP server. ## General ### Integration Methods | Method | Best For | Install | |-----------|---------------------------------------------|-----------------------------------------| | API + SDK | Building custom integrations | npm install @chapter-two-music/sdk | | CLI | Command-line workflows and scripting | npm install -g @chapter-two-music/cli | | MCP | AI assistant integrations (Claude, Cursor) | No install — connect via URL | ### Authentication Two authentication methods are available: **OAuth 2.0 (recommended)** - Authorization: https://mcp.chaptertwo.com/api/oauth/authorize - Token: https://mcp.chaptertwo.com/api/oauth/token - Discovery: https://mcp.chaptertwo.com/.well-known/oauth-authorization-server The OAuth flow issues an access token (1 hour) and a refresh token (30 days). Refresh tokens rotate on each use. After 30 days, re-authentication is required. **API Keys** For service-to-service integrations. API keys must be requested from ChapterTwo — contact support@chaptertwo.com. ``` curl -H "x-api-key: YOUR_API_KEY" \ -H "x-organization-id: YOUR_ORG_ID" \ https://api.prod.chaptertwo.com/trpc/health ``` ### Organizations All data is scoped to an organization. Include `organizationId` in API inputs or set the `x-organization-id` header. ### Base URLs - API: https://api.prod.chaptertwo.com - MCP Server: https://mcp.chaptertwo.com/api/mcp - OAuth Discovery: https://mcp.chaptertwo.com/.well-known/oauth-authorization-server ### Error Handling Errors use tRPC format: ```json [{ "error": { "message": "Rightsholder not found", "code": -32004, "data": { "code": "NOT_FOUND", "errorCode": "RIGHTSHOLDER_NOT_FOUND" } } }] ``` Common codes: NOT_FOUND, UNAUTHORIZED, FORBIDDEN, BAD_REQUEST, CONFLICT. --- ## API Reference ### Base URL https://api.prod.chaptertwo.com ### Protocol (tRPC HTTP) The API uses tRPC over HTTP with the batch protocol. Endpoints are accessed via /trpc/{procedure}. - Every request requires `?batch=1` — this enables tRPC's batch protocol - Input is wrapped with a `"0"` key (the batch index): `{"0": {...}}` - Queries: GET with URL-encoded input in the `input` query parameter - Mutations: POST with JSON body - Responses are arrays indexed to match: `[{"result": {"data": ...}}]` **Organization scoping**: Most procedures require `organizationId` inside the input body (not just the header). The SDK and CLI inject this automatically. For raw HTTP: ``` GET /trpc/rightsholder.list?batch=1&input={"0":{"organizationId":"org-uuid","pagination":{"page":1,"limit":10}}} POST /trpc/rightsholder.create?batch=1 body: {"0":{"organizationId":"org-uuid","name":"Artist"}} ``` The `x-organization-id` header is accepted for convenience but the server reads `organizationId` from the procedure input. ### Token Lifecycle | Token | Lifetime | Purpose | |----------------|----------|--------------------------------------------------| | Access token | 1 hour | Sent as Bearer token in Authorization header | | Refresh token | 30 days | Used to obtain new access tokens without re-login| Access tokens must be refreshed before expiry. The CLI handles this automatically. For custom integrations: ```bash curl -X POST https://mcp.chaptertwo.com/api/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "grant_type=refresh_token&refresh_token=YOUR_REFRESH_TOKEN" ``` Refresh tokens rotate on each use. After 30 days, re-authenticate via `c2 auth login`. ### SDK Usage ```typescript import { createClient } from '@chapter-two-music/sdk'; const client = createClient({ apiUrl: 'https://api.prod.chaptertwo.com', token: process.env.C2_TOKEN!, // from `c2 auth token` orgId: 'your-org-uuid', }); // List rightsholders const result = await client.query('rightsholder.list', { pagination: { page: 1, limit: 10 }, }); // Create a rightsholder const created = await client.mutate('rightsholder.create', { name: 'New Artist', }); // Auto-detect query vs mutation const detail = await client.call('rightsholder.getById', { id: 'uuid-here', }); ``` ### SDK Methods | Method | Description | |----------------------------------|--------------------------------------| | client.query(procedure, input?) | Call a query procedure (GET) | | client.mutate(procedure, input?) | Call a mutation procedure (POST) | | client.call(procedure, input?) | Auto-detect query vs mutation | | client.setToken(token) | Update the bearer token | | client.setOrgId(orgId) | Switch organization context | ### Endpoint Discovery ```typescript import { ENDPOINTS, listEndpoints, getEndpointMeta, isValidEndpoint } from '@chapter-two-music/sdk'; console.log(ENDPOINTS); // All endpoints console.log(listEndpoints('rightsholder')); // Filter by namespace console.log(isValidEndpoint('rightsholder.list'));// true console.log(getEndpointMeta('rightsholder.list'));// Metadata ``` ### Error Handling (SDK) ```typescript import { ChapterTwoError } from '@chapter-two-music/sdk'; try { await client.query('rightsholder.getById', { id: 'invalid' }); } catch (err) { if (err instanceof ChapterTwoError) { console.error(err.message); console.error(err.statusCode); } } ``` --- ## Endpoints The API exposes procedures organized by namespace. Each is a query (GET) or mutation (POST). The endpoints below are available to all authenticated users via the CLI and SDK. ### rightsholder - rightsholder.list (query) — List rightsholders for the organization - rightsholder.getById (query) — Get a rightsholder by ID - rightsholder.create (mutation) — Create a new rightsholder - rightsholder.updateById (mutation) — Update a rightsholder by ID - rightsholder.batchUpdate (mutation) — Batch update multiple rightsholders - rightsholder.deleteById (mutation) — Delete a rightsholder by ID ### upload - upload.list (query) — List uploads for the organization - upload.uploadFile (mutation) — Generate a presigned URL and initiate a file upload - upload.statements (mutation) — List statements for an organization or upload - upload.uploadAction (mutation) — Execute an action on an upload or statement ### product - product.getById (query) — Get a product by ID - product.getProductsByIds (query) — Get multiple products by their IDs - product.updateById (mutation) — Update a product by ID - product.getAllMusicalDetailsForProduct (query) — Get all musical details for a product in a block - product.getMusicalDetailsBatch (query) — Get musical details for multiple products in a block ### export - export.trigger (mutation) — Trigger a new royalty data export - export.list (query) — List all exports for the current organization - export.status (query) — Get the status of a single export - export.listFiles (query) — List output files for a completed export - export.downloadFile (mutation) — Generate a presigned download URL for a specific export file ### file - file.list (query) — List files for the current organization with optional filters - file.deleteFilesByIds (mutation) — Delete specific files from an upload by their IDs ### fileStorage - fileStorage.downloadStatementFile (mutation) — Generate a presigned URL to download a statement file from S3 - fileStorage.getSample (query) — Get a paginated sample of rows from a statement CSV file ### cube (analytics) - cube.meta (query) — Fetch Cube metadata (views, measures, dimensions) - cube.query (mutation) — Execute a Cube REST API query scoped to the organization - cube.queryAsync (mutation) — Submit a Cube query and return immediately with status - cube.queryPoll (mutation) — Poll for a previously submitted Cube query result - cube.sqlQuery (mutation) — Execute a SQL query against the Cube SQL API - cube.sqlQueryAsync (mutation) — Submit a Cube SQL query and return immediately with status - cube.sqlQueryPoll (mutation) — Poll for a previously submitted Cube SQL query result - cube.getSession (query) — Get or create an org-scoped Cube Cloud embed session #### Cube query input schema cube.query, cube.queryAsync, and cube.queryPoll accept a `query` object: ```json { "query": { "measures": ["EarningsView.totalEarnings"], "dimensions": ["EarningsView.trackTitle"], "filters": [{"member": "EarningsView.source", "operator": "equals", "values": ["Spotify"]}], "timeDimensions": [{"dimension": "EarningsView.statementPeriod", "granularity": "month", "dateRange": "Last 12 months"}], "order": {"EarningsView.totalEarnings": "desc"}, "limit": 100, "total": false } } ``` Filter operators: equals, notEquals, contains, notContains, gt, gte, lt, lte, set, notSet, inDateRange, notInDateRange, beforeDate, afterDate. Granularity: day, week, month, quarter, year. Use cube.meta to discover available measures and dimensions. cube.sqlQuery, cube.sqlQueryAsync, and cube.sqlQueryPoll accept a `sql` string instead of the query object. ### clustering - clustering.getCluster (query) — Get a single cluster with its members and products - clustering.getClustersList (query) — Get a summary list of clusters for a block - clustering.getTitleClustering (query) — Get title clustering data for a block - clustering.getTopClusters (query) — Get top clusters for a block - clustering.getTopMdsFromBlock (query) — Get top musical details with earnings from a block - clustering.updateClustering (mutation) — Apply clustering changes ### chat - chat.list (query) — List chats for the current user in the organization ### exchangeRates - exchangeRates.get (query) — Get exchange rates for a given date ### fetching - fetching.inQueueCount (query) — Get the number of items in the processing queue ### user - user.currentUser (query) — Get the currently authenticated user Use the CLI for the complete endpoint list: `c2 endpoints` --- ## MCP Server ### Server URL https://mcp.chaptertwo.com/api/mcp Authentication is handled automatically via OAuth 2.0. ### Claude Desktop Configuration Add to claude_desktop_config.json: ```json { "mcpServers": { "chaptertwo": { "url": "https://mcp.chaptertwo.com/api/mcp" } } } ``` Config file locations: - macOS: ~/Library/Application Support/Claude/claude_desktop_config.json - Windows: %APPDATA%\Claude\claude_desktop_config.json ### Cursor Configuration Add to .cursor/mcp.json in your project root: ```json { "mcpServers": { "chaptertwo": { "url": "https://mcp.chaptertwo.com/api/mcp" } } } ``` ### Generic MCP Clients - Server URL: https://mcp.chaptertwo.com/api/mcp - Transport: Streamable HTTP (POST) - Auth: OAuth 2.0 with PKCE (auto-negotiated) OAuth discovery: - https://mcp.chaptertwo.com/.well-known/oauth-authorization-server - https://mcp.chaptertwo.com/.well-known/oauth-protected-resource ### Authentication Flow 1. Client connects to the server URL 2. Server responds with 401 + WWW-Authenticate header 3. Client fetches OAuth metadata and initiates PKCE flow 4. User logs in via ChapterTwo (Clerk) in browser 5. Client receives access token for subsequent requests Tokens auto-refresh. Set x-organization-id header for multi-org users. ### Available Tools | Tool | Description | |-----------------------|------------------------------------------------------------------| | c2_health | Check API health status | | c2_whoami | Returns current user and organization info | | c2_get_permissions | Get current user permissions and capabilities | | c2_api_call | Call any tRPC API procedure (auto-detects query vs mutation) | | c2_list_endpoints | List available API endpoints, optionally filtered by namespace | | c2_search_endpoints | Search API endpoints by keyword | | c2_describe_endpoint | Returns procedure type, namespace, and route info | | c2_query_analytics | Query royalty analytics via the Cube semantic layer | | c2_cube_meta | Get available Cube measures, dimensions, and descriptions | ### Troubleshooting - "Missing or invalid Authorization header" — Ensure OAuth is configured and client supports PKCE - "No organization found for this user" — Contact admin to be added to an organization - "Multiple organizations found" — Set x-organization-id header to select one - "Access token expired" — Reconnect to trigger new OAuth flow - Connection timeout — 55s limit per request; narrow filters for complex queries --- ## CLI ### Installation ```bash npm install -g @chapter-two-music/cli ``` ### Quick Start ```bash c2 auth login # Authenticate (opens browser) c2 rightsholder.list # List your rightsholders c2 api rightsholder.getById -i '{"id": "uuid"}' # Get details c2 health # Check API health ``` ### Commands | Command | Description | |-----------------------------|-------------------------------------------| | c2 auth login | Authenticate via browser (OAuth2 + PKCE) | | c2 auth logout | Clear saved credentials | | c2 auth status | Show current authentication status | | c2 auth token | Print current access token (for scripts) | | c2 api | Call any API procedure | | c2 endpoints | List available API endpoints | | c2 describe | Show detailed endpoint info | | c2 whoami | Show current user and organization | | c2 health | Check API health status | | c2 config show | Show CLI configuration | | c2 config set | Set a configuration value | ### Shorthand CLI-scoped endpoints can be called directly: ```bash c2 rightsholder.list # same as: c2 api rightsholder.list ``` ### Authentication - Browser OAuth (default): `c2 auth login` - Device flow (headless): `c2 auth login --device` - Tokens: access (1hr, auto-refreshed), refresh (30 days) - Credentials stored in ~/.c2/ - Get current token for scripts: `c2 auth token` ### Output Formats ```bash c2 api rightsholder.list --format json # raw JSON c2 api rightsholder.list --format pretty # formatted (default) c2 api rightsholder.list --format table # table view ```