curl --request POST \
--url https://api.onlyx.ai/v1/conversations/{conversationId}/messages \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: <idempotency-key>' \
--data '
{
"text": "Thinking of you 😘"
}
'import requests
url = "https://api.onlyx.ai/v1/conversations/{conversationId}/messages"
payload = { "text": "Thinking of you 😘" }
headers = {
"Idempotency-Key": "<idempotency-key>",
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {
'Idempotency-Key': '<idempotency-key>',
Authorization: 'Bearer <token>',
'Content-Type': 'application/json'
},
body: JSON.stringify({text: 'Thinking of you 😘'})
};
fetch('https://api.onlyx.ai/v1/conversations/{conversationId}/messages', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));Send a message
Sends a message to the fan on OnlyFans: text, free vault media (previewMediaIds), or a paid message (mediaIds locked behind priceCents, optionally with free teasers). This reaches a real fan and cannot be taken back through the API: when an automation or an AI assistant composes it, show a person the exact text, media and price first. The answer is 202 with the message; follow it with GET …/messages/{messageId} until delivery.status is sent, failed, unconfirmed or not_sent, and never resend an unconfirmed message. Side effects: the conversation moves to team (the AI pauses on it) for 12 hours, and the AI’s pending follow-ups in it are cancelled.
Errors: 409 SALES_OPTED_OUT for a price when the fan asked not to be sold to; 400 SEND_REFUSED when OnlyFans or OnlyX refuses this particular send (the message says why; the same send gets the same answer); 409 CREATOR_NOT_CONNECTED when her account is not connected; 503 SEND_UNAVAILABLE when sending is briefly unavailable (retry with the same key); 503 SENDING_DISABLED when sending through the API is switched off.
Limits: 30 sends per 60 seconds per credential and 300 per hour per creator (all credentials together). An Idempotency-Key header is required: a retry with the same key returns the original message with Idempotent-Replayed: true, and the fan never gets it twice.
Scope: requires messages:send.
Rate limits: 30 requests per 60 seconds per credential; 300 messages per hour per creator, across all credentials — on top of the general limit of 120 requests per 60 seconds per credential.
Idempotency: an Idempotency-Key header is required. Retry a failed or timed-out request with the same key: a replay returns the original response with Idempotent-Replayed: true, and nothing is done twice.
Reaches fans: this operation can reach a real fan or change the live OnlyFans account. Confirm it with a person before calling it on their behalf.
curl --request POST \
--url https://api.onlyx.ai/v1/conversations/{conversationId}/messages \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--header 'Idempotency-Key: <idempotency-key>' \
--data '
{
"text": "Thinking of you 😘"
}
'import requests
url = "https://api.onlyx.ai/v1/conversations/{conversationId}/messages"
payload = { "text": "Thinking of you 😘" }
headers = {
"Idempotency-Key": "<idempotency-key>",
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {
'Idempotency-Key': '<idempotency-key>',
Authorization: 'Bearer <token>',
'Content-Type': 'application/json'
},
body: JSON.stringify({text: 'Thinking of you 😘'})
};
fetch('https://api.onlyx.ai/v1/conversations/{conversationId}/messages', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));Authorizations
An API key (onx_sk_…) created in OnlyX under Settings → API & MCP, or an OAuth access token (onx_at_…) issued to a connected app. Send it as Authorization: Bearer <credential>. API keys may also be sent as X-API-Key: <key>.
Headers
Required. A new unique value per new request (8 to 64 letters, digits, - or _; a UUID is ideal). If the request times out or fails with a 5xx, retry with the SAME key: you get the original response (with Idempotent-Replayed: true) and nothing is done twice. Keys are kept for 24 hours. Without it the answer is 400 IDEMPOTENCY_KEY_REQUIRED.
8 - 64^[A-Za-z0-9_-]{8,64}$Path Parameters
The conversation id (cnv_…), from GET /v1/conversations.
40Body
Up to 20 vault media ids locked behind priceCents. Requires a price of at least 300 cents.
20^\d{1,25}$Up to 20 vault media ids the fan receives free and unlocked. On a paid message these are the teaser.
20^\d{1,25}$The unlock price in cents: 0 (free) or 300 to 500000 ($3.00 to $5,000.00). A price requires mediaIds.
0 <= x <= 500000The message text, up to 4,000 characters. Optional when media is attached.
4000Response
Accepted: the message, with its delivery status.
The conversation it belongs to.
When the message was sent (or, for a queued send, accepted).
Delivery of an outgoing message. null for incoming messages.
Show child attributes
Show child attributes
in from the fan, out to the fan.
in, out The message id (msg_…).
Media attached to the message.
Show child attributes
Show child attributes
Set on a paid message, otherwise null.
Show child attributes
Show child attributes
Who wrote it: the fan, the AI chatter, or your team (including anything sent through the API).
fan, ai, team The message text. May be empty when the message is media only.
A tip that came with this message, in cents, or null.