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

# Build the AI content ladder

> Step by step with real calls: find vault media, create a folder and a ladder, add priced and free levels, attach preview and paid media, reorder, verify and switch it on.

This guide builds a complete sequential ladder for one creator: a free opener, two priced levels and a free thank-you, each with the right media. Read [AI content](/developers/concepts/ai-content) first for what ladders, levels, floors and preview media mean.

**Scopes**: `media:read` (to find media), `ai:read` and `ai:write`. Nothing on this page sends anything to fans: the AI uses the ladder in its own conversations once it is active.

All endpoints live under `/v1/creators/{creatorId}/ai-content`, and **every write returns the whole updated board**, so you always see the result of your change: `201` for the three creates (folder, ladder, level), `200` for everything else. Content writes are limited to 60 per minute per credential (`429 RATE_LIMITED` with `Retry-After` beyond that).

| Action | Method and path |
| - | - |
| Read the board | `GET /ai-content` |
| Create, rename, delete a folder | `POST /ai-content/folders`, `PATCH` or `DELETE /ai-content/folders/{folderId}` |
| Create, update, delete a ladder | `POST /ai-content/collections`, `PATCH` or `DELETE /ai-content/collections/{collectionId}` |
| Reorder a ladder's levels | `PUT /ai-content/collections/{collectionId}/order` |
| Create, update, delete a level | `POST /ai-content/levels`, `PATCH` or `DELETE /ai-content/levels/{levelId}` |
| Add media to a level | `POST /ai-content/levels/{levelId}/media` |
| Remove one item | `DELETE /ai-content/levels/{levelId}/media/{mediaId}` |
| Reorder a level's media | `PUT /ai-content/levels/{levelId}/media/order` |

## Step 1: Pick the media

List the creator's vault and choose ids (see [Vault media](/developers/guides/vault-media)). For this ladder we use:

| Level | Preview (free) | Paid (locked) |
| - | - | - |
| 1. Opener (free) | `4012345601` | none |
| 2. Lace set, at least \$12 | `4012345610` | `4012345611` to `4012345618` (8 photos) |
| 3. Shower video, at least \$25 | `4012345620` | `4012345621` (a video) |
| 4. Thank-you (free aftercare) | `4012345630` | none |

```bash cURL theme={"system"}
curl "https://api.onlyx.ai/v1/creators/cre_5f2d9a1c7b3e4f60a2d1/media?type=photo&limit=100" \
  -H "Authorization: Bearer $ONLYX_API_KEY"
```

## Step 2: Create a folder (optional)

Folders only organize the board for people. Skip this step if you do not need them.

```bash cURL theme={"system"}
curl -X POST https://api.onlyx.ai/v1/creators/cre_5f2d9a1c7b3e4f60a2d1/ai-content/folders \
  -H "Authorization: Bearer $ONLYX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name": "Main"}'
```

Find the new folder's id (`fld_...`) in `folders` of the returned board.

## Step 3: Create the ladder, switched off

Create it with `"active": false` so the AI does not offer a half-built ladder. You switch it on in step 8.

| Field | Rules |
| - | - |
| `name` | Required, 1 to 120 characters. What your team calls it. |
| `folderId` | Optional. Must be a folder of the same creator (`404` otherwise). |
| `mode` | `sequential` (default) or `standard` |
| `description` | Up to 2,000 characters: the theme, in words the AI can use |
| `active` | `true` (default) or `false`. Send `false` here. |

```bash cURL theme={"system"}
curl -X POST https://api.onlyx.ai/v1/creators/cre_5f2d9a1c7b3e4f60a2d1/ai-content/collections \
  -H "Authorization: Bearer $ONLYX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "name": "Bedroom tease",
        "folderId": "fld_2a4c6e8f0b1d3f5a7c9e",
        "mode": "sequential",
        "description": "A lazy morning in bed: cozy, then red lace, then the shower.",
        "active": false
      }'
```

## Step 4: Create the levels

Each level needs `collectionId` and **`priceCents`** (required: `0` makes a free level). Every other field is applied on create too.

| Field | Rules |
| - | - |
| `collectionId` | Required. The ladder. |
| `priceCents` | Required. `0` = free level; otherwise the floor price in cents. |
| `name` | What your team calls it, up to 120 characters |
| `tags` | Up to 30 tags, 40 characters each, matched against what fans talk about |
| `highlight` | Up to 80 characters: a hook line the AI can lead with |
| `description` | Up to 2,000 characters: what is in it |
| `availableInPaidMessages` | `true` (default) lets the AI sell it in chats |
| `active` | `true` (default) or `false` |

Levels are added at the end of the ladder, so create them in climbing order:

