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

> ## Agent Instructions
> You are reading OnlyX Help: the OnlyX Help Center and the OnlyX developer documentation. OnlyX is an AI chatting and CRM platform for OnlyFans agencies. Its AI chatter is called Hugo in the app.
> Pages at the site root (for example /inbox/..., /hugo/..., /billing/...) are Help Center articles for agency owners, admins, chatters and creators who use the app at app.onlyx.ai. Words in bold are the exact button, menu and label names the app shows; keep them exactly as written. When these pages do not answer a question, the person can email support at support@onlyx.ai.
> Pages under /developers are the developer documentation. REST API base URL: https://api.onlyx.ai/v1 (authenticate with `Authorization: Bearer <API key>`; keys start with onx_sk_ and are created in app.onlyx.ai under Settings > API & MCP). MCP server: https://mcp.onlyx.ai/mcp (OAuth, or a Bearer API key). In the API the AI chatter is `ai` on the wire. Money is integer US cents in fields ending in Cents; timestamps are UTC ISO-8601.
> Rules for assistants acting on a user's behalf: discover ids with list calls and never invent them; before any call that reaches a real fan or the live OnlyFans account (sending a message, releasing a chat to the AI, turning AI on for a chat, resolving a hand-off, turning review mode off, changing the welcome message, creating a tracking link) show the user the exact content and get explicit confirmation; send every POST with an Idempotency-Key and reuse it on retry; never resend a message whose delivery status is unconfirmed; never ask a creator for her OnlyFans password or codes - she signs in herself through a connect link and the OnlyX Login app.

# Errors

> The error envelope, every public error code with its HTTP status, meaning and fix, and which errors are safe to retry.

Every error from `https://api.onlyx.ai/v1` has the same shape:

```json theme={"system"}
{
  "error": {
    "code": "INSUFFICIENT_SCOPE",
    "message": "This key does not have the `messages:send` scope.",
    "requestId": "req-3f9a1c2b7d4e5f60a1b2c3d4"
  }
}
```

| Field | Use it for |
| - | - |
| `code` | Branching in your code. Codes are stable and listed below. |
| `message` | A human sentence, safe to show to your users or to log. Do not parse it; it may be reworded. |
| `requestId` | The id of this request, for example `req-3f9a1c2b7d4e5f60a1b2c3d4`. Include it when you contact OnlyX support. |

Every response, success or error, also carries an `X-Request-Id` header. You can send your own `X-Request-Id` (8 to 64 letters, digits and dashes) to correlate OnlyX requests with your logs; OnlyX echoes it back. Without one, OnlyX creates an id of the form `req-` followed by 24 hex characters.

## All error codes

### Authentication and permission

