# Transfer results and unmatched items
URL: https://musicapi.com/docs/managed-transfers/results-and-unmatched.md

> The Match Results API gives your backend the summary of a transfer and every item that did not move, with the reason and the best guess. Per transfer or as a daily bulk pull.

Product: MusicAPI Managed Transfers. Section: Managed Transfers.

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}`](https://musicapi.com/docs/reference/get-transfer-result.md)                     | The summary: status, counters, source and destination, timestamps.                                 |
| [`GET /public/transfers/{transferUuid}/unmatched`](https://musicapi.com/docs/reference/list-transfer-unmatched-items.md) | The unmatched and failed items of one transfer, one page at a time.                                |
| [`GET /public/transfers/unmatched`](https://musicapi.com/docs/reference/list-unmatched-items.md)                         | 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](https://musicapi.com/docs/webhooks/set-up-and-verify.md) 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

```bash
curl "https://api.musicapi.com/public/transfers/6a002d4b-f95b-471f-a27d-609a42e10cdb/unmatched?limit=500" \
  -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET"
```

```bash
curl "https://api.musicapi.com/public/transfers/unmatched?from=2026-10-15T00:00:00Z&to=2026-10-16T00:00:00Z&limit=500" \
  -u "$MUSICAPI_CLIENT_ID:$MUSICAPI_CLIENT_SECRET"
```

```js
async function allUnmatched(transferUuid) {
  const auth = Buffer.from(`${process.env.MUSICAPI_CLIENT_ID}:${process.env.MUSICAPI_CLIENT_SECRET}`).toString("base64");
  const rows = [];
  let cursor = null;
  do {
    const url = new URL(`https://api.musicapi.com/public/transfers/${transferUuid}/unmatched`);
    url.searchParams.set("limit", "500");
    if (cursor !== null) url.searchParams.set("cursor", cursor);
    const response = await fetch(url, { headers: { Authorization: `Basic ${auth}` } });
    if (!response.ok) throw new Error(`MusicAPI ${response.status}`);
    const page = await response.json();
    rows.push(...page.unmatched);
    cursor = page.nextCursor;
  } while (cursor !== null);
  return rows;
}
```

## Query parameters

- `reason` (string): Return only the rows with this reason (table below). Both lists.
- `limit` (integer): Rows per page, at most 500. Both lists.
- `cursor` (string): The `nextCursor` of the previous page. Leave it out for the first page. Both lists.
- `from` (ISO 8601, required): Bulk only. A transfer is in the window when it finished at or after `from`.
- `to` (ISO 8601): Bulk only. A transfer is in the window when it finished before `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

```json
{
  "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

```bash
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.
