Publish a post to the For You feed
Rate limit: 5 posts per creator per rolling 24 hours. Bursts are fine — only the daily volume is capped. Exceeding it returns 429 with a Retry-After header.
/api/external/postsRate limit: 5 posts per creator per rolling 24 hours. Bursts are fine — only the daily volume is capped. Exceeding it returns 429 with a Retry-After header.
Three shapes: • TEXT — caption only. • MEDIA — a gallery of 1-10 images from your vault, each free or paid. • DROP — attach an existing approved drop by productId.
For MEDIA, list your vault first (GET /api/external/vault) and use the id of an item whose moderationStatus is APPROVED to publish without waiting.
Everything posted here goes through exactly the same moderation as a post written in the web composer: captions run the prohibited-word filter, and media posts stay PENDING until every image clears the NSFW pipeline. You cannot use this API to bypass review.
Authentication
Send the creator's API key as a bearer token: Authorization: Bearer dpfn_…. See Authentication & API keys.
Only CREATOR and AGENCY accounts can call this endpoint.
Request body
Content type: application/json
| Name | Type | Required | Description | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
caption | string | Optional | Post text. Required for kind=TEXT. Silently truncated to 2000 characters. Max length: 2000 | ||||||||||||
kind | string | Optional | Post shape. DROP is not a request value — send productId instead. One of: TEXT, MEDIA, SUBSCRIPTION, COMMUNITYDefault: "TEXT" | ||||||||||||
productId | string | Optional | Attach an existing approved drop. Forces kind=DROP. Must belong to you. | ||||||||||||
media | object[] | Optional | Required when kind=MEDIA. Max items: 10 Show child attributes
| ||||||||||||
scheduledAt | string | Optional | ISO-8601. At least 1 minute ahead, at most 30 days. Omit to publish now. Format: date-time |
{
"caption": "im 5 min away, wyd?"
}{
"kind": "MEDIA",
"caption": "rate my fit 1-10",
"media": [
{
"vaultItemId": "clxv1a2b30001item",
"isPaid": false,
"order": 0
}
]
}{
"kind": "MEDIA",
"caption": "can i show you??",
"scheduledAt": "2026-08-26T19:00:00Z",
"media": [
{
"vaultItemId": "clxv1a2b30001item",
"isPaid": false,
"order": 0
},
{
"vaultItemId": "clxv1a2b30002item",
"isPaid": true,
"price": 10,
"order": 1
}
]
}Responses
| Name | Type | Description |
|---|---|---|
id | string | The new post id — poll GET /api/external/posts/{id} with it. |
status | string | PENDING is normal, not an error: media posts wait on the NSFW pipeline and a flagged caption waits on a human. One of: PENDING, APPROVED |
pending | boolean | Convenience mirror of status === "PENDING". |
scheduledAt | string | null | Echoed schedule time, or null for immediate posts. Format: date-time |
url | string | null | The creator’s profile URL (where the post appears once live). Null when the creator has no username yet. Format: uri |
{
"id": "clxp0st000001feed",
"status": "PENDING",
"pending": true,
"scheduledAt": null,
"url": "https://www.dropfans.io/u/valeria"
}Errors
| Status | Body | When |
|---|---|---|
| 400 | {"error":"Invalid JSON body"} | The body is not valid JSON. |
| 400 | {"error":"media must be a non-empty array for kind=MEDIA"} | kind=MEDIA without media. |
| 400 | {"error":"A post can carry at most 10 media items"} | More than 10 media entries. |
| 400 | {"error":"media[0].vaultItemId is required"} | A media entry lacks vaultItemId (index varies). |
| 400 | {"error":"media[0].price must be at least $5 for a paid item"} | isPaid without a valid price (index varies). |
| 400 | {"error":"Invalid schedule time"} | scheduledAt is not a parseable date. |
| 400 | {"error":"Schedule time must be at least a minute in the future"} | scheduledAt is in the past or under a minute ahead. |
| 400 | {"error":"Posts can be scheduled at most 30 days ahead"} | scheduledAt beyond 30 days. |
| 400 | {"error":"Write something first"} | kind=TEXT with an empty caption. |
| 400 | {"error":"Drop is not approved yet"} | productId points at a drop still in moderation — check GET /api/external/drops/{id}. |
| 401 | {"error":"Unauthorized","code":"unauthorized"} | Missing or invalid API key. |
| 403 | {"error":"Only creators can post"} | The key belongs to a CONSUMER account. |
| 404 | {"error":"Drop not found"} | productId does not exist on this account. |
| 422 | {"error":"Your post contains a word that isn’t allowed.","matchedWord":"…"} | Caption tripped the prohibited-word filter — rewrite it and retry. |
| 429 | {"error":"You’ve reached the posting limit (5 per day). Try again in …"} | Daily posting cap (5 per rolling 24h) reached. Retry-After header = seconds to wait. This is separate from the API rate limit (whose body carries code:"rate_limited"). |
Rate limiting
Every response carries the X-RateLimit-Tier header and, on limited tiers, the per-minute and per-day trios — read X-RateLimit-Remaining and X-RateLimit-Reset instead of hardcoding limits. Details in Rate limits.
Code samples
curl -X POST "https://www.dropfans.io/api/external/posts" \
-H "Authorization: Bearer $DROPFANS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"caption": "im 5 min away, wyd?"
}'const res = await fetch(`https://www.dropfans.io/api/external/posts`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
"caption": "im 5 min away, wyd?"
}),
});
console.log(await res.json());import os
import requests
res = requests.post(
"https://www.dropfans.io/api/external/posts",
headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"},
json={
"caption": "im 5 min away, wyd?",
},
)
print(res.json())Notes
The 429 here (posting cap, prose body with Retry-After) is a different limit from the gateway rate limit (429 with code:"rate_limited") — handle both. SUBSCRIPTION and COMMUNITY kinds additionally require an active subscription setup / VIP Telegram channel on the account and 400 otherwise.
Questions? [email protected]
