> ## 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.

# Errors & Rate Limits

> Understand PunchPlay's current developer-facing failure modes and request limits.

This page documents the current developer-facing error behavior for the versioned public and platform APIs.

## Platform error shape

Platform endpoints return machine-readable errors shaped like this:

```json theme={null}
{
  "error": "insufficient_scope",
  "message": "This token is missing the required scope: playback:write.",
  "request_id": "ppreq_1234567890abcdef1234",
  "required_scope": "playback:write"
}
```

Platform responses also include `X-PunchPlay-Request-Id` in the headers. This
envelope is consistent across native platform handlers, including errors from
the underlying list, history, rating, collection, and profile routes.

## Common HTTP status codes

| Status | Meaning                                                                         |
| ------ | ------------------------------------------------------------------------------- |
| `200`  | Request succeeded                                                               |
| `400`  | Invalid request body, parameter, scope request, or unresolved device-code state |
| `401`  | Missing, invalid, expired, revoked, or mismatched credentials                   |
| `403`  | Token does not have access to the requested surface                             |
| `408`  | Request body was not received within 10 seconds                                 |
| `404`  | Unknown route or missing resource                                               |
| `413`  | Request body exceeded the 64 KiB platform limit                                 |
| `429`  | Too many requests                                                               |
| `500`  | Temporary server-side failure                                                   |

## Common platform error codes

| Error                       | Meaning                                                                                             |
| --------------------------- | --------------------------------------------------------------------------------------------------- |
| `authorization_pending`     | User has not approved the device code yet                                                           |
| `conflict`                  | The request could not be applied against the current state                                          |
| `expired`                   | Device code is invalid or expired                                                                   |
| `first_party_only`          | The endpoint is reserved for an official PunchPlay client                                           |
| `idempotency_conflict`      | The `Idempotency-Key` was already used with a different request body                                |
| `idempotency_indeterminate` | An earlier request with this key died before recording its result, so whether it applied is unknown |
| `invalid_client`            | `client_id` or `client_secret` is invalid                                                           |
| `invalid_grant`             | Refresh token or authorization code is invalid, expired, revoked, or already used                   |
| `invalid_request`           | Required fields are missing or malformed                                                            |
| `invalid_scope`             | The request asked for a scope outside the app's configured `allowed_scopes`                         |
| `invalid_token`             | The bearer token is not allowed to use the platform API                                             |
| `insufficient_scope`        | The bearer token does not include the scope required by the endpoint                                |
| `not_found`                 | The requested platform resource does not exist                                                      |
| `rate_limited`              | The request exceeded a current limit                                                                |
| `request_in_progress`       | A request with this `Idempotency-Key` is still running                                              |
| `server_error`              | Temporary server-side failure                                                                       |
| `unauthorized_client`       | The app is inactive or unknown for the attempted OAuth flow                                         |
| `unsupported_grant_type`    | `grant_type` is not supported                                                                       |
| `unsupported_response_type` | `response_type` is not supported                                                                    |

## Current rate limits

These limits reflect the current implementation as of July 28, 2026.

JSON and form request bodies accepted by the Platform API are limited to 64
KiB and must arrive within 10 seconds. A body at exactly 64 KiB is accepted.
Bulk sync endpoints accept up to 512 KiB, because a 100-item batch can exceed
the ordinary ceiling.

### Every authenticated request

Two buckets apply to every request that carries a bearer token. The
endpoint-specific limits below apply **on top of** these, so the tighter of the
two is what you will hit first.

| Bucket    | Scope                     | Limit                                             |
| --------- | ------------------------- | ------------------------------------------------- |
| Per token | app + user                | 300 requests per minute for reads, 120 for writes |
| Per app   | app, across all its users | 6,000 requests per minute                         |

### Rate limit headers

Every platform response reports the bucket closest to being exhausted:

```http theme={null}
X-RateLimit-Limit: 300
X-RateLimit-Remaining: 297
X-RateLimit-Reset: 1785312000
```