```bash cURL theme={"system"}
LADDER=col_8e1f3a5c7d9b2e4f6a0c
BASE=https://api.onlyx.ai/v1/creators/cre_5f2d9a1c7b3e4f60a2d1/ai-content

curl -X POST $BASE/levels -H "Authorization: Bearer $ONLYX_API_KEY" -H "Content-Type: application/json" \
  -d '{"collectionId": "'$LADDER'", "name": "Opener", "priceCents": 0, "tags": ["morning", "cozy"], "highlight": "Just woke up thinking about you"}'

curl -X POST $BASE/levels -H "Authorization: Bearer $ONLYX_API_KEY" -H "Content-Type: application/json" \
  -d '{"collectionId": "'$LADDER'", "name": "Lace set", "priceCents": 1200, "tags": ["lingerie", "lace", "red", "bedroom"],
       "highlight": "The red lace set you asked about", "description": "Eight photos in the red lace set on her bed, morning light, face visible."}'

curl -X POST $BASE/levels -H "Authorization: Bearer $ONLYX_API_KEY" -H "Content-Type: application/json" \
  -d '{"collectionId": "'$LADDER'", "name": "Shower video", "priceCents": 2500, "tags": ["shower", "video", "wet"],
       "highlight": "Three minutes in the shower", "description": "A three-minute shower video, playful and slow."}'

curl -X POST $BASE/levels -H "Authorization: Bearer $ONLYX_API_KEY" -H "Content-Type: application/json" \
  -d '{"collectionId": "'$LADDER'", "name": "Thank-you", "priceCents": 0, "tags": ["thank you"], "highlight": "A little thank-you for you"}'
```

<Warning>
  **A level with `priceCents: 0` is free: its media is given away to every fan who reaches it.** That is right for an opener or a thank-you, and expensive for a real set created with a missing price. After creating levels, check `priceCents` and `free` on each one in the returned board.
</Warning>

## Step 5: Add media

`POST /ai-content/levels/{levelId}/media` with `mediaIds` (1 to 200 vault media ids, as strings) and a `role`:

* `preview`: sent free with the offer, to tease.
* `paid`: locked behind the price.

On a free level everything is stored as preview, whatever you send. An item can be on only one level: adding it here **moves** it from any other level.

```bash cURL theme={"system"}
# Lace set: one free teaser, eight paid photos
curl -X POST $BASE/levels/set_4b6d8f0a2c4e6a8c0e2a/media \
  -H "Authorization: Bearer $ONLYX_API_KEY" -H "Content-Type: application/json" \
  -d '{"mediaIds": ["4012345610"], "role": "preview"}'

curl -X POST $BASE/levels/set_4b6d8f0a2c4e6a8c0e2a/media \
  -H "Authorization: Bearer $ONLYX_API_KEY" -H "Content-Type: application/json" \
  -d '{"mediaIds": ["4012345611","4012345612","4012345613","4012345614","4012345615","4012345616","4012345617","4012345618"], "role": "paid"}'
```

To take one item off a level: `DELETE /ai-content/levels/{levelId}/media/{mediaId}` (`404` if it is not on that level).

## Step 6: Order the media

The preview order is the order fans see the teasers; the paid order is the unlock order. `PUT /ai-content/levels/{levelId}/media/order` with the `role` and the ids in the order you want. Ids you leave out follow in their current order; an id that is not on that side of the level is a `400`:

```bash cURL theme={"system"}
curl -X PUT $BASE/levels/set_4b6d8f0a2c4e6a8c0e2a/media/order \
  -H "Authorization: Bearer $ONLYX_API_KEY" -H "Content-Type: application/json" \
  -d '{"role": "paid", "mediaIds": ["4012345615","4012345611","4012345612","4012345613","4012345614","4012345616","4012345617","4012345618"]}'
```

## Step 7: Order the levels

In a sequential ladder this is the climb every fan follows. `PUT /ai-content/collections/{collectionId}/order` with the level ids in order. Levels you list come first; any you leave out keep their order after them, and an id that is not a level of this ladder is a `400`:

```bash cURL theme={"system"}
curl -X PUT $BASE/collections/col_8e1f3a5c7d9b2e4f6a0c/order \
  -H "Authorization: Bearer $ONLYX_API_KEY" -H "Content-Type: application/json" \
  -d '{"levelIds": ["set_1a3c5e7f9b1d3f5a7c9e", "set_4b6d8f0a2c4e6a8c0e2a", "set_7e9a1c3e5b7d9f1a3c5e", "set_0c2e4a6c8e0b2d4f6a8c"]}'
```

## Step 8: Verify, then switch it on

Read the board and check, for this ladder:

* every priced level (`free: false`) has at least one item in `paid`, or the AI cannot sell it;
* every free level has at least one item;
* `priceCents` is what you meant on every level, and the free ones are only your opener and thank-you;
* `position` follows the climb you want.

