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

# Track promotion links

> List a creator's OnlyFans tracking and trial links with clicks, subscribers, earnings, cost and ROI; record cost and source; create new links with a 202 and poll.

OnlyFans **tracking links** are the creator's campaign links (`onlyfans.com/<handle>/c<code>`): each one counts the clicks and subscribers it brings. **Trial links** also give new fans a free trial. OnlyX reads every link from OnlyFans about once an hour while the creator is connected, lets you record what each promotion cost and where it ran, and works out earnings, profit and ROI per link.

**Scopes**: `links:read` to list and read; `links:write` to update and create.

## List links

`GET /v1/creators/{creatorId}/tracking-links` returns the creator's links, most clicked first, as a page (`limit` 1 to 100, default 25, and `cursor`; see [Pagination](/developers/pagination)). Add `includeArchived=true` to include links you archived.

<CodeGroup>
  ```bash cURL theme={"system"}
  curl "https://api.onlyx.ai/v1/creators/cre_5f2d9a1c7b3e4f60a2d1/tracking-links" \
    -H "Authorization: Bearer $ONLYX_API_KEY"
  ```

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

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

  links = requests.get(f"{API}/creators/{CREATOR}/tracking-links", headers=HEADERS, timeout=30).json()["data"]
  for l in links:
      roi = f"{l['roi']:.0%}" if l["roi"] is not None else "n/a"
      print(l["name"], l["clicks"], l["subscribers"], l["earningsCents"], l["costCents"], roi)
  ```

  ```javascript JavaScript theme={"system"}
  const API = "https://api.onlyx.ai/v1";
  const headers = { Authorization: `Bearer ${process.env.ONLYX_API_KEY}` };
  const CREATOR = "cre_5f2d9a1c7b3e4f60a2d1";

  const { data: links } = await (await fetch(`${API}/creators/${CREATOR}/tracking-links`, { headers })).json();
  for (const l of links) console.log(l.name, l.clicks, l.subscribers, l.earningsCents, l.costCents, l.roi);
  ```
</CodeGroup>

```json Response 200 theme={"system"}
{
  "data": [
    {
      "id": "trk_5a7c9e1b3d5f7a9c1e3b",
      "kind": "tracking",
      "name": "Reddit September",
      "code": "17",
      "url": "https://onlyfans.com/miarose/c17",
      "clicks": 4210,
      "subscribers": 388,
      "claims": 0,
      "claimsLimit": null,
      "trialDays": null,
      "expiresAt": null,
      "ofCreatedAt": "2026-09-01T10:00:00.000Z",
      "costCents": 25000,
      "source": "reddit",
      "note": "Paid posts in three subreddits",
      "archived": false,
      "earningsCents": 612400,
      "earningsKnown": true,
      "attributedFans": 388,
      "accountedFans": 371,
      "conversion": 0.0922,
      "profitCents": 587400,
      "roi": 23.496,
      "cpcCents": 6,
      "epcCents": 145,
      "cpfCents": 64,
      "arpuCents": 1578,
      "firstSeenAt": "2026-09-01T10:14:00.000Z",
      "lastSeenAt": "2026-09-26T14:00:00.000Z"
    }
  ],
  "hasMore": false,
  "nextCursor": null
}
```

### What the numbers mean

**From OnlyFans**

| Field | Meaning |
| - | - |
| `kind` | `tracking` or `trial` |
| `name`, `code`, `url` | The link as it exists on OnlyFans |
| `ofCreatedAt` | When it was created on OnlyFans |
| `firstSeenAt`, `lastSeenAt` | When OnlyX first and last read the link |
| `clicks`, `subscribers` | What OnlyFans counted for the link |
| `claims`, `claimsLimit`, `trialDays`, `expiresAt` | Trial links: how many trials were claimed, the cap (`null` for no limit), the trial length, and when the link stops working (`null` if never) |

**From your team** (set with `PATCH`)

| Field | Meaning |
| - | - |
| `costCents` | What the promotion cost. `null` means nobody entered a cost; `0` means it was free. |
| `source` | Where it ran, for example `reddit`, `instagram`, `shoutout` |
| `note` | Anything else |
| `archived` | Hidden from the default list |

**Computed by OnlyX**

| Field | Formula |
| - | - |
| `earningsCents` | Everything the fans who came through this link have spent with the creator so far (lifetime spend, not only in the campaign window) |
| `attributedFans`, `accountedFans` | How many fans OnlyFans credits to the link, and how many of those OnlyX has spending data for. When `accountedFans` is lower, `earningsCents` is a floor. |
| `earningsKnown` | `false` until earnings could be worked out (no attributed fan is known yet); `earningsCents` is then `null` (unknown, not zero) |
| `conversion` | `subscribers / clicks` |
| `profitCents` | `earningsCents - costCents` |
| `roi` | `profitCents / costCents` (`23.5` means 2,350%) |
| `cpcCents`, `cpfCents` | Cost per click, cost per fan |
| `epcCents`, `arpuCents` | Earnings per click, earnings per fan |

Any metric whose inputs are missing is `null`: for example ROI is `null` until you enter a cost. Never add link earnings together to get the creator's revenue: a fan who came through two links counts in both, and earnings include spend outside the campaign. Use [revenue stats](/developers/guides/pull-stats) for totals.

## One link with its daily history

`GET /v1/creators/{creatorId}/tracking-links/{linkId}?days=30` returns the same fields plus `daily` for the last `days` days (1 to 365, default 90). A link that belongs to another creator is `404 NOT_FOUND`, even in your own workspace.

```json theme={"system"}
{
  "daily": [
    { "day": "2026-09-25", "clicks": 4102, "subscribers": 379, "clicksDelta": 131, "subscribersDelta": 12 },
    { "day": "2026-09-26", "clicks": 4210, "subscribers": 388, "clicksDelta": 108, "subscribersDelta": 9 }
  ]
}
```

`clicks` and `subscribers` are running totals at the end of each day OnlyX read the link (`day` is a UTC date); the `...Delta` fields are that day's change.

## Record cost and source

`PATCH /v1/creators/{creatorId}/tracking-links/{linkId}` changes only what you send. Send `null` to clear `costCents`, `source` or `note`. This only changes OnlyX; nothing on OnlyFans changes, and fields OnlyFans owns (name, code, counts) cannot be sent (`400 VALIDATION_ERROR`). The answer is the updated link. Send an `Idempotency-Key` header to make retries safe.

| Field | Rules |
| - | - |
| `costCents` | 0 to 100,000,000, or `null` |
| `source` | Up to 60 characters |
| `note` | Up to 500 characters |
| `archived` | `true` or `false` |

<CodeGroup>
  ```bash cURL theme={"system"}
  curl -X PATCH https://api.onlyx.ai/v1/creators/cre_5f2d9a1c7b3e4f60a2d1/tracking-links/trk_5a7c9e1b3d5f7a9c1e3b \
    -H "Authorization: Bearer $ONLYX_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"costCents": 25000, "source": "reddit", "note": "Paid posts in three subreddits"}'
  ```

  ```python Python theme={"system"}
  link = requests.patch(
      f"{API}/creators/{CREATOR}/tracking-links/trk_5a7c9e1b3d5f7a9c1e3b",
      headers=HEADERS,
      json={"costCents": 25000, "source": "reddit", "note": "Paid posts in three subreddits"},
      timeout=30,
  ).json()
  print(link["roi"], link["profitCents"])
  ```

  ```javascript JavaScript theme={"system"}
  const link = await (await fetch(`${API}/creators/${CREATOR}/tracking-links/trk_5a7c9e1b3d5f7a9c1e3b`, {
    method: "PATCH",
    headers: { ...headers, "Content-Type": "application/json" },
    body: JSON.stringify({ costCents: 25000, source: "reddit", note: "Paid posts in three subreddits" }),
  })).json();
  ```
</CodeGroup>

## Create a link

`POST /v1/creators/{creatorId}/tracking-links` creates a **real link on the creator's OnlyFans account**. It needs an `Idempotency-Key` header, and the creator must be connected.

| Field | Rules |
| - | - |
| `kind` | `tracking` or `trial` |
| `name` | 1 to 100 characters, as it will appear on OnlyFans |
| `trialDays` | Trial links only: 1 to 365 |
| `claimsLimit` | Trial links only: how many fans can claim it, 1 to 100,000 |
| `expiresAt` | Trial links only: when the link stops working (ISO-8601, in the future) |

<Warning>
  This creates a link on the creator's live OnlyFans account, and it cannot be deleted through the API. Confirm the name and settings first. Limits: **5 links per creator per 24 hours**, and 10 per hour for the workspace. OnlyFans does not allow free-trial links on a free page (subscription price \$0).
</Warning>

Creating takes a little while, so the API answers `202` at once with a `creationId`. Poll `GET /v1/creators/{creatorId}/tracking-links/creations/{creationId}` **every 5 seconds or slower**:

| `state` | Meaning |
| - | - |
| `creating` | Still being created on OnlyFans |
| `completed` | Created. `url` is the new link as soon as OnlyFans shows it. `linkId` is filled in once OnlyX next reads her links, which can take up to about an hour; until then it is `null`. |
| `failed` | Not created. `error.code` is a stable code and `error.message` says why in plain words (see below). |

| `error.code` | Meaning |
| - | - |
| `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. |
| `LINK_REFUSED` | OnlyFans is not letting this account create a link right now. Check her promotions in OnlyFans, then try again. |
| `CREATION_FAILED` | The link could not be created for another reason. Try again later. |