`X-RateLimit-Reset` is Unix time in seconds. All three are listed in
`Access-Control-Expose-Headers`, so browser clients can read them on
cross-origin requests.

Throttle on `X-RateLimit-Remaining` rather than waiting for a `429`. Because the
reported bucket is whichever is closest to empty, honouring it keeps you inside
both the per-token and per-app ceilings without tracking them separately.

### Platform API

| Endpoint area                             | Limit                                                                                    |
| ----------------------------------------- | ---------------------------------------------------------------------------------------- |
| `POST /api/platform/v1/auth/device/code`  | 10 requests per hour per IP                                                              |
| `POST /api/platform/v1/auth/device/token` | 200 requests per 10 minutes per IP                                                       |
| `POST /api/platform/v1/auth/refresh`      | 20 requests per hour per IP                                                              |
| `POST /api/platform/v1/oauth/token`       | 180 admission attempts per minute per IP, then 120 requests per minute per client and IP |
| `GET /api/platform/v1/calendar`           | 60 requests per minute per app and user                                                  |
| `GET /api/platform/v1/me/history`         | 120 requests per minute per app and user                                                 |
| `GET /api/platform/v1/me/lists`           | 120 requests per minute per app and user                                                 |
| `POST /api/platform/v1/playback/{action}` | 120 requests per 5 minutes per user                                                      |
| `GET /api/platform/v1/playback/events`    | 20 connection attempts per minute per token and 60 per minute per user                   |
| `POST /api/platform/v1/oauth/revoke`      | 60 requests per minute per client and IP                                                 |
| `POST /api/platform/v1/oauth/authorize`   | 30 approvals per minute per user                                                         |
| `GET /api/platform/v1/me/sync/changes`    | 120 requests per minute per app and user                                                 |
| `GET /api/platform/v1/me/sync/snapshot`   | 120 requests per minute per app and user                                                 |
| `POST /api/platform/v1/sync/{resource}`   | 30 requests per minute per operation, app, and user                                      |

### Public API

| Endpoint area                           | Limit                         |
| --------------------------------------- | ----------------------------- |
| `GET /api/public/v1/users/{username}`   | 60 requests per minute per IP |
| `GET /api/public/v1/catalog/search`     | 60 requests per minute per IP |
| `GET /api/public/v1/catalog/trending`   | 60 requests per minute per IP |
| `GET /api/public/v1/catalog/discover`   | 90 requests per minute per IP |
| `GET /api/public/v1/community/overview` | 60 requests per minute per IP |

## Retry behavior

Every `429` includes a `Retry-After` header in seconds. Respect that value when
retrying; a retry that ignores it consumes budget you have not yet earned back.

## Recommended client behavior

* Retry `429` responses after `Retry-After`, and throttle on
  `X-RateLimit-Remaining` so you rarely reach one.
* On `409 request_in_progress`, wait and retry the **same** `Idempotency-Key`.
* On `409 idempotency_indeterminate`, do **not** reuse the key. That write may
  or may not have applied; reconcile through
  `GET /api/platform/v1/me/sync/changes` and retry with a new key.
* Treat `401` bearer-token responses as a signal to refresh or re-authorize.
* Treat `403` as a permission problem for that token.
* Treat `400` validation and scope errors as non-retryable until you fix the request.
* Treat `408` as retryable only when the client can send the complete body promptly.
* Reduce the body before retrying a `413` response.
* Do not retry `invalid_client` until you verify app credentials.
* Replace refresh tokens immediately when a refresh response returns a new one.
* Log `request_id`, endpoint, status code, and raw error body for every failed platform request.

## What to send support

If something breaks and you need investigation, send:

* `request_id`
* UTC timestamp
* endpoint path
* HTTP status code
* `client_id`
* raw error body

PunchPlay persists request logs keyed by `request_id`, so that information is enough to trace the failure quickly.

## Stability note

The platform contract is versioned, but rate limits may evolve as usage patterns become clearer. If your integration depends on sustained high-volume traffic, design for graceful backoff and batching where possible.
