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

# Fanvue API reference: conventions, errors, and rate limits

> Base URL, authentication, versioning, pagination, errors, rate limits, idempotency and the shared response conventions for v1 of the Fanvue API.

The Fanvue API is a REST API that speaks JSON over HTTPS. On `v1`, the current URL version, you send requests to:

```text theme={null}
https://api.fanvue.com/v1
```

Every `v1` endpoint carries the prefix, so a chat list is `GET https://api.fanvue.com/v1/chats`. The [previous version](/docs/api-reference/overview) drops it and answers on `https://api.fanvue.com/chats` instead. See [API versioning](#api-versioning) below.

This page documents the conventions that apply across every `v1` endpoint: how to authenticate, how to pin an API version, how paginated responses are shaped, what error bodies look like, how rate limiting is signalled, and how idempotency, timestamps and agency scoping work. Use the navigation to browse endpoints by resource, or try requests directly from each endpoint page.

<Tip>
  Prefer the machine-readable spec? The full OpenAPI 3.1 document for `v1` is served at [`/openapi-v1.json`](/docs/openapi-v1.json), ready to feed to a coding agent, client generator, or Postman.
</Tip>

## Authentication

All endpoints require a Bearer access token obtained via the OAuth 2.0 flow. See the [Authentication guide](/docs/authentication/overview) to set up an OAuth application and obtain tokens.

```http theme={null}
Authorization: Bearer <access_token>
```

Missing, expired or insufficiently scoped credentials return `401` or `403`. See [Error responses](#error-responses) below.

## API versioning

Every request must send the `X-Fanvue-API-Version` header. The version is a date string, and the current version is `2025-06-26`.

```http theme={null}
X-Fanvue-API-Version: 2025-06-26
```

<Warning>
  This header is **required** on every endpoint. A request with no version, or a version the server does not recognise, returns `400` with an [`UnsupportedVersionError`](#error-responses). A version that has been retired returns `410` with a `SunsetVersionResponse` body that includes a `nextVersion` field telling you which version to move to.
</Warning>

The `/v1` prefix on a path is a **separate** version axis. It selects the shape of the endpoint: which query parameters it takes and how a list paginates. `/v1` is the current URL version; the unversioned paths are the previous one. Use the version selector in the top bar to move this reference between `v1` and `v0`. See [Moving to v1](/docs/versions/moving-to-v1) for the endpoint-by-endpoint mapping.

<Note>
  ##### Not everything has a `/v1` form

  App Store endpoints (`/apps/*`) exist only on the unversioned paths, so call them without a prefix even from a `v1` integration. The self-scoped `/account-health` and `/account-health/flagged-media` are also unversioned only; their creator-scoped counterparts do have a `v1` form at `/v1/creators/{creatorUserUuid}/account-health`.
</Note>

## Pagination

Every `v1` list endpoint is cursor-based. You pass an opaque `cursor` and a page-size parameter, and the response returns a `nextCursor` to fetch the following page. Every list wraps its results in a top-level `data` array, so you always read items from `data`.

| Query parameter     | Type   | Notes                                                                                                            |
| ------------------- | ------ | ---------------------------------------------------------------------------------------------------------------- |
| `cursor`            | string | Opaque cursor. Omit on the first request. Pass the `nextCursor` from the previous response to get the next page. |
| `limit` *or* `size` | number | Items per page. Range `1-50`, default `15`. See the naming warning below.                                        |

<Warning>
  ##### The page-size parameter has two names

  The same concept is named differently by endpoint family, and the API silently ignores the wrong one and returns the default page size.

  | Parameter | Endpoints                                                                                                            |
  | --------- | -------------------------------------------------------------------------------------------------------------------- |
  | `limit`   | Chat messages and media, notifications, tracking links, checkout-link payments and subscriptions, online subscribers |
  | `size`    | Everything else, including chats, insights and the agency endpoints                                                  |
</Warning>

The response envelope returns `data` plus a `nextCursor`. When `nextCursor` is `null`, you have reached the end of the collection. Some lists also carry a `total`, which is `null` when no count was computed for that list, so treat it as an optional hint rather than something to page against.

<CodeGroup>
  ```bash Request theme={null}
  curl "https://api.fanvue.com/v1/insights/earnings?size=2" \
    -H "Authorization: Bearer <access_token>" \
    -H "X-Fanvue-API-Version: 2025-06-26"
  ```

  ```json Response theme={null}
  {
    "data": [
      {
        "date": "2025-05-29T14:02:10Z",
        "gross": 1999,
        "net": 1599,
        "currency": "USD",
        "source": "subscription"
      },
      {
        "date": "2025-05-29T09:41:55Z",
        "gross": 500,
        "net": 400,
        "currency": "USD",
        "source": "tip"
      }
    ],
    "nextCursor": "eyJpZCI6IjEyMyJ9"
  }
  ```
</CodeGroup>

To page through the full set, repeat the request with `cursor` set to the previous `nextCursor` until `nextCursor` is `null`:

```bash theme={null}
curl "https://api.fanvue.com/v1/insights/earnings?size=2&cursor=eyJpZCI6IjEyMyJ9" \
  -H "Authorization: Bearer <access_token>" \
  -H "X-Fanvue-API-Version: 2025-06-26"
```

<Note>
  Page-based pagination, with `page` and `hasMore`, belongs to the previous version. No `v1` endpoint accepts a `page` parameter. See [Moving to v1](/docs/versions/moving-to-v1) for how to convert a paging loop.
</Note>

## Error responses

Errors use standard HTTP status codes. The response body is JSON, but its exact shape depends on the kind of error. There is no single global error envelope: validation errors return an `errors` array, while most other errors return a single `error` or `message` string. For the recovery action to take on each error, and which are safe to retry, see [Errors](/docs/api-reference/errors).

| Status        | Meaning                                                                                                             | Body shape                                                                                                        |
| ------------- | ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `400`         | Bad request. Either the API version is not supported, or request validation failed.                                 | `oneOf` an `UnsupportedVersionError` (`{ "error", "message" }`) or a `ValidationError` (`{ "errors": ["..."] }`). |
| `401`         | Unauthorized. Missing, invalid or expired access token.                                                             | `{ "error": "..." }`                                                                                              |
| `403`         | Forbidden. The token is valid but lacks the scope or permission for this resource.                                  | `{ "error": "..." }`                                                                                              |
| `404`         | Not found. The resource (or a referenced UUID) does not exist.                                                      | `{ "message": "..." }`                                                                                            |
| `409`         | Conflict. A uniquely named resource already exists (for example a list, collection or vault folder with that name). | `{ "message": "..." }`                                                                                            |
| `410`         | Gone. The requested API version has been sunset.                                                                    | `{ "error", "message", "nextVersion" }`                                                                           |
| `429`         | Too many requests. The rate limit was exceeded. See [Rate limit headers](#rate-limit-headers).                      | `{ "error": "..." }`                                                                                              |
| `502` / `503` | Upstream or service unavailable. Transient; retry with backoff.                                                     | No defined body; treat as transient and retry.                                                                    |

Some endpoints define additional, more specific `400` variants documented on the endpoint page itself:

<Accordion title="Endpoint-specific 400 variants">
  * **`ValidationError`** (`{ "errors": ["..."] }`): one or more request fields failed validation.
  * **`InvalidUuidError`** (`{ "message": "..." }`): a UUID path or body parameter is not a valid UUID.
  * **`ContactabilityError`** (`{ "message": "..." }`): the target user cannot be contacted (for example messaging restrictions).
  * **`MessageValidationError`** (`{ "message": "..." }`): message-specific validation failed, such as media ownership or content checks.
</Accordion>

<Note>
  Batch endpoints return `200` even when some items fail. Inside the `200` body, individual keys that could not be resolved carry a `PerKeyError` with `error` set to `forbidden`, `not_found` or `internal`, so a batch keeps its partial results instead of failing the whole request.
</Note>

## Rate limit headers

By default each user can make **200 requests per 60 seconds**. See [Rate Limits](/docs/authentication/rate-limits) for the full policy.

<Warning>
  ##### Don't poll for new activity, subscribe

  Polling list endpoints for new messages or sales is the main cause of `429`s. Subscribe to [webhooks](/docs/webhooks/index) for real-time events, and use [Efficient Chat Sync](/docs/tutorials/efficient-chat-sync) to catch up after downtime.
</Warning>

Rate-limited responses (`429`) include the following headers, defined in the spec's `RateLimitResponse`:

| Header                  | Description                                                        |
| ----------------------- | ------------------------------------------------------------------ |
| `X-RateLimit-Limit`     | The maximum number of requests allowed in the current window.      |
| `X-RateLimit-Remaining` | The number of requests remaining in the current window.            |
| `X-RateLimit-Reset`     | The Unix timestamp, in seconds, when the rate limit window resets. |
| `Retry-After`           | The number of seconds to wait before retrying the request.         |

When you receive a `429`, wait `Retry-After` seconds (or until `X-RateLimit-Reset`) before sending the next request.

## Idempotency

The grant endpoint, `POST /v1/media/{uuid}/grant`, is idempotent by design. It grants a consumer access to a media item, and repeated calls with the same parameters return the existing entitlement rather than creating a duplicate.

Idempotency is keyed on the `source` and `sourceRef` fields you supply in the request body:

| Field        | Type          | Purpose                                                                                                                                                         |
| ------------ | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `consumerId` | string (uuid) | The consumer to grant access to.                                                                                                                                |
| `source`     | string        | Identifier for the granting application or reason. Lowercase, digits and underscores only (`^[a-z0-9_]+$`), max 100 chars. For example `spin_the_wheel_reward`. |
| `sourceRef`  | string        | A unique identifier within the `source`, used for the idempotency key. Max 255 chars. For example a spin-attempt UUID.                                          |

The same `source` + `sourceRef` pair always resolves to the same entitlement, so you can safely retry a grant after a network failure without double-granting.

<CodeGroup>
  ```bash Request theme={null}
  curl -X POST https://api.fanvue.com/v1/media/3f1c.../grant \
    -H "Authorization: Bearer <access_token>" \
    -H "X-Fanvue-API-Version: 2025-06-26" \
    -H "Content-Type: application/json" \
    -d '{
      "consumerId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "source": "spin_the_wheel_reward",
      "sourceRef": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
    }'
  ```

  ```json Response theme={null}
  {
    "entitlementId": "9c8b7a65-4321-fedc-ba98-76543210abcd",
    "status": "granted"
  }
  ```
</CodeGroup>

The response returns the same `entitlementId` and `status: "granted"` whether the entitlement was just created or already existed, so a retry is indistinguishable from the first successful call. Granting media requires the `write:media` scope.

## Money figures

All amounts are integers in minor units (USD cents unless a field says otherwise). Two conventions sit behind the words `gross` and `net`, and insights endpoints do not treat refunds and chargebacks the same way as each other:

* `gross` is what the fan paid, `net` is the creator's cut after platform fees.
* Whether a refunded or charged-back purchase is still counted is decided **per field**, not per endpoint. On `GET /v1/insights/fans/{userUuid}` for example, `spending.total` is net of reversals while `spending.sources` are gross of them, so the sources do not sum to the total.

A reversal is always written as its own invoice for the full original amount, never as an edit to the payment it reverses. On `GET /v1/insights/earnings`, `reversedTransactionOrderId` pairs the reversal with the original.

See [Insights Metrics](/docs/core-concepts/insights-metrics) for the payment-source table, the field-by-field gross and net reference, and how often each figure is recomputed.

## Timestamps

All timestamps are UTC and formatted as ISO 8601 datetime strings. Date-range query parameters (such as `startDate` and `endDate` on insights endpoints) accept an ISO 8601 datetime, with or without a timezone offset, and stats are aggregated by UTC day.

```text theme={null}
2025-05-29T14:02:10Z
2024-10-20T00:00:00+01:00
```

<Note>
  Some response fields use a date-only format (`date`) and others a full datetime (`date-time`); each field documents its own format on the endpoint page. Regardless of format, the underlying instant is UTC.
</Note>

## Agency creator scoping

Agency-scoped list endpoints under `/v1/agencies/*` cover every creator an agency manages in a single response. So that consumers can group rows by creator without a second lookup, every row is tagged with the creator it belongs to via a `creatorUuid` field.

This applies across the agency endpoints, for example:

* `GET /v1/agencies/earnings`: each earnings row carries `creatorUuid` (the agency-managed creator the earnings row belongs to).
* `GET /v1/agencies/subscribers`: each subscriber carries `creatorUuid` (the creator they are subscribed to).
* `GET /v1/agencies/chats`: each chat carries `creatorUuid` (the creator that owns the chat).
* `GET /v1/agencies/subscribers-history`: each history row carries `creatorUuid`.

To narrow results to a subset of managed creators, pass the `creatorUuids` query parameter, a comma-separated list of creator UUIDs (max 50):

```bash theme={null}
curl "https://api.fanvue.com/v1/agencies/earnings?creatorUuids=<uuid1>,<uuid2>" \
  -H "Authorization: Bearer <access_token>" \
  -H "X-Fanvue-API-Version: 2025-06-26"
```

Because each row already includes `creatorUuid`, consumers can group or attribute results client-side without joining against a separate creators list.
