RAG & Search

Two complementary query APIs for your synced data. Use Semantic Search to find messages by meaning, or the Query DSL for precise structured queries across messages, chats, fans, and chunks.

Authentication

Both endpoints require an X-API-Key header. The key determines the organization scope — all results are automatically restricted to models belonging to your organization. You cannot query data outside your org regardless of filter values.

Query DSL

Structured queries across four whitelisted tables. All results are scoped to your organization — model_id values are validated against your org's connected models.

curl -X POST "https://api.onlyfans-api.ai/api/query" \
  -H "X-API-Key: sk_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"table": "messages", "filters": {"fan_platform_id": {"eq": "553393565"}, "is_from_model": false}, "sort": {"field": "created_at", "direction": "desc"}, "limit": 5}'
ParameterTypeDescription
tablerequired
stringTable to query. One of: messages, chats, fans, message_chunks.
filters
objectFilter conditions keyed by field name. Each value is an operator object e.g. {"gte": "2026-01-01"}. See filter operators below.
fields
arrayList of field aliases to return. Omit to return all default fields for the table.
sort.field
stringField to sort by. Must be one of the table's sortable fields.Default: created_at
sort.direction
stringSort direction: asc or desc.Default: asc
limit
integerNumber of rows to return. Maximum 1000.Default: 20
offset
integerPagination offset.Default: 0
include_raw_payload
booleanmessages table only. When true, each row includes the full raw OnlyFans payload.Default: false
Response — messages
{
  "table": "messages",
  "total": 1482,
  "limit": 5,
  "offset": 0,
  "data": [
    {
      "message_id": "9d5b817a-eced-46b6-831c-f68fbbaed798",
      "platform_message_id": "9130375619415",
      "fan_platform_id": "553393565",
      "model_id": "16f3d13b-9415-4e6b-babe-e8db6b31bd97",
      "created_at": "2026-05-10T14:32:00+00:00",
      "message_text": "I love outdoor hiking and photography",
      "price": 0,
      "is_tip": false,
      "media_count": 0,
      "is_from_model": false
    }
  ]
}
curl -X POST "https://api.onlyfans-api.ai/api/query" \
  -H "X-API-Key: sk_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{"table": "fans", "filters": {"total_spent": {"gte": 100}}, "sort": {"field": "total_spent", "direction": "desc"}, "limit": 3}'
Response — fans
{
  "table": "fans",
  "total": 358,
  "limit": 3,
  "offset": 0,
  "data": [
    {
      "fan_id": "c1d2e3f4-a5b6-7890-cdef-012345678901",
      "fan_platform_id": "553393565",
      "model_id": "16f3d13b-9415-4e6b-babe-e8db6b31bd97",
      "display_name": "Harry",
      "username": "sirharoldprince",
      "total_spent": 2171.50,
      "tips_spent": 1460.00,
      "messages_spent": 597.50,
      "subscribed_on": true,
      "last_seen": "2026-06-18T09:14:00+00:00",
      "can_receive_message": true,
      "is_blocked": false,
      "created_at": "2025-10-02T11:00:00+00:00",
      "updated_at": "2026-06-18T09:14:00+00:00"
    }
  ]
}

Available Tables

Each table exposes a fixed set of filterable and returnable fields. You cannot query tables or fields not listed here.

