Getting Started

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.

ℹ New to the API? Use a sandbox key (uc_sandbox_*) to test with demo data instantly — no account needed. Go to Sandbox.

Quick Start

1
Create your account
Sign up at portal.undercore.pro with your publishing entity details.
2
Go to API Keys
In the portal sidebar, navigate to Settings → API Keys and click Create Key.
3
Choose your scopes
Select the permissions your key needs: read:works, read:writers, read:royalties, or write:works. Principle of least privilege.
4
Make your first request
Copy the key shown once at creation and make your first API call. The key is shown only once — store it securely.

Your First Request

Pass your key in the X-API-Key header on every request:

bash
curl https://api.undercore.pro/v1/works \
  -H "X-API-Key: YOUR_KEY"
typescript
import { Undercore } from '@undercore/sdk';

const uc = new Undercore({ apiKey: 'YOUR_KEY' });
const result = await uc.works.list();
console.log(result.works);
python
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:

json — list response
{
  "works": [ ... ],
  "page": 1,
  "per_page": 50,
  "total": 142
}
✓ The API is tenant-scoped. Every key is bound to exactly one tenant — you only ever see data belonging to your publishing entity. Cross-tenant access is architecturally prevented.
Authentication

The Undercore API uses API keys passed in the X-API-Key HTTP header. Keys are tenant-scoped and scope-limited.

Header Format

http
X-API-Key: uc_live_<token>

Key Prefixes

PrefixEnvironmentDescription
uc_live_ProductionFull production access. Requires account at portal.undercore.pro.
uc_test_TestFor automated testing pipelines. Scoped to your tenant's test environment.
uc_sandbox_SandboxNo-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.

ScopeAccess GrantedEndpoints
read:worksRead catalog worksGET /v1/works, GET /v1/works/{id}
read:writersRead writers / composersGET /v1/writers, GET /v1/writers/{id}
read:royaltiesRead royalty statementsFuture royalties endpoints
write:worksCreate and update worksFuture write endpoints
⚠ Keys are shown once. At creation, the raw key is returned a single time. It is never retrievable again — only its prefix is stored. If you lose a key, revoke it and create a new one.

Managing Keys

Keys are managed from the portal UI (Settings → API Keys) or via the management API:

bash — create key
# 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"
API Reference

Base URL: https://api.undercore.pro — All endpoints require an X-API-Key header.

Endpoints

MethodPathScopeDescription
GET/v1/anyAPI catalog and version info
GET/v1/worksread:worksList works (paginated)
GET/v1/works/{work_id}read:worksGet single work
GET/v1/writersread:writersList writers (paginated)
GET/v1/writers/{writer_id}read:writersGet single writer

Work Status Values

Works follow the CISAC lifecycle with 6 official status values:

StatusMeaning
draftWork created but not yet complete
needs_reviewWork flagged for editorial review
documentedComplete with ISWC, holders, splits — not yet filed with CMOs
registeredFiled with performance and mechanical rights societies (≥1 each)
conflictSplit or ownership conflict detected
archivedRetired from active use; retained for historical reference
Works

Works represent musical compositions in your catalog. Each work has an optional ISWC, a lifecycle status, and a set of right holders with splits.

GET /v1/works List works

Returns a paginated list of works belonging to your tenant.

Query Parameters

NameTypeRequiredDefaultDescription
pageintegeroptional1Page number (1-indexed)
per_pageintegeroptional50Items per page (max 200)

Response

json — 200 OK
{
  "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
}
Try It Live request to api.undercore.pro
X-API-Key
page per_page
GET /v1/works/{work_id} Get work by ID
Writers

Writers are the composers and lyricists registered in your catalog. Each writer has optional IPI and PRO affiliation fields.

GET /v1/writers List writers

Returns a paginated list of writers (composers / lyricists) for your tenant, ordered by name.

Query Parameters

