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

# Idempotency

> Use the Idempotency-Key header so a retried write never runs twice: format, which endpoints require it, replays, conflicts and the 24-hour window.

Networks fail. When a request times out you cannot know whether OnlyX received it, and retrying a send blindly could message a fan twice. The `Idempotency-Key` header solves this: send the same key with the same request, and OnlyX performs it **at most once**.

## How to use it

```http theme={"system"}
POST /v1/conversations/cnv_9f2c41d7a8b35e06c1f4/messages
Authorization: Bearer onx_sk_...
Content-Type: application/json
Idempotency-Key: 6f1c2d3e-4b5a-4c7d-8e9f-0a1b2c3d4e5f

{"text": "hey you"}
```

* **Format**: 8 to 64 characters from `A-Z`, `a-z`, `0-9`, `-` and `_`. A UUID is ideal.
* **Accepted** on every `POST`, `PUT`, `PATCH` and `DELETE` under `/v1`.
* **Required** on:

  * `POST /v1/conversations/{conversationId}/messages` (sending a message)
  * `POST /v1/creators` (adding a creator)
  * `POST /v1/creators/{creatorId}/tracking-links` (creating a tracking link)

  Without it these return `400 IDEMPOTENCY_KEY_REQUIRED`.
* **Recommended** on every other write that reaches fans: releasing a chat, turning a chat's AI on, turning a creator's AI on, resolving a hand-off, changing AI settings, replacing the welcome message.

Each write's page in the [API reference](/developers/api-reference/introduction) lists the header and whether it is required.

## What happens on a retry

| You send | OnlyX answers |
| - | - |
| A new key | Performs the request. If it succeeds, the response is stored for 24 hours. If it fails with an error (any `4xx` or `5xx` error response), nothing is stored and the key is released. |
| The same key and the same request, after the first one succeeded | The **stored response** (same status and body), with the header `Idempotent-Replayed: true`. Nothing is performed again. |
| The same key after the first attempt failed with an error | The request runs again, as if the key were new. Retrying a `5xx` or a timeout with the same key is exactly what the key is for: if the first attempt did succeed, you get its stored response instead. |
| The same key and the same request, while the first one is still running | `409 IDEMPOTENCY_IN_PROGRESS`. Wait a moment and retry with the same key. |
| The same key with a different request (different method, path or body) | `409 IDEMPOTENCY_KEY_REUSED`. Nothing is performed. |

"The same request" means the same method, the same path and the same JSON body. Key order and whitespace in the body do not matter.

For sends, the key also travels with the message all the way to delivery, so even a retry that raced the original cannot deliver the message twice.

## Rules of thumb

* **One key per intended action.** Generate the key when you decide to do something (for example when a draft is approved), store it with that action, and use it for every attempt.
* **Never generate a new key for a retry.** A new key means a new action: that is how double sends happen.
* **Keys are unique per workspace**, across all your API keys and connected apps. Use random UUIDs, not counters or timestamps that two integrations could produce at the same time.
* **Replays last 24 hours.** After that, the same key is treated as new. Do not retry a write more than a day later expecting protection.
* **Fix, then use a new key.** If the first attempt failed with `400 VALIDATION_ERROR`, the corrected request is a different action: give it a new key. (Reusing the old key would also work, because a failed attempt releases its key, but a new key keeps your logs honest.)
* **Idempotency is not delivery.** A send that returned `202` and later shows `delivery.status: "unconfirmed"` must not be sent again with a new key. The fan almost certainly has it.

## Example: a safe send with retries

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

  API = "https://api.onlyx.ai/v1"
  HEADERS = {"Authorization": f"Bearer {os.environ['ONLYX_API_KEY']}"}


  def send_once(conversation_id, body, key=None, attempts=5):
      key = key or str(uuid.uuid4())  # store this with your draft before the first attempt
      for attempt in range(attempts):
          try:
              r = requests.post(
                  f"{API}/conversations/{conversation_id}/messages",
                  headers={**HEADERS, "Idempotency-Key": key},
                  json=body,
                  timeout=30,
              )
          except requests.RequestException:
              time.sleep(2 ** attempt)
              continue  # same key: safe
          if r.status_code in (429, 409, 500, 502, 503, 504):
              try:
                  code = r.json().get("error", {}).get("code")
              except ValueError:
                  code = None
              if code == "IDEMPOTENCY_KEY_REUSED":
                  raise RuntimeError("This key was used for a different message.")
              if r.status_code == 409 and code != "IDEMPOTENCY_IN_PROGRESS":
                  r.raise_for_status()  # e.g. SALES_OPTED_OUT: do not retry
              time.sleep(int(r.headers.get("Retry-After", 2 ** attempt)))
              continue
          r.raise_for_status()
          replayed = r.headers.get("Idempotent-Replayed") == "true"
          return r.json(), replayed
      raise RuntimeError(f"No answer after {attempts} attempts; retry later with key {key}")
  ```

  ```javascript JavaScript theme={"system"}
  const API = "https://api.onlyx.ai/v1";
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

  export async function sendOnce(conversationId, body, key = crypto.randomUUID(), attempts = 5) {
    // Store `key` with your draft before the first attempt.
    for (let attempt = 0; attempt < attempts; attempt++) {
      let res;
      try {
        res = await fetch(`${API}/conversations/${conversationId}/messages`, {
          method: "POST",
          headers: {
            Authorization: `Bearer ${process.env.ONLYX_API_KEY}`,
            "Content-Type": "application/json",
            "Idempotency-Key": key,
          },
          body: JSON.stringify(body),
        });
      } catch {
        await sleep(2 ** attempt * 1000);
        continue; // same key: safe
      }
      if ([429, 409, 500, 502, 503, 504].includes(res.status)) {
        const { error = {} } = await res.json().catch(() => ({}));
        if (error.code === "IDEMPOTENCY_KEY_REUSED") throw new Error("This key was used for a different message.");
        if (res.status === 409 && error.code !== "IDEMPOTENCY_IN_PROGRESS") throw new Error(`${error.code}: ${error.message}`);
        await sleep(Number(res.headers.get("Retry-After") ?? 2 ** attempt) * 1000);
        continue;
      }
      if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
      return { message: await res.json(), replayed: res.headers.get("Idempotent-Replayed") === "true" };
    }
    throw new Error(`No answer after ${attempts} attempts; retry later with key ${key}`);
  }
  ```
</CodeGroup>

## Related

* [Read and send messages](/developers/guides/read-and-send-messages): delivery states after a send.
* [Errors](/developers/errors): which errors are safe to retry.