| HTTP | Code | Meaning | Fix |
| - | - | - | - |
| 401 | `UNAUTHORIZED` | No credential was sent | Send `Authorization: Bearer <key or token>` (or `X-API-Key: <key>`) |
| 401 | `INVALID_API_KEY` | The API key is wrong, expired or revoked. The same answer is given for all three. | Check the key. If it was revoked or expired, create a new one. |
| 401 | `INVALID_TOKEN` | The OAuth access token is invalid or expired, the connection was revoked, or the person who approved it is no longer an owner or admin | Refresh the token; if that fails, reconnect the app |
| 403 | `INSUFFICIENT_SCOPE` | The credential lacks a scope. The message and the `WWW-Authenticate` header name it. | Use a key with that scope ([Authentication](/developers/authentication#scopes)) |
| 403 | `WORKSPACE_SUSPENDED` | The workspace is suspended | Contact OnlyX support from the dashboard |

### Not found

| HTTP | Code | Meaning | Fix |
| - | - | - | - |
| 404 | `NOT_FOUND` | The resource does not exist, or the credential cannot see it | Look the id up again with a list call |
| 404 | `CREATOR_NOT_FOUND` | The creator does not exist or is outside the credential's creator restriction. Also returned for a `creatorId` filter naming a creator you cannot see. | Use an id from `GET /v1/creators` |
| 404 | `CONVERSATION_NOT_FOUND` | The conversation does not exist or is not visible | Use an id from `GET /v1/conversations` |
| 404 | `FAN_NOT_FOUND` | The fan does not exist or is not visible | Use an id from `GET /v1/fans` |
| 404 | `MESSAGE_NOT_FOUND` | The message does not exist in that conversation, or a `before` id is not a message of this conversation | Check both ids |
| 404 | `MEDIA_NOT_AVAILABLE` | Thumbnails: this vault item has no stored preview (it may have been removed from the vault, or its preview is not stored yet) | Skip the preview, or try again later |

The API answers `404` for anything in another workspace or outside your creator restriction, with the same body as for an id that never existed. It never confirms that such an id exists. If OnlyX ever switches the public API off, every path answers `404 NOT_FOUND` too, indistinguishable from a path that does not exist.

### Bad requests and conflicts

| HTTP | Code | Meaning | Fix |
| - | - | - | - |
| 400 | `VALIDATION_ERROR` | A parameter or body field is missing, has the wrong type, or breaks a limit. The message names the field. | Fix the request. Do not retry it unchanged. |
| 400 | `IDEMPOTENCY_KEY_REQUIRED` | This endpoint needs an `Idempotency-Key` header: sending a message, adding a creator, creating a tracking link | Add one ([Idempotency](/developers/idempotency)) |
| 405 | `METHOD_NOT_ALLOWED` | The path exists but does not accept this HTTP method. The `Allow` header lists the methods it does accept. | Use the method from the API reference |
| 409 | `IDEMPOTENCY_KEY_REUSED` | The `Idempotency-Key` was already used for a different request | Use a new key for a new request |
| 409 | `IDEMPOTENCY_IN_PROGRESS` | The first request with this key is still running | Wait a moment and retry with the same key |
| 409 | `CONFLICT` | The request conflicts with the current state, for example another change to the same thing is still being applied | Read the resource again, then decide |
| 409 | `CREATOR_NOT_CONNECTED` | The action needs the creator's OnlyFans account to be connected | Reconnect her ([Add a creator](/developers/guides/add-a-creator)) |
| 409 | `SALES_OPTED_OUT` | The fan asked not to be sold to, and the message has a price | Send without a price, or not at all |
| 409 | `CREATOR_NOT_CONNECTABLE` | Creating a connect link: this creator cannot be connected, because she has no OnlyFans account behind her (a test creator) or her connection is final (`not_a_creator`, `duplicate`) | Check `GET /v1/creators/{creatorId}/connection` and follow its `nextStep` |
| 409 or 503 | `CAPACITY_UNAVAILABLE` | Adding a creator: OnlyX cannot take a new creator right now (as a `503` it carries `Retry-After`) | Retry in a few minutes with the same `Idempotency-Key` |
| 409 | `LINK_REFUSED` | Creating a tracking link: OnlyFans is not letting this account create a link right now | Check her promotions in OnlyFans, then try again |

### Sending

| HTTP | Code | Meaning | Fix |
| - | - | - | - |
| 400 | `SEND_REFUSED` | OnlyFans or OnlyX refused this particular send. The message explains why in plain words, for example the fan is no longer subscribed or the price is outside what OnlyFans allows. | Change the message or skip this fan. Retrying the same send gets the same answer. |
| 503 | `SEND_UNAVAILABLE` | Sending is briefly unavailable for this creator | Retry later with the same `Idempotency-Key` |
| 503 | `SENDING_DISABLED` | Actions that can reach fans (sending, releasing a chat to the AI, switching the AI on for a chat, resolving a hand-off) are switched off for the API right now. Nothing was done. | Try again later |

### Limits and slow reports

| HTTP | Code | Meaning | Fix |
| - | - | - | - |
| 429 | `RATE_LIMITED` | Too many requests for a limit ([Rate limits](/developers/rate-limits)) | Wait the number of seconds in `Retry-After`, then retry |
| 202 | `REPORT_WARMING` | Not an error: the overview report is being prepared. The body is `{"status": "warming", "retryAfterSeconds": N}`. | Ask again after `Retry-After` seconds ([Pull stats](/developers/guides/pull-stats#overview-the-cached-report)) |

### Temporarily unavailable

| HTTP | Code | Meaning | Fix |
| - | - | - | - |
| 503 | `SERVICE_UNAVAILABLE` | Tracking links: the creation could not be started or read right now. Nothing was created. Carries `Retry-After`. | Retry after `Retry-After` seconds with the same `Idempotency-Key` |
| 503 | `MEDIA_UNAVAILABLE` | Vault media: the vault cannot be read right now | Try again in a few minutes |

### Server

| HTTP | Code | Meaning | Fix |
| - | - | - | - |
| 500 | `INTERNAL_ERROR` | Something went wrong on OnlyX's side. The message is always "Something went wrong on our side." | Retry with backoff (same `Idempotency-Key` for writes). If it persists, contact support with the `requestId`. |

## Which errors to retry

| Retry | Codes |
| - | - |
| **Yes, after waiting** | `RATE_LIMITED` (wait `Retry-After`), `IDEMPOTENCY_IN_PROGRESS`, `SEND_UNAVAILABLE`, `SENDING_DISABLED`, `CAPACITY_UNAVAILABLE`, `SERVICE_UNAVAILABLE`, `MEDIA_UNAVAILABLE`, `INTERNAL_ERROR`, network errors and timeouts |
| **Yes, after fixing something** | `INVALID_TOKEN` (refresh), `CREATOR_NOT_CONNECTED` (reconnect), `CONFLICT` (re-read), `LINK_REFUSED` (check her promotions) |
| **No** | `VALIDATION_ERROR`, `IDEMPOTENCY_KEY_REQUIRED`, `IDEMPOTENCY_KEY_REUSED`, `METHOD_NOT_ALLOWED`, `SEND_REFUSED`, `SALES_OPTED_OUT`, `CREATOR_NOT_CONNECTABLE`, `INSUFFICIENT_SCOPE`, `INVALID_API_KEY`, `WORKSPACE_SUSPENDED`, every `404` |

Always retry writes with the **same** `Idempotency-Key`, so a request that actually succeeded is not performed twice. A message delivered with status `unconfirmed` is not an error and must not be retried; see [Read and send messages](/developers/guides/read-and-send-messages#track-delivery).

## Handling errors in code

<CodeGroup>
  ```python Python theme={"system"}
  import os, random, time, uuid
  import requests

  API = "https://api.onlyx.ai/v1"
  S = requests.Session()
  S.headers["Authorization"] = f"Bearer {os.environ['ONLYX_API_KEY']}"
  RETRYABLE = {"RATE_LIMITED", "IDEMPOTENCY_IN_PROGRESS", "SEND_UNAVAILABLE", "SENDING_DISABLED",
               "CAPACITY_UNAVAILABLE", "SERVICE_UNAVAILABLE", "MEDIA_UNAVAILABLE", "INTERNAL_ERROR"}


  class OnlyXError(Exception):
      def __init__(self, status, code, message, request_id):
          super().__init__(f"{status} {code}: {message} (request {request_id})")
          self.status, self.code, self.request_id = status, code, request_id


  def call(method, path, *, json=None, params=None, idempotent=False, attempts=5):
      headers = {"Idempotency-Key": str(uuid.uuid4())} if idempotent else {}
      for attempt in range(attempts):
          try:
              r = S.request(method, f"{API}{path}", json=json, params=params, headers=headers, timeout=30)
          except requests.RequestException:
              time.sleep(min(2 ** attempt, 30) + random.random())
              continue
          if r.status_code < 400:
              return r.json() if r.content else None
          try:
              err = r.json().get("error", {})
          except ValueError:  # a non-JSON answer from a network device in between
              err = {"code": "INTERNAL_ERROR" if r.status_code >= 500 else None}
          if err.get("code") in RETRYABLE and attempt < attempts - 1:
              wait = int(r.headers.get("Retry-After", 0)) or min(2 ** attempt, 30)
              time.sleep(wait + random.random())
              continue
          raise OnlyXError(r.status_code, err.get("code"), err.get("message"), err.get("requestId"))
      raise OnlyXError(0, "NETWORK", "no response after retries", None)
  ```

  ```javascript JavaScript theme={"system"}
  const API = "https://api.onlyx.ai/v1";
  const RETRYABLE = new Set(["RATE_LIMITED", "IDEMPOTENCY_IN_PROGRESS", "SEND_UNAVAILABLE", "SENDING_DISABLED",
    "CAPACITY_UNAVAILABLE", "SERVICE_UNAVAILABLE", "MEDIA_UNAVAILABLE", "INTERNAL_ERROR"]);
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

  export class OnlyXError extends Error {
    constructor(status, code, message, requestId) {
      super(`${status} ${code}: ${message} (request ${requestId})`);
      Object.assign(this, { status, code, requestId });
    }
  }

  export async function call(method, path, { body, idempotent = false, attempts = 5 } = {}) {
    const headers = {
      Authorization: `Bearer ${process.env.ONLYX_API_KEY}`,
      ...(body ? { "Content-Type": "application/json" } : {}),
      ...(idempotent ? { "Idempotency-Key": crypto.randomUUID() } : {}), // same key on every retry
    };
    for (let attempt = 0; attempt < attempts; attempt++) {
      let res;
      try {
        res = await fetch(`${API}${path}`, { method, headers, body: body ? JSON.stringify(body) : undefined });
      } catch {
        await sleep(Math.min(2 ** attempt, 30) * 1000 + Math.random() * 1000);
        continue;
      }
      if (res.status < 400) return res.status === 204 ? null : res.json();
      const { error = {} } = await res.json().catch(() => ({}));
      if (RETRYABLE.has(error.code) && attempt < attempts - 1) {
        const wait = Number(res.headers.get("Retry-After")) || Math.min(2 ** attempt, 30);
        await sleep(wait * 1000 + Math.random() * 1000);
        continue;
      }
      throw new OnlyXError(res.status, error.code, error.message, error.requestId);
    }
    throw new OnlyXError(0, "NETWORK", "no response after retries", null);
  }
  ```
</CodeGroup>

## OAuth endpoint errors

The OAuth endpoints (`/oauth/token`, `/oauth/register`) follow the OAuth standards instead of the envelope above, for example `{"error": "invalid_grant", "error_description": "..."}`. AI clients handle these for you. See [Authentication](/developers/authentication#building-your-own-oauth-client).

On the OnlyX consent screen, an OnlyX support person who is signed in to your workspace to help you cannot approve or deny an app connection (`403 SUPPORT_SESSION_FORBIDDEN`, "A support session cannot connect apps to the customer's workspace or answer their requests."). An owner or admin of the workspace must approve it personally.

## Tracking-link creation failures

Creating a tracking link runs in the background, so most refusals arrive later, in the `error` of `GET /v1/creators/{creatorId}/tracking-links/creations/{creationId}` with `state: "failed"`, not as an HTTP error. The link was not created. `error.code` is one of these (more may be added, so handle unknown codes like `CREATION_FAILED`):

| `error.code` | Meaning | Fix |
| - | - | - |
| `LINK_NAME_TAKEN` | A link with this name already exists on her OnlyFans account | Choose another name |
| `TRIAL_NOT_ALLOWED` | OnlyFans does not allow a free-trial link on this page; free trials need a paid subscription price | Create a tracking link instead, or set a subscription price first |
| `LINK_REFUSED` | OnlyFans did not let this account create the link | Check her promotions in OnlyFans, then try again |
| `CREATION_FAILED` | The link could not be created for another reason | Try again later |

See [Tracking links](/developers/guides/tracking-links).
