Catalogue Ingest API

Push your catalogue of musical works into TakeRecord over HTTPS. Authenticate with a per-client key, upsert works, and get validation and gaps back in the response.

Get an API keyBase URLingest.takerecord.com

Overview#

  • What it is: a single endpoint, POST/api/v1/catalogue/works, that upserts one or more works into a catalogue.
  • Inbound only: the API accepts your data. It does not return your stored catalogue, and there is no GET/list/download here.
  • Strict and honest: the request schema is strict (a misspelled field is rejected, not ignored), shares are validated, and every transformation the pipeline applies to your data is reported back to you in the response.
  • Idempotent commits: retries are safe when you send an Idempotency-Key.

Getting your API key#

API keys are created in the TakeRecord app and are scoped to a single client (your account). To create one:

  1. Open Settings.
  2. Go to the API keys section.
  3. Click Manage API keys.
  4. Click Create key and give it a name.
  5. Copy the secret. It is shown once, at creation time, and starts with trk_live_. Store it somewhere safe (a secrets manager); if you lose it, revoke it and create a new one.

The key resolves your client server-side, so you never send a client id in normal use. Treat the key like a password: anyone holding it can write to your catalogue.


Authentication#

Send the key as a Bearer token:

code
Authorization: Bearer trk_live_...
  • The key identifies your client, so no x-client-id header is required.
  • If you do send x-client-id (or a body client_id), it must equal the client bound to the key. A mismatch is rejected (403 for the header, 400 for the body value).
  • A missing, invalid, expired, or revoked key returns 401.
  • Only a per-client trk_ key works. Generic or shared internal keys are rejected with 403.

Base URL#

code
https://ingest.takerecord.com

All requests are HTTPS. The only endpoint on this surface is POST/api/v1/catalogue/works.


Rate limits#

  • 60 requests per minute per key. Exceeding this returns 429; wait and retry with backoff.
  • Every response (success or error) includes an x-request-id header. Quote it when contacting support.

Idempotency#

Commits (not dry-runs) accept an Idempotency-Key header so a network retry never double-writes.

  • Send a unique Idempotency-Key per logical request (a UUID is ideal). When paging a large catalogue, use one key per chunk.
  • Same key + same body: the original stored response is replayed, with header Idempotent-Replay: true. Your retry is a no-op that returns the first result.
  • Same key + a request still processing: 409. Retry shortly.
  • Same key + a different body: 422. Keys are bound to the exact body they first ran; reusing one with changed content is an error.
  • Retention: keys are remembered for 24 hours.

Dry-run requests ignore the Idempotency-Key header (they persist nothing, so there is nothing to make idempotent).

For the strongest guarantee, also put a stable client_work_ref on every work: it makes a re-send an exact upsert (create-or-update) at the database level, so even a retry outside the 24 hour idempotency window cannot create a duplicate. The Idempotency-Key is the convenience layer, client_work_ref is the durable one.


POST /api/v1/catalogue/works#

Upsert one to 500 works into a catalogue. Creates a new catalogue when catalog_id is omitted, or appends to an existing one when it is provided.

Headers#

HeaderRequiredDescription
AuthorizationYesBearer trk_live_...
Content-TypeYesapplication/json
Idempotency-KeyRecommended (commit)Unique per request; makes a commit safe to retry. Ignored in dry-run.
x-client-idNoIf sent, must equal the key's client, else 403.

Query parameters#

ParamTypeDescription
dry_runboolean?dry_run=true validates and reports without persisting (equivalent to body "mode": "validate").

Request body#

The body is a strict JSON object. An unknown or misspelled field returns 400; it is not silently ignored. All shares (writer, publisher, master) are expressed on a 0 to 100 scale, so 50 means 50% and 1 means 1%. An out-of-range share is a 400.