Then activate the ladder:

```bash cURL theme={"system"}
curl -X PATCH $BASE/collections/col_8e1f3a5c7d9b2e4f6a0c \
  -H "Authorization: Bearer $ONLYX_API_KEY" -H "Content-Type: application/json" \
  -d '{"active": true}'
```

Finally, `GET /v1/creators/{creatorId}` should show the readiness check `content` as `ok: true`.

## The whole build as one script

This script does steps 2 to 8. It sends each create with an `Idempotency-Key`, so a retry never creates a duplicate, and it finds each new id by comparing the board before and after.

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

  API = "https://api.onlyx.ai/v1"
  S = requests.Session()
  S.headers["Authorization"] = f"Bearer {os.environ['ONLYX_API_KEY']}"
  BASE = f"{API}/creators/cre_5f2d9a1c7b3e4f60a2d1/ai-content"


  def write(method, path, body):
      headers = {"Idempotency-Key": str(uuid.uuid4())} if method == "POST" else {}
      r = S.request(method, f"{BASE}{path}", json=body, headers=headers, timeout=30)
      if not r.ok:
          raise SystemExit(f"{method} {path}: {r.json()['error']}")
      return r.json()  # the whole board


  def created(before, after, key):
      old = {x["id"] for x in before[key]}
      return next(x["id"] for x in after[key] if x["id"] not in old)


  board = S.get(BASE, timeout=30).json()

  after = write("POST", "/folders", {"name": "Main"})
  folder_id, board = created(board, after, "folders"), after

  after = write("POST", "/collections", {
      "name": "Bedroom tease", "folderId": folder_id, "mode": "sequential",
      "description": "A lazy morning in bed: cozy, then red lace, then the shower.", "active": False,
  })
  ladder_id, board = created(board, after, "collections"), after

  LEVELS = [
      ({"name": "Opener", "priceCents": 0, "tags": ["morning", "cozy"],
        "highlight": "Just woke up thinking about you"}, ["4012345601"], []),
      ({"name": "Lace set", "priceCents": 1200, "tags": ["lingerie", "lace", "red", "bedroom"],
        "highlight": "The red lace set you asked about",
        "description": "Eight photos in the red lace set on her bed, morning light, face visible."},
       ["4012345610"], [f"40123456{n}" for n in range(11, 19)]),
      ({"name": "Shower video", "priceCents": 2500, "tags": ["shower", "video"],
        "highlight": "Three minutes in the shower",
        "description": "A three-minute shower video, playful and slow."}, ["4012345620"], ["4012345621"]),
      ({"name": "Thank-you", "priceCents": 0, "tags": ["thank you"],
        "highlight": "A little thank-you for you"}, ["4012345630"], []),
  ]

  level_ids = []
  for fields, preview, paid in LEVELS:
      after = write("POST", "/levels", {"collectionId": ladder_id, **fields})
      level_id, board = created(board, after, "levels"), after
      level_ids.append(level_id)
      if preview:
          board = write("POST", f"/levels/{level_id}/media", {"mediaIds": preview, "role": "preview"})
      if paid:
          board = write("POST", f"/levels/{level_id}/media", {"mediaIds": paid, "role": "paid"})

  board = write("PUT", f"/collections/{ladder_id}/order", {"levelIds": level_ids})

  # Verify before switching on
  for lvl in (l for l in board["levels"] if l["collectionId"] == ladder_id):
      assert lvl["free"] == (lvl["priceCents"] == 0)
      assert lvl["paid"] or lvl["free"], f"{lvl['name']} has no paid media"
      assert lvl["preview"] or lvl["paid"], f"{lvl['name']} is empty"
      print(lvl["position"], lvl["name"], lvl["priceCents"], len(lvl["preview"]), len(lvl["paid"]))

  board = write("PATCH", f"/collections/{ladder_id}", {"active": True})
  print("Ladder is live:", ladder_id)
  ```

  ```javascript JavaScript theme={"system"}
  const API = "https://api.onlyx.ai/v1";
  const auth = { Authorization: `Bearer ${process.env.ONLYX_API_KEY}` };
  const BASE = `${API}/creators/cre_5f2d9a1c7b3e4f60a2d1/ai-content`;

  async function write(method, path, body) {
    const res = await fetch(`${BASE}${path}`, {
      method,
      headers: { ...auth, "Content-Type": "application/json", ...(method === "POST" ? { "Idempotency-Key": crypto.randomUUID() } : {}) },
      body: JSON.stringify(body),
    });
    const data = await res.json();
    if (!res.ok) throw new Error(`${method} ${path}: ${data.error.code} ${data.error.message}`);
    return data; // the whole board
  }
  const created = (before, after, key) => {
    const old = new Set(before[key].map((x) => x.id));
    return after[key].find((x) => !old.has(x.id)).id;
  };

  let board = await (await fetch(BASE, { headers: auth })).json();

  let after = await write("POST", "/folders", { name: "Main" });
  const folderId = created(board, after, "folders"); board = after;

  after = await write("POST", "/collections", {
    name: "Bedroom tease", folderId, mode: "sequential",
    description: "A lazy morning in bed: cozy, then red lace, then the shower.", active: false,
  });
  const ladderId = created(board, after, "collections"); board = after;

  const LEVELS = [
    [{ name: "Opener", priceCents: 0, tags: ["morning", "cozy"], highlight: "Just woke up thinking about you" }, ["4012345601"], []],
    [{ name: "Lace set", priceCents: 1200, tags: ["lingerie", "lace", "red", "bedroom"], highlight: "The red lace set you asked about",
       description: "Eight photos in the red lace set on her bed, morning light, face visible." },
     ["4012345610"], Array.from({ length: 8 }, (_, i) => `40123456${11 + i}`)],
    [{ name: "Shower video", priceCents: 2500, tags: ["shower", "video"], highlight: "Three minutes in the shower",
       description: "A three-minute shower video, playful and slow." }, ["4012345620"], ["4012345621"]],
    [{ name: "Thank-you", priceCents: 0, tags: ["thank you"], highlight: "A little thank-you for you" }, ["4012345630"], []],
  ];

  const levelIds = [];
  for (const [fields, preview, paid] of LEVELS) {
    after = await write("POST", "/levels", { collectionId: ladderId, ...fields });
    const levelId = created(board, after, "levels"); board = after;
    levelIds.push(levelId);
    if (preview.length) board = await write("POST", `/levels/${levelId}/media`, { mediaIds: preview, role: "preview" });
    if (paid.length) board = await write("POST", `/levels/${levelId}/media`, { mediaIds: paid, role: "paid" });
  }

  board = await write("PUT", `/collections/${ladderId}/order`, { levelIds });

  for (const l of board.levels.filter((l) => l.collectionId === ladderId)) {
    if (l.free !== (l.priceCents === 0)) throw new Error(`${l.name}: unexpected price`);
    if (!l.free && l.paid.length === 0) throw new Error(`${l.name} has no paid media`);
    console.log(l.position, l.name, l.priceCents, l.preview.length, l.paid.length);
  }

  board = await write("PATCH", `/collections/${ladderId}`, { active: true });
  console.log("Ladder is live:", ladderId);
  ```
</CodeGroup>

## Changing a live ladder

* **Change a price**: `PATCH /ai-content/levels/{levelId}` with `{"priceCents": 1500}`. Moving a level between free and priced converts all its media: free makes everything preview; priced makes everything paid (re-add the teaser as preview afterwards).
* **Pause one level**: `{"active": false}` on the level. **Pause the whole ladder**: `{"active": false}` on the collection.
* **Keep a level out of chats** but in the board: `{"availableInPaidMessages": false}`.
* **Delete**: deleting a level renumbers the rest; deleting a ladder deletes its levels; deleting a folder keeps its ladders, unfiled.

## Errors

| Status | Code | When |
| - | - | - |
| 400 | `VALIDATION_ERROR` | A missing `priceCents` or `collectionId`, a bad `mode` or `role`, an unknown field, a reorder id that is not on that ladder or level side, too many tags, text too long |
| 404 | `NOT_FOUND` | A folder, ladder, level or media placement that does not exist for this creator |
| 404 | `CREATOR_NOT_FOUND` | The creator does not exist or the key cannot see her |
| 403 | `INSUFFICIENT_SCOPE` | The key lacks `ai:write` |
| 409 | `IDEMPOTENCY_KEY_REUSED` | The same `Idempotency-Key` was sent with a different body |
| 429 | `RATE_LIMITED` | More than 60 content writes in a minute from this credential; wait `Retry-After` seconds |

## Build it with your AI assistant

With the [MCP server](/developers/mcp/overview) connected, the `build_content_ladder` prompt walks the same steps: it lists the vault (and can look at thumbnails), proposes levels and prices, shows you the plan, and only then creates everything with the `create_content_*` and `add_media_to_level` tools. For example: *"Build Mia a sequential ladder from her red lace photos and shower videos, with a free opener, and show me the plan before creating anything."*

## Related

* [AI content](/developers/concepts/ai-content): modes, floors, preview versus paid, how the AI chooses.
* [Vault media](/developers/guides/vault-media): finding media ids.
* [Set up the AI persona](/developers/guides/ai-persona): describe her content in words.