More codes may be added; handle unknown ones like `CREATION_FAILED`.

<CodeGroup>
  ```bash cURL theme={"system"}
  curl -X POST https://api.onlyx.ai/v1/creators/cre_5f2d9a1c7b3e4f60a2d1/tracking-links \
    -H "Authorization: Bearer $ONLYX_API_KEY" \
    -H "Content-Type: application/json" \
    -H "Idempotency-Key: link-miarose-tiktok-oct" \
    -d '{"kind": "tracking", "name": "TikTok October"}'

  curl https://api.onlyx.ai/v1/creators/cre_5f2d9a1c7b3e4f60a2d1/tracking-links/creations/c9e2a4f61b7d3e8a5f0c \
    -H "Authorization: Bearer $ONLYX_API_KEY"
  ```

  ```python Python theme={"system"}
  r = requests.post(
      f"{API}/creators/{CREATOR}/tracking-links",
      headers={**HEADERS, "Idempotency-Key": str(uuid.uuid4())},
      json={"kind": "trial", "name": "Instagram trial October", "trialDays": 7, "claimsLimit": 200,
            "expiresAt": "2026-10-31T23:59:59Z"},
      timeout=30,
  )
  r.raise_for_status()  # 202
  creation_id = r.json()["creationId"]

  while True:
      time.sleep(5)
      c = requests.get(f"{API}/creators/{CREATOR}/tracking-links/creations/{creation_id}",
                       headers=HEADERS, timeout=30).json()
      if c["state"] == "failed":
          raise SystemExit(f"Not created: {c['error']['message']}")
      if c["state"] == "completed":
          print("Created:", c["url"] or "(find the URL in the list shortly)")
          break
  ```

  ```javascript JavaScript theme={"system"}
  const res = await fetch(`${API}/creators/${CREATOR}/tracking-links`, {
    method: "POST",
    headers: { ...headers, "Content-Type": "application/json", "Idempotency-Key": crypto.randomUUID() },
    body: JSON.stringify({ kind: "tracking", name: "TikTok October" }),
  });
  if (res.status !== 202) throw new Error(`${res.status} ${await res.text()}`);
  const { creationId } = await res.json();

  for (;;) {
    await new Promise((r) => setTimeout(r, 5000));
    const c = await (await fetch(`${API}/creators/${CREATOR}/tracking-links/creations/${creationId}`, { headers })).json();
    if (c.state === "failed") throw new Error(`Not created: ${c.error.message}`);
    if (c.state === "completed") { console.log("Created:", c.url ?? "(find the URL in the list shortly)"); break; }
  }
  ```