json
{
  "catalog_name": "Q3 Catalogue Sync",
  "works": [
    {
      "primary_title": "Midnight Drive",
      "client_work_ref": "ACME-0001",
      "recording": {
        "artist_name": "Nova Ray",
        "isrc": "USRC12345678",
        "album_title": "Night Sessions",
        "release_date": "2024-06-15",
        "label": "Acme Records",
        "upc": "012345678901",
        "master_share": 100
      },
      "writers": [
        {
          "writer_first_name": "Alex",
          "writer_last_name": "Stone",
          "writer_ipi": "00123456789",
          "writer_role_code": "CA",
          "writer_pro_affiliation": "ASCAP",
          "writer_ownership_pct": 50,
          "writer_controlled": true
        }
      ],
      "publishers": [
        {
          "publisher_name": "Acme Publishing",
          "publisher_ipi": "00987654321",
          "publisher_collection_share": 25,
          "publisher_pr_share": 50,
          "publisher_pro_affiliation": "ASCAP",
          "publisher_controlled": true,
          "publisher_role": "E",
          "publisher_mlc_number": "800000001",
          "territory": "World",
          "linked_writer_index": 0
        }
      ],
      "iswc": "T-123.456.789-0",
      "mlc_song_code": "A12345",
      "duration": "03:42"
    }
  ]
}

Envelope (top level)#

FieldTypeRequiredDescription
client_idstringNoIf present, must match the client bound to your key.
catalog_namestringNoNames a new catalogue. Default: API Import YYYY-MM-DD.
catalog_idstringNoAppend to an existing catalogue. A new one is created if omitted. 404 if it is not one of yours.
modeenumNo"validate" (dry-run) or "commit" (default).
worksarrayYes1 to 500 works.

works[]#

FieldTypeRequiredDescription
primary_titlestringYesSong title.
client_work_refstringRecommendedYour own stable id for this work. It is the upsert key: re-sending the same ref updates that work instead of creating a duplicate, and the response echoes it against our work_id. On an update your payload is authoritative for the roster (the writers and publishers you send replace the stored ones), while identifiers you omit (iswc, duration, mlc_song_code) and data we discovered by enrichment are kept. An update always stays in the work's existing catalogue (a catalog_id in the request only governs where new works are created).
recordingobjectYesSee below.
writersarrayNoSee below.
publishersarrayNoSee below.
iswcstringNoCanonicalised on store.
mlc_song_codestringNoMLC song identifier.
durationstringNoCanonicalised to HH:MM:SS on store.

works[].recording#

FieldTypeRequiredDescription
artist_namestringYesPrimary recording artist.
isrcstringNoThe recommended match anchor. Canonicalised on store.
album_titlestringNoAlbum name.
release_datestringNoRelease date (ISO format recommended).
labelstringNoRecord label.
upcstringNoUniversal Product Code.
master_sharenumber (0-100)NoSound-recording ownership (SoundExchange "Percentage Claimed").

works[].writers[]#

FieldTypeRequiredDescription
writer_first_namestringYesWriter's first name.
writer_last_namestringYesWriter's last name.
writer_ipistringNoIPI name number (9 to 11 digits).
writer_role_codeenumNoOne of CA (default), C, A, AR, AD, SR, SA, TR.
writer_pro_affiliationstringNoPRO (e.g. ASCAP, BMI, SOCAN).
writer_ownership_pctnumber (0-100)NoThe writer's share of the composition.
writer_controlledbooleanNoWhether you control this writer. Default false.

works[].publishers[]#

FieldTypeRequiredDescription
publisher_namestringYesPublisher name.
publisher_ipistringNoPublisher IPI name number.
publisher_collection_sharenumber (0-100)NoMechanical collection share (the MLC value).
publisher_pr_sharenumber (0-100)NoPerformance collection share.
publisher_pro_affiliationstringNoPublisher's PRO.
publisher_controlledbooleanNoWhether this is a controlled publisher. Default false.
publisher_roleenumNoE (original), AM (admin), SE (sub-publisher).
publisher_mlc_numberstringNoMLC publisher number.
territorystringNoTerritory. Default "World".
linked_writer_indexintegerNo0-based index into this work's writers array: which writer this publisher collects for.
linked_writer_ipistringNoAlternative to the index: the IPI of the writer this publisher collects for.

Share semantics#

All shares use the 0 to 100 scale. 50 is 50%, 1 is 1%. Out-of-range values return 400.


Your data will be transformed#

TakeRecord normalises what you send, and it reports every change back in the response so nothing is silently mutated. Expect the following:

  • Shares are divided by 100 on store. You send 50; it is stored as the fraction 0.5. This is lossless and only affects how the value appears in downstream systems.
  • Identifiers are canonicalised. ISRC, ISWC, and duration are normalised to their canonical forms (for example duration becomes HH:MM:SS). The canonicalised count in the response tells you how many fields changed.
  • Junk identifiers are blanked to a gap. If an identifier cell holds prose or obvious junk rather than a real code, it is cleared and the field is reported as a gap. The cleansed_identifiers object counts these per identifier type (for example { "ISRC": 3 }).
  • Request-prose is stripped. Free-text instructions that leaked into structured fields are removed. The prose_stripped count reports how many works were affected.
  • Rosters can be split and de-duplicated. A single cell that lists several writers is split into separate writers, and duplicates are merged. Because of this the stored writer count can differ from what you sent; the roster_adjusted count reports how many works had their roster changed.

