- Docs
- Managed Transfers
Transfer results and unmatched items
The Match Results API gives your backend the items that a transfer could not move: the tracks, albums and artists that found no match, or that the destination refused. Use it to show the user what is missing, to retry with your own catalog search, or to measure your catalog gaps.
| Call | What it returns |
|---|---|
| GET/public/transfers/{transferUuid} | The summary: status, counters, source and destination, timestamps. |
| GET/public/transfers/{transferUuid}/unmatched | The unmatched and failed items of one transfer, one page at a time. |
| GET/public/transfers/unmatched | The unmatched and failed items of every transfer that finished in a time window. For a daily pull. |
When to call it
After match.report_ready. The webhook carries resultsUrl, the address of the unmatched items of that transfer. Or once a day, with the bulk endpoint. Do not poll a running transfer: webhooks tell you when it ends.
Authentication
Server to server, with Basic (Client ID and Client Secret). The report holds your users' libraries.
- No credential:
401. - A Client ID token, a DevToken or a one-time token:
403. - Managed Transfers off for your account:
403. - A transfer of another account:
404, the same answer as a uuid that does not exist.
A signed-in docs session may also read it, so Try it works here. These calls are not billed.
Fetch the unmatched items
curl "https://api.musicapi.com/public/transfers/6a002d4b-f95b-471f-a27d-609a42e10cdb/unmatched?limit=500" \
-u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET"
Query parameters
nextCursor of the previous page. Leave it out for the first page. Both lists.from.to. The window is at most 31 days.Pagination
Each list answers { "unmatched": [...], "nextCursor": "..." }. Send nextCursor back as cursor, and stop when it is null. The cursor is opaque: do not build or change it.
The rows of one transfer come in a stable order, so a page never repeats or skips a row. The bulk endpoint orders by transfer, then by item. A bulk page can hold fewer rows than limit and still have a nextCursor: keep going until it is null.
One row
{
"transferUuid": "6a002d4b-f95b-471f-a27d-609a42e10cdb",
"title": "Low",
"artist": "Artist A, Artist B",
"playlistName": "Road trip",
"reason": "lowConfidence",
"bestGuess": "Low (Live) — Artist A",
"bestGuessUrl": "https://tidal.com/track/123456789",
"status": "unmatched",
"itemType": "track",
"matchingPercentage": 55,
"source": {
"id": "4uLU6hMCjMI75M1A2tKUQC",
"entryId": null,
"name": "Low",
"artists": ["Artist A", "Artist B"],
"album": "The Album",
"isrc": "USRC17607839",
"durationMs": 201000
},
"group": { "type": "playlist", "name": "Road trip", "sourceId": "37i9dQZF1DXcBWIGoYBM5M" },
"bestGuessCandidate": {
"id": "123456789",
"name": "Low (Live)",
"artists": ["Artist A"],
"album": "Live",
"isrc": null,
"url": "https://tidal.com/track/123456789"
}
}
| Field | Meaning |
|---|---|
title, artist, playlistName, reason, bestGuess | The same values as the results page and its CSV export. |
status | unmatched: no good match. failed: the destination refused the item. |
itemType | track, album or artist. |
matchingPercentage | The score of the best guess, 0 to 100. null without a guess. |
source | The item on the source service. A field is null when the source does not give it. |
group | The collection that held the item: playlist, likedTracks, albums, artists or podcasts. sourceId is the source playlist id, null for the other types. |
bestGuessCandidate | The closest item on the destination, which we did not add. null when there is none. |
bestGuessUrl | The web address of the best guess. null without a guess, or when the service gives no address. |
Reasons
reason | What happened |
|---|---|
notFound | The destination has no item that matches. |
lowConfidence | The best match scored below your match threshold. bestGuessCandidate holds it. |
regionLocked | The match exists, but not in the user's country. |
error | The item failed on the way to the destination (status is failed). |
cannotMatch | The user looked at the candidates and refused all of them. |
limitReached | The user's plan allows no more items. |
The summary
curl "https://api.musicapi.com/public/transfers/6a002d4b-f95b-471f-a27d-609a42e10cdb" \
-u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET"
The answer has the status, the counters (total, processed, matched, unmatched, skipped, failed), matchRatePct, durationSeconds, the source and target sides and the timestamps.
matched counts what we added, and skipped what the destination already had. A second run of a finished transfer reports almost everything as skipped and almost nothing as matched: it changed nothing.