NameTypeRequiredDefaultDescription
pageintegeroptional1Page number (1-indexed)
per_pageintegeroptional50Items per page (max 200)

Response

json — 200 OK
{
  "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
}
Try It Live request to api.undercore.pro
X-API-Key
page per_page
GET /v1/writers/{writer_id} Get writer by ID
Sandbox

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.

⚠ Sandbox keys expire in 24 hours. They are rate-limited to 100 requests/hour. For production access, create an account.

Demo Data Available

The sandbox tenant contains the following fictitious publishing data for testing:

Works (5 demo entries)

TitleISWCStatus
Midnight SerenadeT-000.000.001-0registered
Electric DreamsT-000.000.002-0documented
River of Stars—draft
Golden HourT-000.000.003-0registered
Echoes in the Dark—needs_review

Writers (3 demo entries)

NameIPIPRO
ALEX DEMO00000000001ASCAP
JORDAN SAMPLE00000000002BMI
RILEY TESTWRITER00000000003SESAC

Sandbox Status API

bash
curl https://api.undercore.pro/api/undercore/sandbox/status
SDKs

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.

bash — install
npm install @undercore/sdk

Quick Start

typescript
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

typescript
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

typescript
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.

bash — install
pip install undercore-sdk

Quick Start

python
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

python
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

python
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

Rate limits are enforced per API key. When exceeded, the API returns 429 Too Many Requests.

Tiers

TierLimitWho Gets It
Free100 requests / hourDefault for all new keys and sandbox keys
Pro1,000 requests / hourProfessional plan subscribers
Enterprise10,000 requests / hourEnterprise plan subscribers
ℹ To upgrade your tier, contact sales@undercore.pro or visit your billing settings at portal.undercore.pro.

Rate Limit Response

http — 429 Too Many Requests
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
Errors

All errors follow a consistent JSON format. HTTP status codes reflect standard semantics.

Error Format

json
{
  "detail": "Human-readable error message"
}

Status Codes

StatusMeaningCommon Causes
200OKRequest succeeded
401UnauthorizedMissing X-API-Key header, invalid key, revoked key, expired key
403ForbiddenKey does not have the required scope for the endpoint
404Not FoundResource ID does not exist or does not belong to your tenant
422UnprocessableInvalid query parameter types or values
429Rate LimitedToo many requests within the time window for your tier
500Server ErrorUnexpected internal error. If persistent, contact api@undercore.pro

Common Error Messages

MessageCauseFix
X-API-Key header requeridaNo key in requestAdd X-API-Key: YOUR_KEY header
API key inválida o revocadaKey not found or revokedVerify key is correct; generate new key if revoked
API key expiradaKey past its expiry dateCreate a new key; sandbox keys expire after 24h
Scope 'read:works' requeridoKey missing required scopeCreate a key with the needed scope
Obra no encontradaWork ID not in your tenantVerify the work ID belongs to your tenant
Writer no encontradoWriter ID not in your tenantVerify the writer ID belongs to your tenant
Changelog

All notable changes to the Undercore API are documented here. The API follows semantic versioning.

v1.0.0 2026-04-13 Initial Release
Introduced public API v1 with tenant-scoped authentication via API keys
GET /v1/works — List works with pagination (page, per_page up to 200)
GET /v1/works/{work_id} — Retrieve a single work by ID
GET /v1/writers — List writers with pagination
GET /v1/writers/{writer_id} — Retrieve a single writer by ID
API key management: create, list, revoke keys with scope selection
Rate limiting tiers: Free (100/hr), Pro (1,000/hr), Enterprise (10,000/hr)
Sandbox environment: instant uc_sandbox_ keys with demo publishing data, no account required
Official SDKs: @undercore/sdk (TypeScript/Node 18+, zero runtime deps) and undercore-sdk (Python 3.9+, httpx)
CISAC-compliant work status lifecycle: draft, needs_review, documented, registered, conflict, archived