messagesSynced message records from fan conversations.
Filter fieldscreated_at, is_from_model, price, is_tip, media_count, message_text, fan_platform_id, model_id
Sort fieldscreated_at, price, media_count
Return fieldsmessage_id, platform_message_id, fan_platform_id, model_id, created_at, message_text, price, is_tip, media_count, is_from_model
Opt-in fieldsraw_payload (via include_raw_payload: true)
chatsChat thread records — one per fan–model pair.
Filter fieldscreated_at, updated_at, fan_platform_id, model_id
Sort fieldscreated_at, updated_at
Return fieldschat_id, platform_chat_id, fan_platform_id, model_id, created_at, updated_at
Opt-in fields
fansEnriched fan profiles with spending and subscription data.
Filter fieldsfan_platform_id, model_id, is_verified, subscribed_on, subscribed_by, can_receive_message, is_blocked, is_restricted, tips_enabled, last_seen, created_at, updated_at, total_spent, messages_spent, tips_spent, subscribe_price, tips_min, tips_max
Sort fieldstotal_spent, messages_spent, tips_spent, subscribe_price, last_seen, created_at, updated_at
Return fieldsfan_id, fan_platform_id, model_id, display_name, username, name, avatar, is_verified, subscribed_on, subscribed_by, last_seen, can_receive_message, is_blocked, total_spent, messages_spent, tips_spent, posts_spent, subscriptions_spent, created_at, updated_at
Opt-in fields
message_chunksVector embedding chunks derived from messages. Used for semantic search.
Filter fieldsmessage_id, chunk_type, created_at, model_id, fan_platform_id
Sort fieldscreated_at
Return fieldschunk_id, message_id, chunk_type, text, normalized_text, embedding, chunk_metadata, created_at
Opt-in fields

Filter Operators

Filters are expressed as an object keyed by field name. Each value is either a direct value (shorthand for eq) or an operator object.

Filter examples
{
  "filters": {
    "fan_platform_id": "553393565",            // shorthand eq
    "created_at": {
      "gte": "2026-01-01T00:00:00Z",
      "lte": "2026-06-30T23:59:59Z"
    },
    "total_spent": { "gte": 50 },
    "is_from_model": { "eq": false },
    "message_text": { "like": "%birthday%" },
    "fan_platform_id": { "in": ["553393565", "123220869", "37098033"] }
  }
}
OperatorSupported typesDescription
eqstring, integer, float, bool, datetimeExact match. Shorthand: pass a value directly without wrapping in an object.
gteinteger, float, datetimeGreater than or equal to.
lteinteger, float, datetimeLess than or equal to.
gtinteger, float, datetimeStrictly greater than.
ltinteger, float, datetimeStrictly less than.
instringMatch any value in a list. Maximum 500 values.
likestringCase-insensitive substring match. Use % as wildcard: %keyword%.

DSL Error Codes

All DSL validation errors return HTTP 400 with a code field identifying the rejection reason.

Error response shape
{
  "error": "Filter field 'unknown_field' not allowed for table 'messages'",
  "code": "DSL_ERROR"
}
CodeStatusDescription
MISSING_TABLE400The table field was not provided in the request body.
INVALID_TABLE400The requested table is not in the allowed list.
INVALID_FILTERS400The filters field is not a valid object.
INVALID_FIELDS400The fields field is not a valid array.
INVALID_PAGINATION400limit or offset is not an integer.
DSL_ERROR400A filter field, operator, or return field is not allowed. The error message specifies which field caused the rejection.

Org scoping is always enforced

Even if you pass a model_id filter that belongs to another organization, the query will return zero rows — not an error. All results are silently restricted to models in your org before any user-provided filters are applied.

Pagination

The DSL query endpoint uses offset-based pagination.

FieldDescription
totalTotal number of rows matching the filters (before limit/offset). Use this to calculate page count.
limitThe limit applied to this request (echoed back).
offsetThe offset applied to this request (echoed back).
dataArray of result rows for this page.
Fetch all pages
// Page through all results
let offset = 0
const limit = 100
let allData = []

do {
  const res = await fetch(`${BASE_URL}/api/query`, {
    method: "POST",
    headers: { "X-API-Key": apiKey, "Content-Type": "application/json" },
    body: JSON.stringify({ table: "messages", filters: {}, limit, offset }),
  })
  const { data, total } = await res.json()
  allData.push(...data)
  offset += limit
} while (offset < total)

Maximum limit is 1,000

A single request cannot return more than 1,000 rows. Use pagination to retrieve larger datasets.