The response fields cleansed_identifiers, roster_adjusted, prose_stripped, and canonicalised report exactly what the pipeline changed.


Dry-run / validate mode#

To validate without persisting anything, use either:

  • the query param ?dry_run=true, or
  • the body field "mode": "validate".

A dry-run returns 200 (not 201), with:

  • work_ids: [],
  • every results[].work_id set to null,
  • the same gaps and validation report you would get from a real commit.

Nothing is stored, no catalogue is created, and Idempotency-Key is ignored. Use it to preview data quality before committing.


Sending a full catalogue (~5,000 songs)#

The endpoint accepts up to 500 works per request. To load a large catalogue, page it in chunks and let each chunk be independently retryable.

Rules of thumb:

  • Split into chunks of 500 works each.
  • Send one Idempotency-Key per chunk (a fresh UUID per chunk). A retried chunk then replays rather than double-writing.
  • Put a stable client_work_ref on every work so a re-send upserts (merges) instead of creating duplicates.
  • On 429 or any 5xx, retry the same chunk (same body, same key) with exponential backoff.
  • Optionally run each chunk as a dry-run first to preview gaps, then commit.

Pseudo-code:

code
for chunk in chunks(works, size=500):
    key = uuid()
    backoff = 1s
    while true:
        res = POST /api/v1/catalogue/works
              Authorization: Bearer trk_live_...
              Idempotency-Key: key
              body: { catalog_id, works: chunk }
        if res.status in (200, 201):
            break                       # success (idempotent replay counts as success)
        if res.status in (429, 500, 502, 503, 504):
            sleep(backoff); backoff = min(backoff * 2, 60s); continue
        raise res                       # 4xx other than 429: fix the request, do not retry

Create the catalogue once on the first chunk (omit catalog_id), then pass the returned catalog_id on every subsequent chunk so all chunks land in the same catalogue.

Curl: dry-run a single chunk, then commit it.

bash
# 1) Dry-run: validate this chunk, persist nothing (returns 200).
curl -X POST "https://ingest.takerecord.com/api/v1/catalogue/works?dry_run=true" \
  -H "Authorization: Bearer $TRK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "catalog_name": "Q3 Catalogue Sync",
    "works": [
      {
        "primary_title": "Midnight Drive",
        "client_work_ref": "ACME-0001",
        "recording": { "artist_name": "Nova Ray", "isrc": "USRC12345678", "master_share": 100 },
        "writers": [
          { "writer_first_name": "Alex", "writer_last_name": "Stone", "writer_ipi": "00123456789", "writer_ownership_pct": 50 }
        ],
        "publishers": [
          { "publisher_name": "Acme Publishing", "publisher_collection_share": 25, "linked_writer_index": 0 }
        ]
      }
    ]
  }'

# 2) Commit: same chunk, live, with an Idempotency-Key so a retry is safe (returns 201).
curl -X POST "https://ingest.takerecord.com/api/v1/catalogue/works" \
  -H "Authorization: Bearer $TRK_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 5f2c9a1e-8b3d-4c7a-9e21-0a1b2c3d4e5f" \
  -d '{
    "catalog_name": "Q3 Catalogue Sync",
    "works": [
      {
        "primary_title": "Midnight Drive",
        "client_work_ref": "ACME-0001",
        "recording": { "artist_name": "Nova Ray", "isrc": "USRC12345678", "master_share": 100 },
        "writers": [
          { "writer_first_name": "Alex", "writer_last_name": "Stone", "writer_ipi": "00123456789", "writer_ownership_pct": 50 }
        ],
        "publishers": [
          { "publisher_name": "Acme Publishing", "publisher_collection_share": 25, "linked_writer_index": 0 }
        ]
      }
    ]
  }'

Response#

A commit returns 201; a dry-run returns 200. The body is the same shape either way:

