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#

HeaderRequiredDescription
AuthorizationYesBearer trk_live_...
Content-TypeYesapplication/json
Idempotency-KeyRecommended on commitUnique per request. Makes a commit safe to retry. Ignored in dry-run.
x-client-idNoIf sent, it must equal the key's client, or we return 403.

Query parameters#

ParameterTypeDescription
dry_runboolean?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#

FieldTypeRequiredDescription
worksarrayYes1 to 500 works.
client_artist_refstringNoYour stable artist id. Routes works to that artist's catalogue. See Catalogue routing.
catalog_namestringNoNames a new catalogue. Default: API Import YYYY-MM-DD.
catalog_idstringNoAppends to an existing catalogue. 404 if it is not one of yours.
client_idstringNoIf present, it must match the client bound to your key.
modeenumNo"validate" for a dry-run, or "commit". Default "commit".
enforcementenumNo"warn" (default) stores the works and reports the problems. "strict" refuses the request when a blocking issue is present, and stores nothing.

works[]#

FieldTypeRequiredDescription
primary_titlestringYesThe title of the work.
recordingobjectYesSee below.
client_work_refstringRecommendedYour own stable id for this work. See below.
writersarrayNoSee below.
publishersarrayNoSee below.
iswcstringNoWe canonicalise it on store.
mlc_song_codestringNoThe MLC identifier for the work.
durationstringNoWe 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:

json
{
  "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_held with 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 writers and publishers you 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_id in the request only governs where new works are created.

works[].recording#

FieldTypeRequiredDescription
artist_namestringYesThe primary recording artist.
isrcstringNoThe recommended match anchor. We canonicalise it on store.
album_titlestringNoThe album name.
release_datestringNoThe release date. ISO format is best.
labelstringNoThe record label.
upcstringNoThe Universal Product Code of the release.
master_sharenumber (0-100)NoOwnership of the sound recording, not of the work. Leave it out if you do not know it.
spotify_urlstringNoA link to your release on Spotify, not to the original.
apple_urlstringNoA link to your release on Apple Music. We keep the track id from it.
original_release_datestringNoYYYY-MM-DD. When the recording FIRST came out, when that differs from release_date.
is_rereleasebooleanNoTrue when this release is the same recording issued again, rather than a new one.
migrated_onstringNoYYYY-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[]#

FieldTypeRequiredDescription
writer_first_namestringYesThe writer's first name.
writer_last_namestringYesThe writer's last name.
writer_middle_namestringNoThe middle name or initial, if the writer registered with one.
writer_ipistringNoThe IPI name number, 9 to 11 digits. Not needed for a writer with no society: see below.
writer_role_codeenumNoOne of CA (default), C, A, AR, AD, SR, SA, TR.
writer_pro_affiliationstringNoThe writer's society, such as ASCAP or BMI. We accept 279 codes: see below.
writer_ownership_pctnumber (0-100)NoThe writer's share of the work.
writer_controlledbooleanWhen answeredtrue when the writer agreed that you collect for them, false when the writer declined. Leave it out when nobody asked. See below.
authorized_onstring (YYYY-MM-DD)When controlledThe date this writer agreed to let you administer their share.
authorization_refstringWhen controlledYour own reference for that agreement, so we can ask you for it later.
writer_emailstringNoA 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:

text
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[]#

FieldTypeRequiredDescription
publisher_namestringYesThe publisher name.
publisher_ipistringNoThe publisher IPI name number.
publisher_collection_sharenumber (0-100)NoThe mechanical collection share. This is the MLC value.
publisher_pr_sharenumber (0-100)NoThe performance collection share.
publisher_pro_affiliationstringNoThe publisher's PRO.
publisher_controlledbooleanNoWhether this is a controlled publisher. Default false.
publisher_roleenumNoE original publisher, AM administrator, SE sub-publisher.
publisher_mlc_numberstringNoThe MLC publisher number.
territorystringNoThe territory. Default "World".
linked_writer_indexintegerNoA 0-based index into this work's writers array.
linked_writer_ipistringNoThe 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.

json
"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.

StatusMeaning
not_registered_by_recordThe work has no controlled writer, so Record registers nothing for it. This is not a fault. fix is empty.
heldThe 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.
blockedAt least one thing you can send is missing or wrong. fix lists each one.
waiting_on_recordEverything that remains belongs to Record, for example a field on a Record publisher row. fix is empty.
readyThe 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:

CodeWhat to send
primary_titleThe title of the work.
writersAt least one writer.
has_identifiable_writerA real name for at least one writer. "Unknown Writer" is not a name.
writer_nameEach writer's name in Latin letters. For a name in another script, the romanised form.
writer_last_nameA last name for each writer who opted in.
writer_role_codeA role for each writer, for example CA for a writer of music and lyrics.
writer_pro_affiliationThe society of each writer, or "NA" if they have none. A blank means unknown, so the work waits.
writer_ownership_pctThe ownership share of each writer.
writer_ipiThe IPI of each writer who has a society. A writer sent with "NA" needs no IPI.
writer_share_totalWriter 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_reconcilesWriter ownership shares that add up to exactly 100%. The roster you sent is under 100%.
writer_composer_roleThe role C (composer) or CA (composer and author) on at least one writer who opted in.
writers_within_ebr_limitAt most 10 writers on one work.
mlc_recording_block_completeThe recording title and the artist name, because you sent an ISRC or a label.
isrcThe 12-character ISRC of the recording, for example USRC17607839.
controlled_publisherA controlled publisher.
publisher_nameA name for each publisher.
publisher_ipiA valid IPI for each controlled publisher.
publisher_pro_affiliationThe society of each publisher.
publisher_collection_shareThe collection share of each controlled publisher.
collection_share_positiveA collection share above 0% on at least one controlled publisher.
collection_share_totalPublisher collection shares lowered so they add up to 100% or less.
mlc_publisher_writer_linkA controlled publisher linked to each controlled writer.
mlc_unlinked_publisherA controlled writer linked to each controlled publisher.
publisher_writer_linkA publisher linked to each controlled writer.
unlinked_publisherA writer linked to each publisher.
publishers_within_ebr_limitAt 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.

json
"hold_details": [
  { "hold": "third_party_publisher", "writer": "Ada Lee", "administrator": "Songtrust" }
]