Catalogue Ingest API
Send works
The request body for POST /api/v1/catalogue/works, field by field.
POST/api/v1/catalogue/works
Upsert one to 500 works. We create a catalogue when you omit catalog_id, or append to an existing one
when you send it.
Headers#
| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer trk_live_... |
Content-Type | Yes | application/json |
Idempotency-Key | Recommended on commit | Unique per request. Makes a commit safe to retry. Ignored in dry-run. |
x-client-id | No | If sent, it must equal the key's client, or we return 403. |
Query parameters#
| Parameter | Type | Description |
|---|---|---|
dry_run | boolean | ?dry_run=true validates and reports without a write. Same as body "mode": "validate". |
Request body#
The body is a strict JSON object. An unknown or misspelled field returns 400. We do not ignore it.
All shares use a 0 to 100 scale, so 50 means 50% and 1 means 1%. A share outside that range returns
400.
Envelope#
| Field | Type | Required | Description |
|---|---|---|---|
works | array | Yes | 1 to 500 works. |
client_artist_ref | string | No | Your stable artist id. Routes works to that artist's catalogue. See Catalogue routing. |
catalog_name | string | No | Names a new catalogue. Default: API Import YYYY-MM-DD. |
catalog_id | string | No | Appends to an existing catalogue. 404 if it is not one of yours. |
client_id | string | No | If present, it must match the client bound to your key. |
mode | enum | No | "validate" for a dry-run, or "commit". Default "commit". |
enforcement | enum | No | "warn" (default) stores the works and reports the problems. "strict" refuses the request when a blocking issue is present, and stores nothing. |
works[]#
| Field | Type | Required | Description |
|---|---|---|---|
primary_title | string | Yes | The title of the work. |
recording | object | Yes | See below. |
client_work_ref | string | Recommended | Your own stable id for this work. See below. |
writers | array | No | See below. |
publishers | array | No | See below. |
iswc | string | No | We canonicalise it on store. |
mlc_song_code | string | No | The MLC identifier for the work. |
duration | string | No | We canonicalise it to HH:MM:SS on store. |
Covers, remixes and edits#
A new recording of an existing song does not make you a writer of that song. If your release is a remix, an edit, a cover, a sample or an arrangement, say so:
{
"primary_title": "Lady (Hear Me Tonight) - Slowed",
"recording": { "artist_name": "Your Artist", "isrc": "..." },
"derivative_of": { "kind": "cover", "iswc": "T-070.123.456-1" }
}
kind is one of remix, cover, edit, sample or arrangement. Send iswc or mlc_song_code
for the original where you know it, or neither.
A declared remix, cover or edit claims nothing on the composition. We do not ask you for a
songwriter split, and we register none. A sample or an arrangement can carry a real share. We
still ask for the roster, and a person reviews the work.
We never guess this from a title. A word like "slowed" in a title is not a declaration, so a work
without derivative_of is a claim that you wrote it.
About client_work_ref#
client_work_ref is the upsert key, and it is the single most useful field you can send.
- Re-sending the same ref updates that work. It does not create a duplicate.
- A ref that carries an ISRC you already hold under another ref is a second work for one recording. We store it and report
isrc_already_heldwith the ref that holds the ISRC, on every send. One recording on two releases is one work: send the same ref for it on every release. - The response echoes your ref beside our
work_id, so you can reconcile the two systems. - On an update your payload is authoritative for the roster. The
writersandpublishersyou send replace the stored ones. - An update that carries no writers keeps the stored roster. We also report
roster_wipe, so the omission stays visible. An absent field is not an instruction to erase a roster. A partner who corrects a title must not lose every writer by leaving the field out. - To remove one writer, send the roster that remains. There is no way to empty a roster in one call, and that is deliberate.
- Identifiers you omit are kept, not cleared. So are values we found by enrichment.
- An update stays in the work's existing catalogue. A
catalog_idin the request only governs where new works are created.
works[].recording#
| Field | Type | Required | Description |
|---|---|---|---|
artist_name | string | Yes | The primary recording artist. |
isrc | string | No | The recommended match anchor. We canonicalise it on store. |
album_title | string | No | The album name. |
release_date | string | No | The release date. ISO format is best. |
label | string | No | The record label. |
upc | string | No | The Universal Product Code of the release. |
master_share | number (0-100) | No | Ownership of the sound recording, not of the work. Leave it out if you do not know it. |
spotify_url | string | No | A link to your release on Spotify, not to the original. |
apple_url | string | No | A link to your release on Apple Music. We keep the track id from it. |
original_release_date | string | No | YYYY-MM-DD. When the recording FIRST came out, when that differs from release_date. |
is_rerelease | boolean | No | True when this release is the same recording issued again, rather than a new one. |
migrated_on | string | No | YYYY-MM-DD. When the release moved to you from another distributor. |
Send a link where you have one. It lets us confirm that the ISRC belongs to the recording you distributed. It says nothing about who wrote the song, so it never replaces the writer splits.
Send the ISRC where you have it. It is the strongest anchor we have for matching your recording to industry data.
works[].writers[]#
| Field | Type | Required | Description |
|---|---|---|---|
writer_first_name | string | Yes | The writer's first name. |
writer_last_name | string | Yes | The writer's last name. |
writer_middle_name | string | No | The middle name or initial, if the writer registered with one. |
writer_ipi | string | No | The IPI name number, 9 to 11 digits. Not needed for a writer with no society: see below. |
writer_role_code | enum | No | One of CA (default), C, A, AR, AD, SR, SA, TR. |
writer_pro_affiliation | string | No | The writer's society, such as ASCAP or BMI. We accept 279 codes: see below. |
writer_ownership_pct | number (0-100) | No | The writer's share of the work. |
writer_controlled | boolean | When answered | true when the writer agreed that you collect for them, false when the writer declined. Leave it out when nobody asked. See below. |
authorized_on | string (YYYY-MM-DD) | When controlled | The date this writer agreed to let you administer their share. |
authorization_ref | string | When controlled | Your own reference for that agreement, so we can ask you for it later. |
writer_email | string | No | A contact address for the writer. |
Send the authorization fields for every writer you mark as controlled. The flag on its own says we administer the share. It does not say who agreed, and a society that questions the claim asks us exactly that. You do not need to send us the signed document.
Collect names however suits your interface, but send the split fields above. Use the name the writer
registered with their PRO. Do not send a single writer_full_name. We reject unknown fields.
A writer with no society#
A society registers a writer who is not a member as a non-member. Such a writer holds no IPI (Interested Party Information) number, and none is needed.
Send writer_pro_affiliation as one of these values, and leave writer_ipi out:
NA N/A AN NONE NON-AFFILIATED NON AFFILIATED UNAFFILIATED
Our export checks apply the same exemption, so such a writer does not become a gap.
A writer who does hold an IPI number must still send it. We check its format and its check digits.
Society codes#
We accept the 279 CISAC (International Confederation of Societies of Authors and Composers) society
codes. The full list, with the non-member values above, is at
pro-codes.json.
Check a value against that list at your own intake. The person who typed it is still on screen, and only they can correct it. A value we refuse later reaches you long after they have gone.
works[].publishers[]#
| Field | Type | Required | Description |
|---|---|---|---|
publisher_name | string | Yes | The publisher name. |
publisher_ipi | string | No | The publisher IPI name number. |
publisher_collection_share | number (0-100) | No | The mechanical collection share. This is the MLC value. |
publisher_pr_share | number (0-100) | No | The performance collection share. |
publisher_pro_affiliation | string | No | The publisher's PRO. |
publisher_controlled | boolean | No | Whether this is a controlled publisher. Default false. |
publisher_role | enum | No | E original publisher, AM administrator, SE sub-publisher. |
publisher_mlc_number | string | No | The MLC publisher number. |
territory | string | No | The territory. Default "World". |
linked_writer_index | integer | No | A 0-based index into this work's writers array. |
linked_writer_ipi | string | No | The IPI of the linked writer. An alternative to the index. |
Response#
A commit returns 201. A dry-run returns 200. The body has the same shape either way. See
Validation and gaps for every response field.
Each stored results[] entry carries an isrc verdict: what Apple Music lists under the code you
sent. On a mismatch or shared verdict the work goes to no MLC, EBR, CWR or AllTrack file until the
code is right.
Readiness#
Each stored results[] entry carries a readiness object. It says whether the work is ready for the
MLC and for the PROs, and what you can send. A skipped entry has none.
"readiness": {
"mlc": { "status": "ready", "fix": [] },
"pro": {
"status": "blocked",
"fix": [
{ "code": "writer_pro_affiliation", "message": "Send the society of each writer, or send \"NA\" if they have none. A blank society means unknown, so the work waits." },
{ "code": "writer_ownership_reconciles", "message": "Change the writer ownership shares so they add up to exactly 100%." }
]
}
}
status is one of five values. We give the first one that applies.
| Status | Meaning |
|---|---|
not_registered_by_record | The work has no controlled writer, so Record registers nothing for it. This is not a fault. fix is empty. |
held | The work fails a hold in holds. It goes to no society until every hold clears. fix still lists what you can send, and never lists a hold. |
blocked | At least one thing you can send is missing or wrong. fix lists each one. |
waiting_on_record | Everything that remains belongs to Record, for example a field on a Record publisher row. fix is empty. |
ready | The work needs nothing more. |
fix[].code is the contract, and fix[].message is one or two sentences for your end user. The
wording may change. The codes:
| Code | What to send |
|---|---|
primary_title | The title of the work. |
writers | At least one writer. |
has_identifiable_writer | A real name for at least one writer. "Unknown Writer" is not a name. |
writer_name | Each writer's name in Latin letters. For a name in another script, the romanised form. |
writer_last_name | A last name for each writer who opted in. |
writer_role_code | A role for each writer, for example CA for a writer of music and lyrics. |
writer_pro_affiliation | The society of each writer, or "NA" if they have none. A blank means unknown, so the work waits. |
writer_ownership_pct | The ownership share of each writer. |
writer_ipi | The IPI of each writer who has a society. A writer sent with "NA" needs no IPI. |
writer_share_total | Writer ownership shares that add up to exactly 100%. The roster you sent is over 100%. We then omit writer_ownership_reconciles, which says the same thing. |
writer_ownership_reconciles | Writer ownership shares that add up to exactly 100%. The roster you sent is under 100%. |
writer_composer_role | The role C (composer) or CA (composer and author) on at least one writer who opted in. |
writers_within_ebr_limit | At most 10 writers on one work. |
mlc_recording_block_complete | The recording title and the artist name, because you sent an ISRC or a label. |
isrc | The 12-character ISRC of the recording, for example USRC17607839. |
controlled_publisher | A controlled publisher. |
publisher_name | A name for each publisher. |
publisher_ipi | A valid IPI for each controlled publisher. |
publisher_pro_affiliation | The society of each publisher. |
publisher_collection_share | The collection share of each controlled publisher. |
collection_share_positive | A collection share above 0% on at least one controlled publisher. |
collection_share_total | Publisher collection shares lowered so they add up to 100% or less. |
mlc_publisher_writer_link | A controlled publisher linked to each controlled writer. |
mlc_unlinked_publisher | A controlled writer linked to each controlled publisher. |
publisher_writer_link | A publisher linked to each controlled writer. |
unlinked_publisher | A writer linked to each publisher. |
publishers_within_ebr_limit | At most 10 publishers on one work. |
Where Record is the publisher of your controlled writers, Record writes its own publisher rows. A
publisher code that fails on a Record row belongs to Record, and it never appears in fix. A
publisher code that fails on a row that you sent stays in fix.
hold_details#
hold_details carries one entry per value in holds. A third_party_publisher entry names the held
writer and the publisher that currently administers them. We never guess a writer from a name, so a
publisher row that links to no writer gives "writer": null. Every other hold gives null for both.
"hold_details": [
{ "hold": "third_party_publisher", "writer": "Ada Lee", "administrator": "Songtrust" }
]