json
{
  "catalog_id": "cat-a1b2c3d4",
  "catalog_name": "Q3 Catalogue Sync",
  "mode": "commit",
  "works_created": 1,
  "works_updated": 0,
  "works_skipped": 0,
  "work_ids": ["REC-2026-00001"],
  "results": [
    { "client_work_ref": "ACME-0001", "work_id": "REC-2026-00001", "action": "created" }
  ],
  "gaps": [
    { "work_index": 0, "title": "Midnight Drive", "missing_fields": ["iswc"] }
  ],
  "validation": {
    "total_works": 1,
    "works_flagged": 1,
    "attestation_ran": true,
    "issues": [
      {
        "work_index": 0,
        "work_id": "REC-2026-00001",
        "title": "Midnight Drive",
        "fields": [
          {
            "field_name": "writer_ipi",
            "reason": "low_confidence",
            "code": "ipi_wrong_party",
            "suggested_action": "IPI \"00123456789\" is registered to a different party. Check this is the right writer."
          }
        ]
      }
    ]
  },
  "cleansed_identifiers": { "ISRC": 0 },
  "roster_adjusted": 0,
  "prose_stripped": 0,
  "canonicalised": 1
}

Fields#

FieldTypeDescription
catalog_idstringThe catalogue the works landed in (or would land in, for a dry-run).
catalog_namestringIts name.
modeenum"commit" or "validate".
works_createdintegerNew works created.
works_updatedintegerExisting works updated (matched by client_work_ref).
works_skippedintegerWorks skipped (a title already present when appending without a client_work_ref).
work_idsstring[]Ids of created works, format REC-YYYY-NNNNN (for example REC-2026-00001). Empty in dry-run.
resultsarrayPer-work echo: { client_work_ref, work_id, action } where action is "created", "updated", or "skipped". Reconcile your id to ours here. In dry-run, work_id is null.
gapsarrayPresence report: { work_index, title, missing_fields[] }.
validationobjectCorrectness report (see below).
cleansed_identifiersobjectOptional. Counts of identifier cells blanked to a gap, keyed by type (for example { "ISRC": 3 }).
roster_adjustedintegerWorks whose writer roster was split/de-duplicated.
prose_strippedintegerWorks that had request-prose removed from structured fields.
canonicalisedintegerIdentifier/duration fields normalised to canonical form.

gaps vs validation#

These are two complementary reports, and neither blocks the write. Works are always stored as delivered; both reports surface things to fix.

  • gaps is a presence report. It lists what is missing. Possible missing_fields: isrc, iswc, writers, writer_ipi, writer_ownership_pct, publishers, publisher_collection_share.
  • validation is a correctness report. It flags data that is present but wrong. validation.issues lists only flagged works; each fields[] entry is { field_name, reason, code, suggested_action }.

Validation code values:

codeMeaning
ipi_formatWriter/publisher IPI is not a 9 to 11 digit number.
ipi_unregisteredWriter IPI is in no registry (a fake or typo).
ipi_wrong_partyWriter IPI is registered, but to a different party (a valid-but-wrong-person IPI).
iswc_formatISWC present but not a valid ISWC.
isrc_formatISRC present but not a valid ISRC.
writer_share_overWriter ownership shares exceed 100%.
collection_share_overPublisher collection shares exceed 100%.

attestation_ran is false when the registry check was skipped (for example the registry was unavailable). Format and share checks still run; only the registry checks (ipi_unregistered, ipi_wrong_party) are skipped.


Errors#

Error bodies have the shape { "error": "message", "code"?: "..." }. Every response, including errors, carries an x-request-id header.

StatusMeaning
400Validation failed: an unknown/misspelled field, a share out of the 0 to 100 range, or a body client_id that does not match your key.
401Missing, invalid, expired, or revoked API key.
403Not a per-client key (a generic/shared key), missing scope, or an x-client-id header that does not match your key's client.
404catalog_id is not one of your catalogues.
409An Idempotency-Key is still being processed. Retry shortly.
422An Idempotency-Key was reused with a different request body.
429Rate limit exceeded (60/min). Back off and retry.
500Unexpected server error. Retry with backoff; quote the x-request-id.

Versioning#

This surface is versioned in the path (/api/v1/). Changes are additive: we may add new optional request fields and new response fields without notice, so parse the response leniently and ignore fields you do not recognise. Any breaking change ships under a new version path (for example /api/v2/); /api/v1/ will not change in a way that breaks existing integrations.