Get your first API response in under 10 minutes. The Undercore API gives you programmatic access to music publishing data — works, writers, and royalties — scoped to your tenant.
Quick Start
Your First Request
Pass your key in the X-API-Key header on every request:
curl https://api.undercore.pro/v1/works \ -H "X-API-Key: YOUR_KEY"
import { Undercore } from '@undercore/sdk'; const uc = new Undercore({ apiKey: 'YOUR_KEY' }); const result = await uc.works.list(); console.log(result.works);
from undercore import Undercore with Undercore(api_key="YOUR_KEY") as uc: result = uc.works.list() print(result.works)
Response Structure
List endpoints return a paginated envelope. Single-resource endpoints return the object under its type key:
{
"works": [ ... ],
"page": 1,
"per_page": 50,
"total": 142
}The Undercore API uses API keys passed in the X-API-Key HTTP header. Keys are tenant-scoped and scope-limited.
Header Format
X-API-Key: uc_live_<token>
Key Prefixes
| Prefix | Environment | Description |
|---|---|---|
| uc_live_ | Production | Full production access. Requires account at portal.undercore.pro. |
| uc_test_ | Test | For automated testing pipelines. Scoped to your tenant's test environment. |
| uc_sandbox_ | Sandbox | No-account-needed demo keys. Scoped to demo tenant with fictitious data only. Expires in 24h. |
Scopes
Each key is issued with a specific set of scopes. Requests that require a scope not granted to your key return 403 Forbidden.
| Scope | Access Granted | Endpoints |
|---|---|---|
| read:works | Read catalog works | GET /v1/works, GET /v1/works/{id} |
| read:writers | Read writers / composers | GET /v1/writers, GET /v1/writers/{id} |
| read:royalties | Read royalty statements | Future royalties endpoints |
| write:works | Create and update works | Future write endpoints |
Managing Keys
Keys are managed from the portal UI (Settings → API Keys) or via the management API:
# Requires portal session cookie (portal.undercore.pro) curl -X POST https://api.undercore.pro/api/undercore/api-keys \ -H "Content-Type: application/json" \ -d '{"name":"my-integration","scopes":["read:works","read:writers"],"expires_in_days":365}' \ --cookie "pt_undercore=YOUR_SESSION"
Base URL: https://api.undercore.pro — All endpoints require an X-API-Key header.
Endpoints
| Method | Path | Scope | Description |
|---|---|---|---|
| GET | /v1/ | any | API catalog and version info |
| GET | /v1/works | read:works | List works (paginated) |
| GET | /v1/works/{work_id} | read:works | Get single work |
| GET | /v1/writers | read:writers | List writers (paginated) |
| GET | /v1/writers/{writer_id} | read:writers | Get single writer |
Work Status Values
Works follow the CISAC lifecycle with 6 official status values:
| Status | Meaning |
|---|---|
| draft | Work created but not yet complete |
| needs_review | Work flagged for editorial review |
| documented | Complete with ISWC, holders, splits — not yet filed with CMOs |
| registered | Filed with performance and mechanical rights societies (≥1 each) |
| conflict | Split or ownership conflict detected |
| archived | Retired from active use; retained for historical reference |
Works represent musical compositions in your catalog. Each work has an optional ISWC, a lifecycle status, and a set of right holders with splits.
Returns a paginated list of works belonging to your tenant.
Query Parameters
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| page | integer | optional | 1 | Page number (1-indexed) |
| per_page | integer | optional | 50 | Items per page (max 200) |
Response
{
"works": [
{
"id": 1,
"title": "Midnight Serenade",
"iswc": "T-000.000.001-0",
"status": "registered",
"created_at": "2026-01-15T10:00:00Z",
"updated_at": "2026-02-01T08:30:00Z"
}
],
"page": 1,
"per_page": 50,
"total": 142
}Writers are the composers and lyricists registered in your catalog. Each writer has optional IPI and PRO affiliation fields.
Returns a paginated list of writers (composers / lyricists) for your tenant, ordered by name.
Query Parameters
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| page | integer | optional | 1 | Page number (1-indexed) |
| per_page | integer | optional | 50 | Items per page (max 200) |
Response
{
"writers": [
{
"id": 1,
"name": "ALEX DEMO",
"ipi": "00000000001",
"pro_affiliation": "ASCAP",
"created_at": "2026-01-10T09:00:00Z"
}
],
"page": 1,
"per_page": 50,
"total": 3
}Test the API with demo data instantly — no account needed. Sandbox keys are scoped to a demo tenant with fictitious publishing data and expire after 24 hours.
Generate a Sandbox Key
Click the button below to generate a uc_sandbox_ key instantly. No sign-up required.
The sandbox key grants read-only access (read:works, read:writers, read:royalties) to demo data. It cannot create or modify works.
Demo Data Available
The sandbox tenant contains the following fictitious publishing data for testing:
Works (5 demo entries)
| Title | ISWC | Status |
|---|---|---|
| Midnight Serenade | T-000.000.001-0 | registered |
| Electric Dreams | T-000.000.002-0 | documented |
| River of Stars | — | draft |
| Golden Hour | T-000.000.003-0 | registered |
| Echoes in the Dark | — | needs_review |
Writers (3 demo entries)
| Name | IPI | PRO |
|---|---|---|
| ALEX DEMO | 00000000001 | ASCAP |
| JORDAN SAMPLE | 00000000002 | BMI |
| RILEY TESTWRITER | 00000000003 | SESAC |
Sandbox Status API
curl https://api.undercore.pro/api/undercore/sandbox/status
Official thin-wrapper SDKs for TypeScript/Node and Python. Zero config beyond your API key.
@undercore/sdk
Works in any Node.js 18+ environment or bundler (Vite, webpack, esbuild). Zero runtime dependencies — uses the native fetch API.
npm install @undercore/sdk
Quick Start
import { Undercore } from '@undercore/sdk'; const uc = new Undercore({ apiKey: 'uc_live_...', // baseUrl: 'https://api.undercore.pro', // default }); // List works (paginated) const result = await uc.works.list({ page: 1, per_page: 50 }); console.log(result.works); // Work[] console.log(result.total); // number // Get single work const { work } = await uc.works.get(42); console.log(work.status); // 'registered' | 'documented' | ... // List writers const writers = await uc.writers.list();
Error Handling
import { Undercore, UndercoreAPIError } from '@undercore/sdk'; try { const work = await uc.works.get(99999); } catch (err) { if (err instanceof UndercoreAPIError) { console.log(err.status); // 404 console.log(err.detail); // "Obra no encontrada" } }
TypeScript Types
import { Work, Writer, WorksResponse } from '@undercore/sdk'; // Work.status is a discriminated union: // 'draft' | 'needs_review' | 'documented' | 'registered' | 'conflict' | 'archived' function isPublished(work: Work): boolean { return work.status === 'registered'; }
undercore-sdk
Supports Python 3.9+. Uses httpx under the hood — both sync and context manager patterns supported.
pip install undercore-sdk
Quick Start
from undercore import Undercore # Context manager (recommended — auto-closes HTTP connection) with Undercore(api_key="uc_live_...") as uc: # List works result = uc.works.list(page=1, per_page=50) for work in result.works: print(work.title, work.status) # Get single work work = uc.works.get(1) print(work.iswc) # Optional[str] # List writers writers = uc.writers.list() print(writers.total)
Error Handling
from undercore import Undercore, UndercoreError with Undercore(api_key="uc_live_...") as uc: try: work = uc.works.get(99999) except UndercoreError as e: print(e.status) # 404 print(e.detail) # "Obra no encontrada"
Typed Dataclasses
from undercore.types import Work, Writer, WorksResponse # Work.status is a Literal type: # "draft" | "needs_review" | "documented" | "registered" | "conflict" | "archived" def is_published(work: Work) -> bool: return work.status == "registered"
Rate limits are enforced per API key. When exceeded, the API returns 429 Too Many Requests.
Tiers
| Tier | Limit | Who Gets It |
|---|---|---|
| Free | 100 requests / hour | Default for all new keys and sandbox keys |
| Pro | 1,000 requests / hour | Professional plan subscribers |
| Enterprise | 10,000 requests / hour | Enterprise plan subscribers |
Rate Limit Response
HTTP/1.1 429 Too Many Requests Content-Type: application/json Retry-After: 3600 { "detail": "Rate limit exceeded" }
Best Practices
- Implement exponential backoff when receiving 429 responses
- Cache responses when polling the same resource frequently
- Use pagination (per_page up to 200) to reduce request count when fetching large datasets
- Use webhook notifications (when available) instead of polling for changes
All errors follow a consistent JSON format. HTTP status codes reflect standard semantics.
Error Format
{
"detail": "Human-readable error message"
}Status Codes
| Status | Meaning | Common Causes |
|---|---|---|
| 200 | OK | Request succeeded |
| 401 | Unauthorized | Missing X-API-Key header, invalid key, revoked key, expired key |
| 403 | Forbidden | Key does not have the required scope for the endpoint |
| 404 | Not Found | Resource ID does not exist or does not belong to your tenant |
| 422 | Unprocessable | Invalid query parameter types or values |
| 429 | Rate Limited | Too many requests within the time window for your tier |
| 500 | Server Error | Unexpected internal error. If persistent, contact api@undercore.pro |
Common Error Messages
| Message | Cause | Fix |
|---|---|---|
| X-API-Key header requerida | No key in request | Add X-API-Key: YOUR_KEY header |
| API key inválida o revocada | Key not found or revoked | Verify key is correct; generate new key if revoked |
| API key expirada | Key past its expiry date | Create a new key; sandbox keys expire after 24h |
| Scope 'read:works' requerido | Key missing required scope | Create a key with the needed scope |
| Obra no encontrada | Work ID not in your tenant | Verify the work ID belongs to your tenant |
| Writer no encontrado | Writer ID not in your tenant | Verify the writer ID belongs to your tenant |
All notable changes to the Undercore API are documented here. The API follows semantic versioning.