</CodeGroup>

```json Response 202 theme={"system"}
{ "creationId": "c9e2a4f61b7d3e8a5f0c", "state": "creating" }
```

```json Creation status (completed) theme={"system"}
{
  "creationId": "c9e2a4f61b7d3e8a5f0c",
  "state": "completed",
  "linkId": "trk_9c1e3a5b7d9f1b3d5a7c",
  "url": "https://onlyfans.com/miarose/c18",
  "error": null
}
```

```json Creation status (failed) theme={"system"}
{
  "creationId": "c9e2a4f61b7d3e8a5f0c",
  "state": "failed",
  "linkId": null,
  "url": null,
  "error": {
    "code": "LINK_NAME_TAKEN",
    "message": "A link with this name already exists on her OnlyFans account. Choose another name."
  }
}
```

A creation can be polled for about two hours. After that (or for a `creationId` that was not issued for this creator) the answer is `404 NOT_FOUND`: look for the link in the list instead.

| Status | Code | When |
| - | - | - |
| 400 | `IDEMPOTENCY_KEY_REQUIRED` | No `Idempotency-Key` header |
| 400 | `VALIDATION_ERROR` | A missing name, trial fields on a `tracking` link, values out of range |
| 409 | `CREATOR_NOT_CONNECTED` | The creator is not connected |
| 409 | `LINK_REFUSED` | OnlyFans is not letting this account create links right now |
| 409 | `IDEMPOTENCY_KEY_REUSED` / `IDEMPOTENCY_IN_PROGRESS` | The key was used for a different request, or the first request is still running |
| 429 | `RATE_LIMITED` | 5 links for this creator in the last 24 hours, or 10 in the last hour for the workspace. Wait `Retry-After` seconds. |
| 503 | `SERVICE_UNAVAILABLE` | Link creation is briefly unavailable. Retry after `Retry-After` seconds with the same `Idempotency-Key`. The creation poll can answer this too. |

## Freshness

Clicks and subscribers come from OnlyFans about once an hour while the creator is connected (see `lastSeenAt`). Earnings update as fans spend. There is no need to poll the list more often than every few minutes.

## From your AI assistant

With the [MCP server](/developers/mcp/overview) connected: *"Which of Mia's tracking links had the best ROI this month? Set the cost of the Reddit link to \$250."* uses `list_tracking_links` and `update_tracking_link`. Creating a link (`create_tracking_link`) creates it on the live OnlyFans account, so the assistant asks you to confirm first.

## Related

* [Pull stats](/developers/guides/pull-stats): revenue totals.
* [Fans](/developers/guides/fans): the fans behind the numbers.
* [Rate limits](/developers/rate-limits): link creation limits.
