> ## Documentation Index
> Fetch the complete documentation index at: https://docs.punchplay.tv/llms.txt
> Use this file to discover all available pages before exploring further.

# Partner Sync

> Mirror a PunchPlay account and write back in bulk, without polling the library endpoints.

The library endpoints — history, ratings, collection, lists — are shaped for
presentation. Re-downloading them on a timer is expensive for you and for
PunchPlay, and it cannot tell you what changed.

Partner sync exists for clients that maintain their own copy of an account: a
durable change feed to read from, and bulk endpoints to write back through.

## When to use it

Use partner sync if you keep a local mirror of a user's library and need to stay
in step with it. If you only read a screenful at a time, the ordinary library
endpoints with `limit` and `cursor` are simpler and enough.

## Reading: snapshot, then follow the feed

The change feed is populated by database triggers, so web activity, imports,
provider webhooks, first-party apps, and your own writes all land in one ordered
feed per account. Resources are filtered to the read scopes on your token.

<Steps>
  <Step title="Open a cursor">
    Call `GET /api/platform/v1/me/sync/changes` with no cursor. Keep the
    returned `nextCursor`. A new client sees `resetRequired: true`.
  </Step>

  <Step title="Backfill each resource">
    For every resource named in `resources`, page through
    `GET /api/platform/v1/me/sync/snapshot?resource=...&after=...&limit=500`
    until it is exhausted.
  </Step>

  <Step title="Follow the feed">
    Call `/me/sync/changes?cursor=RETAINED_CURSOR`. Apply changes in the order
    returned, and persist each `nextCursor` only after that page has committed
    locally. If you persist the cursor first and then fail, you have silently
    skipped changes.
  </Step>

  <Step title="Handle tombstones and resets">
    `operation: "delete"` with `data: null` is a tombstone — remove the row. If
    `resetRequired` ever becomes `true`, discard the mirror and repeat the
    snapshot step.
  </Step>
</Steps>

Cursors stay valid for 90 days. A cursor older than that returns
`resetRequired: true` rather than an error.

## Writing: bulk endpoints

| Data                             | Endpoint                               | Scope           |
| -------------------------------- | -------------------------------------- | --------------- |
| History                          | `POST /api/platform/v1/sync/history`   | `history:write` |
| Ratings, favourites, statuses    | `POST /api/platform/v1/sync/ratings`   | `ratings:write` |
| Watchlist additions and removals | `POST /api/platform/v1/sync/watchlist` | `lists:write`   |

Each request takes up to 100 items within a 512 KiB body. Size batches against
the byte limit as well as the item count — a batch of long-titled entries can
reach 512 KiB before it reaches 100 items.

Responses carry one outcome per submitted index. An `invalid` item does not fail
its valid neighbours, so surface the per-item error text rather than treating
the whole batch as failed.

History items also need a stable `client_item_id`, derived from your source
watch event, for example `nuvio-profile-4:history:1735927200:tmdb:550`. It
prevents duplicates across separately keyed batches.

## Idempotency

Every bulk write requires an `Idempotency-Key` header. Keys are retained for 24
hours, and replaying a key with the same body returns the original result rather
than applying it twice.

Three responses need different handling, and the difference matters:

| Response                        | Meaning                                       | What to do                        |
| ------------------------------- | --------------------------------------------- | --------------------------------- |
| `409 request_in_progress`       | The original request is still running         | Wait, retry the **same** key      |
| `409 idempotency_conflict`      | The key was used with a different body        | Fix the body, or use a new key    |
| `409 idempotency_indeterminate` | The original died before recording its result | Reconcile, then use a **new** key |

<Warning>
  `idempotency_indeterminate` means the write **may or may not** have applied.
  PunchPlay will not repeat it blindly, because doing so could double-log a
  user's watch history. Read `/me/sync/changes` to establish what actually
  landed, then retry the remainder under a new key. Reusing the old key only
  returns the same error.
</Warning>

## Rate limits

Bulk endpoints allow 30 requests per minute per operation, app, and user, and
the change and snapshot reads allow 120 per minute per app and user. The
per-token and per-app buckets in
[Errors & Rate Limits](/errors-rate-limits) apply on top of those.

Throttle on `X-RateLimit-Remaining` and honour `Retry-After` on `429`.
