# Dropfans API Documentation — complete edition > Every page of https://www.dropfans.io/developers in one file, in reading > order. The index lives at https://www.dropfans.io/developers/llms.txt; the machine contract at https://www.dropfans.io/developers/openapi.json. --- # Dropfans API > Sell drops, manage a creator’s vault, post to the For You feed and read earnings from any software, with one dpfn_ API key per creator. - Source: https://www.dropfans.io/developers - Section: Get started - OpenAPI: https://www.dropfans.io/developers/openapi.json Dropfans is where creators sell drops — paid media bundles — plus tips and subscriptions. This API lets your software act for a creator, with a key the creator hands you. ## What you can build - **Sell drops from a CRM or a chat.** Package vault content into a paid drop and hand the buyer a checkout link — in a Telegram conversation, a DM, anywhere a URL goes. [Sell a drop end-to-end](https://www.dropfans.io/developers/guides/sell-a-drop.md). - **Manage the vault.** Upload images, audio and video, organise folders, tag content. [Upload media](https://www.dropfans.io/developers/guides/upload-media.md). - **Post to the For You feed.** Text, image and drop posts from a script, a scheduler or an AI agent. [Post to the feed](https://www.dropfans.io/developers/guides/post-to-feed.md). - **Mirror earnings.** Rebuild the creator’s dashboard numbers — earnings, transactions, payout balance — inside your own product. [Earnings & balance](https://www.dropfans.io/developers/guides/earnings-and-balance.md). ## How it works - **One key, one creator.** Every request carries `Authorization: Bearer dpfn_…`. A key is bound to one creator account and grants the full surface — there are no scopes. Creators generate keys in the dashboard under [Vault → API Connect](https://www.dropfans.io/dashboard/vault?apiConnect=1). - **Moderation parity.** Everything created through the API runs exactly the same review as the dashboard composer. You cannot use the API to bypass moderation — a `PENDING` response is normal, not an error. - **Rate limits by tier.** Personal keys get 60 requests a minute, approved apps 300. Dropfans-operated integrations are exempt. Read the `X-RateLimit-*` headers rather than hardcoding — [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md). ## Base URL ``` https://www.dropfans.io ``` Every endpoint lives under `/api/external/`. Responses are JSON; uploads are multipart. Built on this API: [KVIQA](https://kviqa.com), the Telegram and WhatsApp CRM for creators — vault, drops and earnings in one inbox. Next: [Make your first call](https://www.dropfans.io/developers/get-started/first-call.md) or [Connect a creator to your app](https://www.dropfans.io/developers/get-started/connect-a-creator.md). --- Next: [Make your first call](https://www.dropfans.io/developers/get-started/first-call.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # Make your first call > One authenticated request to GET /api/external/me that proves your key works. - Source: https://www.dropfans.io/developers/get-started/first-call - Section: Get started - OpenAPI: https://www.dropfans.io/developers/openapi.json The shortest path to a working integration is one request that returns the creator behind your key. ## 1. Generate a key In the Dropfans dashboard, open [Vault → API Connect](https://www.dropfans.io/dashboard/vault?apiConnect=1) and hit **Generate new key**. Pick **Personal** for your own scripts, or your app if it has been [approved](https://www.dropfans.io/developers/concepts/apps-and-approval.md). The key starts with `dpfn_` — you can copy it again from the same screen any time. > [!WARNING] > A key gives full access to that creator’s vault, drops, earnings and feed. Treat it like a password and keep it out of client-side code and shared chats. ## 2. Send the request ```bash tab="curl" curl "https://www.dropfans.io/api/external/me" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ```javascript tab="Node" const res = await fetch('https://www.dropfans.io/api/external/me', { headers: { Authorization: 'Bearer ' + process.env.DROPFANS_API_KEY }, }); const me = await res.json(); console.log(me.username); ``` ```python tab="Python" import os, requests res = requests.get( "https://www.dropfans.io/api/external/me", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()["username"]) ``` ## 3. Read the response ```json { "id": "cmawq81x40001lb04xyz12abc", "username": "ava", "name": "Ava", "image": "https://cdn.dropfans.io/ava/avatar.jpg", "accountType": "CREATOR", "key": { "name": "Personal", "app": null, "tier": "personal" } } ``` If `username` is the creator you expected, authentication works. `key.app` names the app the key was generated for (`null` for a personal key), and `key.tier` is the rate-limit tier the key runs at. ## 4. Decode a 401 ```json { "error": "Unauthorized", "code": "unauthorized" } ``` Check, in order: the header is exactly `Authorization: Bearer dpfn_…` (the literal `Bearer ` prefix, one space); the key was not truncated or padded with whitespace when you copied it; the creator has not revoked it — a revoked key returns 401 forever, there is no grace period. Next: [Connect a creator to your app](https://www.dropfans.io/developers/get-started/connect-a-creator.md) or [Sell a drop end-to-end](https://www.dropfans.io/developers/guides/sell-a-drop.md). --- Previous: [Dropfans API](https://www.dropfans.io/developers/get-started/overview.md) · Next: [Connect a creator to your app](https://www.dropfans.io/developers/get-started/connect-a-creator.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # Connect a creator to your app > How a third-party app obtains and safely stores a creator’s Dropfans API key. - Source: https://www.dropfans.io/developers/get-started/connect-a-creator - Section: Get started - OpenAPI: https://www.dropfans.io/developers/openapi.json > [!NOTE] Looking for OAuth? > There is no OAuth flow. The creator generates a key **for your app** and pastes it into your product — that key is the whole handshake. It never expires and there is nothing to refresh. ## How a creator connects 1. [Apply for API access](https://www.dropfans.io/developers/apply). Once approved, your app appears in the key picker every creator sees under Vault → API Connect. 2. Send the creator your connect link: ``` https://www.dropfans.io/dashboard/vault?apiConnect=1&app= ``` It opens the picker with your app preselected — the creator hits Generate and copies the key. The link survives login, so it works for creators who are signed out when they click it. 3. The creator pastes the key into your app. Agencies connect the same way, once per managed creator — each key is bound to exactly one creator. See [Agencies](https://www.dropfans.io/developers/concepts/agencies.md). ## Validate and store - Validate the pasted key immediately with [GET /api/external/me](https://www.dropfans.io/developers/reference/get-me.md) and show the `username` back, so the creator sees they connected the right account. - Store the key encrypted at rest. Never log it, never send it to your frontend, never echo it in error messages. - One key per creator per app is enough — there is no benefit to minting more. ## Revocation and suspension - **The creator revokes the key** (same screen it was made on): every request returns `401` from that moment, forever. Handle it by asking the creator to reconnect — do not retry in a loop. - **Dropfans suspends your app**: every request with an app-bound key returns `403 { "error": …, "code": "app_suspended" }`. Keys are not deleted — when the app is reinstated they work again unchanged. See [Apps & approval](https://www.dropfans.io/developers/concepts/apps-and-approval.md). Next: [Testing without a sandbox](https://www.dropfans.io/developers/get-started/testing.md) or [Authentication & API keys](https://www.dropfans.io/developers/concepts/authentication.md). --- Previous: [Make your first call](https://www.dropfans.io/developers/get-started/first-call.md) · Next: [Testing without a sandbox](https://www.dropfans.io/developers/get-started/testing.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # Testing without a sandbox > There is no sandbox — test against a creator account you control, with free drops and pending content. - Source: https://www.dropfans.io/developers/get-started/testing - Section: Get started - OpenAPI: https://www.dropfans.io/developers/openapi.json The Dropfans API has no sandbox environment. You test against a real creator account that you control — same endpoints, same moderation, same data model as production, because it *is* production. ## Set up a test creator 1. Sign up at [dropfans.io](https://www.dropfans.io) and complete creator verification. 2. Upload at least one item to the vault — keys are generated from [Vault → API Connect](https://www.dropfans.io/dashboard/vault?apiConnect=1), and you need content in the vault before anything downstream (drops, media posts) can be exercised. 3. Generate a **Personal** key for the test account and keep it separate from any real creator’s key in your config. ## What to use for end-to-end runs - **Free drops.** `POST /drops` accepts `price: 0` — a free drop walks the whole create → link → checkout path without moving money. Do not buy paid drops with real cards unless you intend to pay; there is no test card. - **`includePending=true`** on [GET /vault](https://www.dropfans.io/developers/reference/list-vault.md) shows items still in review, including their `moderationStatus` — without it, pending items come back with an empty `filePath` and no status field. - **Moderation takes real time.** Uploads and posts go through the same review pipeline as everything else, so `PENDING` for minutes is normal, not a failure. Build your polling now — production behaves identically. See [Moderation & statuses](https://www.dropfans.io/developers/concepts/moderation-and-statuses.md). ## Cleaning up - Posts delete hard: `DELETE /posts/{id}` removes them outright. - Vault items soft-hide: `DELETE /vault/{id}` hides the item from every list but does not purge the file. Re-deleting is a no-op success. - Drops cannot be deleted through the API — keep test drops free so a stray link costs nobody anything. Next: [Authentication & API keys](https://www.dropfans.io/developers/concepts/authentication.md) or [Sell a drop end-to-end](https://www.dropfans.io/developers/guides/sell-a-drop.md). --- Previous: [Connect a creator to your app](https://www.dropfans.io/developers/get-started/connect-a-creator.md) · Next: [Authentication & API keys](https://www.dropfans.io/developers/concepts/authentication.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # Authentication & API keys > Every request carries Authorization: Bearer dpfn_… — a key is bound to one creator and grants the full surface. - Source: https://www.dropfans.io/developers/concepts/authentication - Section: Concepts - OpenAPI: https://www.dropfans.io/developers/openapi.json ## The header Every request sends the key in the `Authorization` header, exactly like this: ```http Authorization: Bearer dpfn_your_key_here ``` The literal `Bearer ` prefix, one space, then the key. Keys always start with `dpfn_`. Anything else — a missing prefix, a stray newline, an `X-Api-Key` header — returns `401 { "error": "Unauthorized", "code": "unauthorized" }`. ## What a key grants A key is bound to **one creator account** and grants the full surface for that creator: vault read and write, drops, posts, earnings, balance, links and Telegram notification settings. There are **no scopes** and **no expiry** — a key works until the creator revokes it. Posts and drops created with a key are always authored by that creator. ## Personal keys vs app keys The creator picks at generation time, and the choice is fixed for the life of the key: - **Personal** — for the creator’s own scripts or AI agent. Runs at the personal rate tier, and all personal keys of one creator share a single budget, so minting more keys does not buy more throughput. - **App-bound** — generated for an [approved app](https://www.dropfans.io/developers/concepts/apps-and-approval.md). Runs at the app tier with a per-key budget, and identifies your software to Dropfans. `GET /me` tells you which kind you hold — see [Make your first call](https://www.dropfans.io/developers/get-started/first-call.md). ## Revocation Creators revoke keys from the same screen they were generated on (Vault → API Connect). A revoked key returns `401` immediately and forever. Handle a 401 by asking the creator to reconnect, never by retrying in a loop. ## Storage hygiene - Store keys encrypted at rest; decrypt only to make the request. - Keep keys server-side. Never ship one in client code, a repo, or a shared chat. - Never log the full key. If you must reference it, use the first 12 characters — that prefix is what the creator sees in their key list. Next: [Apps & approval](https://www.dropfans.io/developers/concepts/apps-and-approval.md) or [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md). --- Previous: [Testing without a sandbox](https://www.dropfans.io/developers/get-started/testing.md) · Next: [Apps & approval](https://www.dropfans.io/developers/concepts/apps-and-approval.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # Apps & approval > Personal keys work immediately; apps that connect other people’s creator accounts need a one-time approval. - Source: https://www.dropfans.io/developers/concepts/apps-and-approval - Section: Concepts - OpenAPI: https://www.dropfans.io/developers/openapi.json ## Two ways in - **Personal keys need no approval.** A creator (or you, on your own test account) can generate one right now and script against the full API at the personal rate tier. - **Apps** are for software other people’s creators connect to. You apply once; after approval your app appears in the key picker every creator sees, and keys generated for it run at the higher app tier. ## Applying Apply at [/developers/apply](https://www.dropfans.io/developers/apply) — you need to be logged in to a Dropfans account. The form asks what the app does, which parts of the API it uses, how many creators you expect, and how you store creator keys. You can have at most 3 applications under review and 10 apps in total. What we review: that the use case is a real product, that keys are stored responsibly (encrypted, server-side), that the website checks out, and that the capabilities you ticked match the story. Capability choices are for review only — **keys are not scoped**; an approved app’s key still grants the full surface. ## Statuses | Status | Shown as | What it means for requests | | --- | --- | --- | | `PENDING` | Under review | No keys can be generated for the app yet. Personal keys keep working. | | `APPROVED` | Approved | The app is in every creator’s key picker; keys generated for it work at the app tier. | | `REJECTED` | Not approved | No keys can be generated. The decision email and your [status page](https://www.dropfans.io/developers/apply) say why; you can reapply after fixing the issue. | | `SUSPENDED` | Suspended | Every request with a key bound to the app returns `403 { "error": …, "code": "app_suspended" }`. Keys are not deleted — they resume unchanged if the app is reinstated. | ## What approval unlocks - A listing in the key picker under Vault → API Connect, with your name, logo and description — plus a connect link you can send creators: `https://www.dropfans.io/dashboard/vault?apiConnect=1&app=`. - The app rate tier: 300 requests/minute and 50,000/day **per key**, instead of the shared personal budget. Higher per-app overrides are available on request. ## Decisions Every decision — approved, rejected, suspended, reinstated — is emailed to the contact address on the application, and your current status always shows at [/developers/apply](https://www.dropfans.io/developers/apply). Next: [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md) or [Connect a creator to your app](https://www.dropfans.io/developers/get-started/connect-a-creator.md). --- Previous: [Authentication & API keys](https://www.dropfans.io/developers/concepts/authentication.md) · Next: [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # Rate limits > Fixed per-minute and per-day windows per key, by tier — read the X-RateLimit-* headers rather than hardcoding. - Source: https://www.dropfans.io/developers/concepts/rate-limits - Section: Concepts - OpenAPI: https://www.dropfans.io/developers/openapi.json ## Tiers | Tier | Per minute | Per day | Who | | --- | --- | --- | --- | | Personal | 60 | 5,000 | Personal keys. The budget is shared across **all** personal keys of the same creator — minting more keys does not multiply it. | | App | 300 | 50,000 | Keys bound to an [approved app](https://www.dropfans.io/developers/concepts/apps-and-approval.md), per key. Higher per-app overrides on request. | | First party | unlimited | unlimited | Dropfans-operated integrations are exempt. | Windows are fixed, not rolling: the minute window is the current clock minute, the day window is the current UTC day. Both reset on the boundary. > [!TIP] > Always read `X-RateLimit-Limit` off the response instead of assuming a number — env-level and per-app overrides mean the live limit can differ from the defaults printed here. ## Headers on every response | Header | Meaning | | --- | --- | | `X-RateLimit-Tier` | `personal`, `app` or `first_party`. | | `X-RateLimit-Limit` | Requests allowed in the current minute window. | | `X-RateLimit-Remaining` | Requests left in the current minute window. | | `X-RateLimit-Reset` | Unix seconds when the minute window resets. | | `X-RateLimit-Limit-Day` | Requests allowed in the current UTC-day window. | | `X-RateLimit-Remaining-Day` | Requests left in the current UTC-day window. | | `X-RateLimit-Reset-Day` | Unix seconds when the day window resets. | | `Retry-After` | On `429` only — seconds to wait before retrying. | First-party responses carry only the tier header; limited tiers carry the full set. ## When you hit the limit ```json { "error": "Rate limit exceeded", "code": "rate_limited" } ``` Status `429`, with `Retry-After` in seconds. Wait that long, then resume — do not hammer the boundary. ## The separate posting cap Creating feed posts has its own cap, independent of the request budget: **5 posts per creator per rolling 24 hours** — the same cap the dashboard composer enforces. Exceeding it also returns `429` with `Retry-After`, but the body is prose without a `code`: ```json { "error": "You’ve reached the posting limit (5 per day). Try again in 3 hours." } ``` Tell the two apart by the `code` field: `rate_limited` means request volume; no code on a posts `429` means the daily posting cap. ## Retry wrapper ```javascript tab="Node" async function callApi(url, options = {}, attempt = 0) { const res = await fetch(url, { ...options, headers: { Authorization: 'Bearer ' + process.env.DROPFANS_API_KEY, ...(options.headers || {}), }, }); if (res.status === 429 && attempt < 3) { const wait = parseInt(res.headers.get('Retry-After') || '60', 10); await new Promise((r) => setTimeout(r, wait * 1000)); return callApi(url, options, attempt + 1); } return res; } ``` ```python tab="Python" import os, time, requests def call_api(method, url, attempt=0, **kwargs): headers = kwargs.pop("headers", {}) headers["Authorization"] = f"Bearer {os.environ['DROPFANS_API_KEY']}" res = requests.request(method, url, headers=headers, **kwargs) if res.status_code == 429 and attempt < 3: wait = int(res.headers.get("Retry-After", "60")) time.sleep(wait) return call_api(method, url, attempt + 1, **kwargs) return res ``` ## Capacity planning Most throttling comes from polling too much, not from real traffic. Budget it: polling [check-status](https://www.dropfans.io/developers/reference/check-drop-status.md) once a minute costs 1,440 requests a day — well inside the app tier, but over a quarter of a personal day budget. Use the batch endpoints (one call covers up to 200 drops), follow the [polling patterns](https://www.dropfans.io/developers/webhooks/overview.md), and watch that page for webhooks, which will replace most polling. Next: [Errors](https://www.dropfans.io/developers/concepts/errors.md) or [Webhooks & polling](https://www.dropfans.io/developers/webhooks/overview.md). --- Previous: [Apps & approval](https://www.dropfans.io/developers/concepts/apps-and-approval.md) · Next: [Errors](https://www.dropfans.io/developers/concepts/errors.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # Errors > HTTP status meanings and the (deliberately honest) catalogue of body shapes you will see. - Source: https://www.dropfans.io/developers/concepts/errors - Section: Concepts - OpenAPI: https://www.dropfans.io/developers/openapi.json The API grew endpoint by endpoint, and its error bodies are not uniform. Rather than pretend otherwise, here is the exact catalogue — what each status means and every shape a body can take. ## Status codes | Status | Meaning | | --- | --- | | `400` | Validation failed — a missing field, a bad value, malformed JSON, or a prohibited word on drop fields. | | `401` | Missing, malformed or revoked API key. | | `403` | The key works but the request is not allowed: not a creator account (posts), a resource you do not own (previews), a suspended app (`app_suspended`), or an expired video-upload token. | | `404` | Resource not found — including resources that exist but belong to another creator (deliberately indistinguishable). | | `409` | Conflict: duplicate folder name, a video upload that has not finished (retry), or a creator with no username on [GET /links](https://www.dropfans.io/developers/reference/get-links.md). | | `413` | Upload too large — see [Upload media](https://www.dropfans.io/developers/guides/upload-media.md) for the caps. | | `422` | A post caption tripped the prohibited-word filter. | | `429` | Rate limited, or the daily posting cap — see [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md). | | `500` | Our fault. Retry with backoff; if it persists, tell support@dropfans.io. | ## Error body shapes | Shape | Where | | --- | --- | | `{ "error": "…" }` | The baseline — human-readable prose, everywhere not listed below. | | `{ "error": "…", "code": "…" }` | Gateway errors: `401` (`unauthorized`), `403` (`app_suspended`, `first_party_only`), `429` (`rate_limited`), and `409` (`username_required` on links). When a `code` is present, branch on it — the prose can change, the code will not. | | `{ "error": "…", "matchedWord": "…" }` | `422` from [POST /posts](https://www.dropfans.io/developers/reference/create-post.md) — the caption word that tripped the filter. | | `{ "error": "…", "field": "…", "matchedWord": "…" }` | `400` from [POST /drops](https://www.dropfans.io/developers/reference/create-drop.md) — which field (`name` or `description`) tripped it, and on what word. | | `{ "error": "Invalid JSON body" }` | `400` from any JSON endpoint when the body does not parse. | ## Success envelopes Successes are not uniform either: | Shape | Where | | --- | --- | | Bare object | `GET /me`, `GET/PUT /timezone`, `GET /balance`, `GET /earnings`, `GET /links`, `GET /posts/{id}`, `POST /posts` (201), `POST /drops`, `POST /vault/folders`, and both list endpoints. | | `{ "success": true, … }` | Vault writes (`DELETE /vault/{id}`, folder move, tags, folder delete, uploads), `POST /drops/{id}/previews`, notification settings. | | `{ "ok": true }` | `DELETE /posts/{id}`. | | Keyed map | The batch endpoints: `POST /vault/video-status` → `{ "statuses": { … } }`, `POST /drops/check-status` → `{ "sales": { … } }`. | > [!IMPORTANT] Batch endpoints omit, they don’t error > `video-status` and `check-status` leave out ids they cannot resolve — unknown, unowned, or unpaid — instead of failing the call. A missing key in the result map is an answer, not an error. Every reference page lists its exact error rows — see the [API reference](https://www.dropfans.io/developers/reference/overview.md). Next: [Pagination & batch limits](https://www.dropfans.io/developers/concepts/pagination.md) or [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md). --- Previous: [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md) · Next: [Pagination & batch limits](https://www.dropfans.io/developers/concepts/pagination.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # Pagination & batch limits > Two page styles, one cap of 50, and two batch endpoints that truncate silently — chunk client-side. - Source: https://www.dropfans.io/developers/concepts/pagination - Section: Concepts - OpenAPI: https://www.dropfans.io/developers/openapi.json ## Vault style — top-level fields [GET /vault](https://www.dropfans.io/developers/reference/list-vault.md) paginates with top-level fields. `limit` defaults to the maximum, 50: ```json { "items": [ … ], "folders": [ … ], "hasMore": true, "total": 132, "page": 1, "limit": 50 } ``` ## Posts style — nested pagination [GET /posts](https://www.dropfans.io/developers/reference/list-posts.md) nests the same information under `pagination`. `limit` defaults to 20 and caps at 50: ```json { "posts": [ … ], "pagination": { "page": 1, "limit": 20, "total": 47, "hasMore": true }, "limits": { … } } ``` > [!IMPORTANT] Iterate on hasMore, not on short pages > A page can legitimately come back shorter than `limit` while more items remain. The only correct loop is: request page 1, then keep incrementing `page` while `hasMore` is true. ## What does not paginate - **Folders** — [GET /vault/folders](https://www.dropfans.io/developers/reference/list-folders.md) returns every folder, unpaginated. - **Earnings transactions** — [GET /earnings](https://www.dropfans.io/developers/reference/get-earnings.md) always returns the 50 most recent transactions in the range; narrow the date range to see older ones. ## Batch caps — truncation is silent Two endpoints take arrays of ids and **silently drop the excess** — no error, no `truncated` flag: | Endpoint | Cap | | --- | --- | | [POST /vault/video-status](https://www.dropfans.io/developers/reference/video-status.md) | 50 ids per call | | [POST /drops/check-status](https://www.dropfans.io/developers/reference/check-drop-status.md) | 200 ids per call | Chunk client-side: send at most the cap per request and merge the result maps. Both endpoints also omit ids they cannot resolve (see [Errors](https://www.dropfans.io/developers/concepts/errors.md)), so never treat a missing key as a transport failure. Next: [Moderation & statuses](https://www.dropfans.io/developers/concepts/moderation-and-statuses.md) or [Errors](https://www.dropfans.io/developers/concepts/errors.md). --- Previous: [Errors](https://www.dropfans.io/developers/concepts/errors.md) · Next: [Moderation & statuses](https://www.dropfans.io/developers/concepts/moderation-and-statuses.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # Moderation & statuses > Nothing posted through the API skips review — every status enum and lifecycle in one place. - Source: https://www.dropfans.io/developers/concepts/moderation-and-statuses - Section: Concepts - OpenAPI: https://www.dropfans.io/developers/openapi.json Everything created through the API runs the same review pipeline as the dashboard: captions and names go through the prohibited-word filter, media rides the NSFW pipeline. The API cannot bypass review — by design. ## The enum `moderationStatus` on vault items, posts and drops is one of: | Value | Meaning | | --- | --- | | `PENDING` | In review. **Normal, not an error** — it clears by itself. | | `APPROVED` | Cleared. Usable and visible. | | `FLAGGED` | Held for human review. May still become APPROVED. | | `REJECTED` | Refused. Will not go live. | ## Vault item lifecycle Upload → `PENDING` → `APPROVED`, `FLAGGED` or `REJECTED`. Two things to know: - [GET /vault](https://www.dropfans.io/developers/reference/list-vault.md) returns only approved items by default. Pass `includePending=true` to see items still in review — those come back with their `moderationStatus` and an **empty `filePath`** until approved. - Review takes real minutes. Upload, then poll the list rather than assuming instant availability. ## Video processing is separate from moderation A video must first finish **processing** on the CDN (encode, thumbnails), then it enters **moderation**. [POST /vault/video-status](https://www.dropfans.io/developers/reference/video-status.md) reports processing only: `{ isReady, isProcessing, isFailed }`. `isReady: true` does not mean approved — check the vault list for `moderationStatus` after processing completes. ## Drop status — derived from its items A drop’s status is computed from its vault items: any `REJECTED` or `FLAGGED` item wins, else any `PENDING` item makes the drop `PENDING`, else it is `APPROVED`. As of this release the status is computed correctly **at creation** — a drop built from approved items is born `APPROVED` and can be attached to a post immediately. Read it back with [GET /drops/{id}](https://www.dropfans.io/developers/reference/get-drop.md). ## Post status and `live` Posts carry the same enum plus `live` — true only when the post is `APPROVED` **and** its publish time has passed. A scheduled, approved post is not live yet. Text posts with clean captions usually approve instantly; media posts stay `PENDING` until every image clears. ## The word filter - [POST /posts](https://www.dropfans.io/developers/reference/create-post.md): a caught caption returns `422 { "error": …, "matchedWord": "…" }` — rewrite and retry once. - [POST /drops](https://www.dropfans.io/developers/reference/create-drop.md): a caught `name` or `description` returns `400 { "error": …, "field": "…", "matchedWord": "…" }`. ## Polling guidance Poll [GET /posts/{id}](https://www.dropfans.io/developers/reference/get-post.md) or the vault list every 30–60 seconds while something is `PENDING`, and stop on any terminal state. Budget it against your [rate tier](https://www.dropfans.io/developers/concepts/rate-limits.md). Next: [Money & units](https://www.dropfans.io/developers/concepts/money-and-units.md) or [Pagination & batch limits](https://www.dropfans.io/developers/concepts/pagination.md). --- Previous: [Pagination & batch limits](https://www.dropfans.io/developers/concepts/pagination.md) · Next: [Money & units](https://www.dropfans.io/developers/concepts/money-and-units.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # Money & units > Prices go in as USD dollars; earnings and sales come out as integer cents; balance is dollars — per endpoint. - Source: https://www.dropfans.io/developers/concepts/money-and-units - Section: Concepts - OpenAPI: https://www.dropfans.io/developers/openapi.json Everything is **USD**. There is no other currency anywhere in the API. The unit, however, differs by endpoint — this table is the truth: | Endpoint | Field | Unit | | --- | --- | --- | | [POST /drops](https://www.dropfans.io/developers/reference/create-drop.md) | `price` (in) | USD **dollars** — `0` (free) or 5–750 | | [POST /posts](https://www.dropfans.io/developers/reference/create-post.md) | `media[].price` (in) | USD **dollars** — at least 5 when `isPaid` | | [GET /earnings](https://www.dropfans.io/developers/reference/get-earnings.md) | everything `…Cents` (out) | integer **cents** | | [POST /drops/check-status](https://www.dropfans.io/developers/reference/check-drop-status.md) | `saleAmountCents` (out) | integer **cents**, gross | | [GET /balance](https://www.dropfans.io/developers/reference/get-balance.md) | all four buckets (out) | USD **dollars** | | [GET /links](https://www.dropfans.io/developers/reference/get-links.md) | `web.tipTemplate` `{usd}` | dollars | | [GET /links](https://www.dropfans.io/developers/reference/get-links.md) | `telegram.tipTemplate` `{cents}` | cents | > [!WARNING] Refunds are counted differently > `earnings` excludes refunded and charged-back transactions. `check-status` does **not** — a refunded sale still reports `paid: true`. Use earnings for money truth, check-status for fulfilment. ## Price limits - Drop price: free (`0`) or between $5 and $750. - Paid post media: at least $5 per item. - Tips (via [tip links](https://www.dropfans.io/developers/guides/links-and-deep-links.md)): $5–$750. ## Gross vs net Dropfans takes a 15% platform fee; the creator keeps 85%. `earnings` reports both: `totalEarningsCents` is **net** (what the creator keeps), `grossEarningsCents` is what buyers paid, and `typeTotals` breaks both down by `drop` / `tip` / `subscription`. `check-status` reports **gross** only. ## Payouts Daily payouts on a rolling 14-day release. The minimum payout is $20. [GET /balance](https://www.dropfans.io/developers/reference/get-balance.md) shows where the money sits: `pending` (in the release hold), `available` (payable), `processing` (in a payout run) and `paidOut` (lifetime). ## Timezones `earnings` buckets by day in the timezone you pass as `tz` (IANA name; invalid values silently fall back to UTC) — so your "today" can match the creator’s dashboard. See [Earnings & balance](https://www.dropfans.io/developers/guides/earnings-and-balance.md). Next: [Agencies](https://www.dropfans.io/developers/concepts/agencies.md) or [Earnings & balance](https://www.dropfans.io/developers/guides/earnings-and-balance.md). --- Previous: [Moderation & statuses](https://www.dropfans.io/developers/concepts/moderation-and-statuses.md) · Next: [Agencies](https://www.dropfans.io/developers/concepts/agencies.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # Agencies > Agency accounts hold one key per managed creator; everything else about the API is identical. - Source: https://www.dropfans.io/developers/concepts/agencies - Section: Concepts - OpenAPI: https://www.dropfans.io/developers/openapi.json Agencies on Dropfans manage multiple creator accounts. The API model stays simple: **one key per managed creator** — there is no cross-creator key and no agency-wide endpoint. ## Generating keys for managed creators While acting as a managed creator in the dashboard, the agency opens that creator’s [Vault → API Connect](https://www.dropfans.io/dashboard/vault?apiConnect=1) and generates a key there. The key list shows which creator each key belongs to, so a roster of keys stays auditable. ## What that means for your integration - Every call is scoped to the creator behind the key you send. To act for five creators, hold five keys and pick per request. There are no cross-creator calls. - [GET /me](https://www.dropfans.io/developers/reference/get-me.md) on each key tells you which creator it is — validate on connect and store the mapping. - Posting requires a CREATOR or AGENCY account type; managed-creator keys satisfy this. - Rate limits apply per the key’s tier as usual — for [approved apps](https://www.dropfans.io/developers/concepts/apps-and-approval.md) each key has its own budget, so throughput scales with the roster. ## Balance quirk [GET /balance](https://www.dropfans.io/developers/reference/get-balance.md) can be **negative** for agency-managed accounts (internal settlements can pull a bucket below zero). Render it signed; do not clamp to zero. Next: [Sell a drop end-to-end](https://www.dropfans.io/developers/guides/sell-a-drop.md) or [Connect a creator to your app](https://www.dropfans.io/developers/get-started/connect-a-creator.md). --- Previous: [Money & units](https://www.dropfans.io/developers/concepts/money-and-units.md) · Next: [Sell a drop end-to-end](https://www.dropfans.io/developers/guides/sell-a-drop.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # Sell a drop end-to-end > From approved vault items to a paid sale: create the drop, share the link, poll for the sale, read the earnings. - Source: https://www.dropfans.io/developers/guides/sell-a-drop - Section: Guides - OpenAPI: https://www.dropfans.io/developers/openapi.json A drop is a paid media bundle with a checkout page. This guide walks the whole loop: pick content → create the drop → share the link → learn about the sale. ## 1. Pick approved vault items Only `APPROVED` items can go into a drop. The default vault listing returns approved items only, so anything it gives you is usable: ```bash tab="curl" curl "https://www.dropfans.io/api/external/vault?limit=10" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ```javascript tab="Node" const res = await fetch('https://www.dropfans.io/api/external/vault?limit=10', { headers: { Authorization: 'Bearer ' + process.env.DROPFANS_API_KEY }, }); const { items } = await res.json(); const ids = items.slice(0, 3).map((i) => i.id); ``` ```python tab="Python" import os, requests HEADERS = {"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"} res = requests.get("https://www.dropfans.io/api/external/vault?limit=10", headers=HEADERS) ids = [i["id"] for i in res.json()["items"][:3]] ``` Need to upload first? See [Upload media](https://www.dropfans.io/developers/guides/upload-media.md). ## 2. Create the drop Price is USD dollars: `0` for free, otherwise $5–$750. At most 10 vault items per drop. ```bash tab="curl" curl -X POST "https://www.dropfans.io/api/external/drops" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Beach set", "price": 15, "vaultItemIds": ["VAULT_ITEM_A", "VAULT_ITEM_B"] }' ``` ```javascript tab="Node" const res = await fetch('https://www.dropfans.io/api/external/drops', { method: 'POST', headers: { Authorization: 'Bearer ' + process.env.DROPFANS_API_KEY, 'Content-Type': 'application/json', }, body: JSON.stringify({ name: 'Beach set', price: 15, vaultItemIds: ids }), }); const { productId, buyUrl } = await res.json(); ``` ```python tab="Python" res = requests.post( "https://www.dropfans.io/api/external/drops", headers=HEADERS, json={"name": "Beach set", "price": 15, "vaultItemIds": ids}, ) drop = res.json() # { "productId": …, "buyUrl": …, "mediaCount": 2 } ``` > [!WARNING] description is accepted, validated — and not stored > `POST /drops` validates a `description` field for prohibited words but deliberately does not persist it. Do not build UI that expects it back. A drop built from approved items is born `APPROVED`. Read it back any time with [GET /drops/{id}](https://www.dropfans.io/developers/reference/get-drop.md) — it returns the status, per-item moderation, `salesCount` and `lastSaleAt`: ```bash curl "https://www.dropfans.io/api/external/drops/PRODUCT_ID" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ## 3. Optional: attach blurred previews If your product bakes its own teaser blur, attach a JPEG preview per media item (≤ 8 MB each) so the checkout page shows your blur instead of the default: ```bash curl -X POST "https://www.dropfans.io/api/external/drops/PRODUCT_ID/previews" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -F "previewBlob_VAULT_ITEM_A=@teaser_a.jpg;type=image/jpeg" \ -F "blurMeta_VAULT_ITEM_A=partial" ``` The response is `{ "success": true, "updated": n }` — compare `updated` with the number you sent; non-JPEG or oversized parts are skipped silently. ## 4. Share the link - **Web**: the `buyUrl` from the create response — `https://www.dropfans.io/buy/`. - **Telegram**: take `telegram.buyTemplate` from [GET /links](https://www.dropfans.io/developers/reference/get-links.md) and substitute the product id — it opens the Dropfans Mini App straight on the checkout. When `telegram` is `null` the bot is not configured; fall back to the web URL. See [Links & deep links](https://www.dropfans.io/developers/guides/links-and-deep-links.md). ## 5. Poll for the sale `POST /drops/check-status` takes up to 200 product ids and returns a map of the ones that sold: ```bash tab="curl" curl -X POST "https://www.dropfans.io/api/external/drops/check-status" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "productIds": ["PRODUCT_ID"] }' ``` ```javascript tab="Node" const res = await fetch('https://www.dropfans.io/api/external/drops/check-status', { method: 'POST', headers: { Authorization: 'Bearer ' + process.env.DROPFANS_API_KEY, 'Content-Type': 'application/json', }, body: JSON.stringify({ productIds: [productId] }), }); const { sales } = await res.json(); if (sales[productId]) console.log('sold for', sales[productId].saleAmountCents, 'cents'); ``` ```python tab="Python" res = requests.post( "https://www.dropfans.io/api/external/drops/check-status", headers=HEADERS, json={"productIds": [drop["productId"]]}, ) sales = res.json()["sales"] # unsold ids are simply absent ``` Cadence: check on demand (when the buyer says "paid") plus a periodic sweep every 1–5 minutes over your open drops, chunked at 200 ids. Unsold ids are omitted from the map — absence means "not sold yet", not an error. > [!WARNING] check-status does not know about refunds > A refunded sale still reports `paid: true` here. For money truth, reconcile against [GET /earnings](https://www.dropfans.io/developers/reference/get-earnings.md), which excludes refunds and chargebacks. ## 6. Read the money `saleAmountCents` is the **gross** charge in cents. Net (what the creator keeps), per-type totals and the transaction log live on [GET /earnings](https://www.dropfans.io/developers/reference/get-earnings.md) — see [Earnings & balance](https://www.dropfans.io/developers/guides/earnings-and-balance.md). Next: [Links & deep links](https://www.dropfans.io/developers/guides/links-and-deep-links.md) or [Earnings & balance](https://www.dropfans.io/developers/guides/earnings-and-balance.md). --- Previous: [Agencies](https://www.dropfans.io/developers/concepts/agencies.md) · Next: [Upload images, audio and video](https://www.dropfans.io/developers/guides/upload-media.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # Upload images, audio and video > Small files go through one multipart call; videos go straight to the CDN with a three-step TUS flow. - Source: https://www.dropfans.io/developers/guides/upload-media - Section: Guides - OpenAPI: https://www.dropfans.io/developers/openapi.json Three media types, two paths: images and audio go through one multipart request; video is too big for that and uploads straight to the CDN. ## Images — one multipart call Send `fileType=image`, `originalName`, and **both** an original (`displayFile`) and a thumbnail (`thumbnailFile`) part. The whole multipart body must stay under 4 MB — compress client-side before sending. ```bash tab="curl" curl -X POST "https://www.dropfans.io/api/external/vault" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -F "fileType=image" \ -F "originalName=beach.jpg" \ -F "displayFile=@beach.jpg" \ -F "thumbnailFile=@beach_thumb.jpg" ``` ```javascript tab="Node" import { openAsBlob } from 'node:fs'; const form = new FormData(); form.set('fileType', 'image'); form.set('originalName', 'beach.jpg'); form.set('displayFile', await openAsBlob('beach.jpg'), 'beach.jpg'); form.set('thumbnailFile', await openAsBlob('beach_thumb.jpg'), 'beach_thumb.jpg'); const res = await fetch('https://www.dropfans.io/api/external/vault', { method: 'POST', headers: { Authorization: 'Bearer ' + process.env.DROPFANS_API_KEY }, body: form, }); const { item } = await res.json(); ``` ```python tab="Python" import os, requests HEADERS = {"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"} res = requests.post( "https://www.dropfans.io/api/external/vault", headers=HEADERS, data={"fileType": "image", "originalName": "beach.jpg"}, files={ "displayFile": open("beach.jpg", "rb"), "thumbnailFile": open("beach_thumb.jpg", "rb"), }, ) item = res.json()["item"] ``` The response is `{ "success": true, "item": { … } }`. The item starts `PENDING` — see [Moderation & statuses](https://www.dropfans.io/developers/concepts/moderation-and-statuses.md). ## Audio — same call, one file part Send `fileType=audio` with a single `file` part (≤ 20 MB, else `413`) and an optional `durationSeconds` (capped at 3600 seconds): ```bash curl -X POST "https://www.dropfans.io/api/external/vault" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -F "fileType=audio" \ -F "originalName=voice-note.ogg" \ -F "durationSeconds=42" \ -F "file=@voice-note.ogg" ``` ## Video — three steps via TUS Videos (up to 500 MB) never touch our servers directly — you upload straight to the Bunny CDN with the resumable [TUS protocol](https://tus.io/). ### Step 1 — start ```bash curl -X POST "https://www.dropfans.io/api/external/vault/video-upload" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "originalName": "clip.mp4", "fileSize": 104857600 }' ``` ```json { "videoId": "9f2c…", "tusEndpoint": "https://video.bunnycdn.com/tusupload", "libraryId": 572783, "signature": "…", "expires": 1755640000000, "completionToken": "…" } ``` Keep `completionToken` — it is valid for 8 hours and step 3 requires it. ### Step 2 — TUS upload to the CDN Send the file to `tusEndpoint` with these TUS metadata headers: `AuthorizationSignature` (= `signature`), `AuthorizationExpire` (= `expires`), `VideoId` (= `videoId`), `LibraryId` (= `libraryId`). With `tus-js-client`: ```javascript import * as tus from 'tus-js-client'; import fs from 'node:fs'; await new Promise((resolve, reject) => { const upload = new tus.Upload(fs.createReadStream('clip.mp4'), { endpoint: creds.tusEndpoint, chunkSize: 50 * 1024 * 1024, // 50 MB chunks retryDelays: [0, 1500, 4000, 8000, 15000], headers: { AuthorizationSignature: creds.signature, AuthorizationExpire: String(creds.expires), VideoId: creds.videoId, LibraryId: String(creds.libraryId), }, metadata: { filetype: 'video/mp4', title: 'clip.mp4' }, uploadSize: fs.statSync('clip.mp4').size, onError: reject, onSuccess: resolve, }); upload.start(); }); ``` ### Step 3 — complete ```bash curl -X POST "https://www.dropfans.io/api/external/vault/video-upload/complete" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "videoId": "9f2c…", "originalName": "clip.mp4", "completionToken": "…" }' ``` A `409` means the CDN has not surfaced the finished upload yet — **this is expected**, retry on a ladder of roughly 2 s, 4 s, 6 s, 10 s, 15 s, 20 s, 30 s (give up after ~8 attempts). Retry `409` and `5xx` only; a `403` means the completion token expired (restart from step 1) and a `413` means the file was over the cap. The call is idempotent — completing twice returns the same item. ### Then poll processing [POST /vault/video-status](https://www.dropfans.io/developers/reference/video-status.md) (up to 50 ids) reports `{ isReady, isProcessing, isFailed }` per video. Poll every 10–15 seconds until `isReady` or `isFailed`. Processing is separate from moderation — after `isReady`, the item still clears review like everything else. ## Folders and tags - Create folders with `POST /vault/folders` (`{ "name": … }`, ≤ 50 chars, duplicate names `409`); move items with `PATCH /vault/{id}/folder`; pass `folderId` at upload time to file directly. - Tag items with `PATCH /vault/{id}/tags` — `{ "contentTags": [ … ] }`, up to 50 tags of ≤ 64 characters each. Next: [Sell a drop end-to-end](https://www.dropfans.io/developers/guides/sell-a-drop.md) or [Moderation & statuses](https://www.dropfans.io/developers/concepts/moderation-and-statuses.md). --- Previous: [Sell a drop end-to-end](https://www.dropfans.io/developers/guides/sell-a-drop.md) · Next: [Links & Telegram deep links](https://www.dropfans.io/developers/guides/links-and-deep-links.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # Links & Telegram deep links > Canonical profile, tip, subscribe and buy URLs for a creator — on the web and inside the Dropfans Telegram Mini App. - Source: https://www.dropfans.io/developers/guides/links-and-deep-links - Section: Guides - OpenAPI: https://www.dropfans.io/developers/openapi.json Never assemble Dropfans URLs by hand — [GET /links](https://www.dropfans.io/developers/reference/get-links.md) returns every canonical link for the creator behind your key, in one call. ## The response ```bash curl "https://www.dropfans.io/api/external/links" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ```json { "username": "ava", "web": { "profile": "https://www.dropfans.io/u/ava", "tip": "https://www.dropfans.io/u/ava?tip=1", "tipTemplate": "https://www.dropfans.io/u/ava?tip={usd}", "subscribe": "https://www.dropfans.io/u/ava?subscribe=1", "buyTemplate": "https://www.dropfans.io/buy/{productId}" }, "telegram": { "bot": "dropfansbot", "profile": "https://t.me/dropfansbot/app?startapp=p_ava", "tip": "https://t.me/dropfansbot/app?startapp=t_ava", "tipTemplate": "https://t.me/dropfansbot/app?startapp=pt_{cents}_ava", "subscribe": "https://t.me/dropfansbot/app?startapp=s_ava", "spin": "https://t.me/dropfansbot/app?startapp=w_ava", "buyTemplate": "https://t.me/dropfansbot/app?startapp=b_{productId}" } } ``` A creator without a username returns `409 { "error": …, "code": "username_required" }` — ask them to set one in their profile first. ## Web links - `/u/` — the profile. - `/u/?tip=1` — profile with the tip sheet auto-opened. - `/u/?tip=` — tip sheet **prefilled** with that dollar amount, when it is within $5–$750; out-of-range values open the sheet unfilled. - `/u/?subscribe=1` — profile with the subscribe sheet open. - `/buy/` — a drop’s checkout page. ## Telegram Mini App links `t.me//app?startapp=` opens the Dropfans Mini App inside Telegram. The `startapp` prefixes: | Prefix | Opens | | --- | --- | | `p_` | the creator’s profile | | `t_` | the tip sheet | | `pt__` | the tip sheet prefilled — **amount in cents** | | `s_` | the subscribe sheet | | `w_` | the reward-wheel spin | | `b_` | a drop’s checkout | > [!IMPORTANT] Never hardcode the bot username > When the `telegram` block is `null`, the Telegram bot is not configured for this environment — fall back to the web links. Always build Telegram links from the templates in the response, never from a bot name you saw once. ## Templating into a message ```javascript const links = await (await fetch('https://www.dropfans.io/api/external/links', { headers: { Authorization: 'Bearer ' + process.env.DROPFANS_API_KEY }, })).json(); const buyLink = (links.telegram ?? links.web).buyTemplate.replace('{productId}', productId); const message = 'new set just dropped — ' + buyLink; ``` Note the units difference between the two tip templates: `web.tipTemplate` takes **dollars** (`{usd}`), `telegram.tipTemplate` takes **cents** (`{cents}`). $10 is `?tip=10` on the web and `pt_1000_ava` in Telegram. Next: [Sell a drop end-to-end](https://www.dropfans.io/developers/guides/sell-a-drop.md) or [Post to the For You feed](https://www.dropfans.io/developers/guides/post-to-feed.md). --- Previous: [Upload images, audio and video](https://www.dropfans.io/developers/guides/upload-media.md) · Next: [Post to the For You feed](https://www.dropfans.io/developers/guides/post-to-feed.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # Post to the For You feed > Text, image and drop posts from a script or an agent — with the same moderation and daily cap as the composer. - Source: https://www.dropfans.io/developers/guides/post-to-feed - Section: Guides - OpenAPI: https://www.dropfans.io/developers/openapi.json One endpoint, one API key. Publish text and image posts to the creator’s Dropfans feed from a script, a scheduler, or an AI assistant — content runs the same moderation as the composer, so nothing skips review. ## Publish a post The smallest useful call is a text post: ```bash 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?" }' ``` | Field | Type | Notes | | --- | --- | --- | | `caption` | string | Post text, up to 2000 characters. Required for a text post. | | `kind` | string | `"TEXT"` (default), `"MEDIA"`, `"SUBSCRIPTION"` or `"COMMUNITY"`. | | `media[]` | array | Required for MEDIA. Up to 10 items: `{ vaultItemId, isPaid, price, order }`. | | `productId` | string | Attach one of your approved drops. Forces kind DROP. | | `scheduledAt` | ISO date | Publish later — at least 1 minute ahead, at most 30 days. | A `201` returns `{ id, status, pending, scheduledAt, url }` — `status: "APPROVED"` means it is already live; `"PENDING"` means it is waiting on review. Caption house style: short (under ~80 characters), lowercase, second person, present tense, almost no emoji — ending on a cheap question works best. See the [agent prompt](https://www.dropfans.io/developers/build-with-ai/agent-prompt.md) for the same rules in paste-into-an-assistant form. ## Images from your vault Image posts reference content already in the vault, so nothing is uploaded twice. List the vault, pick an item whose `moderationStatus` is APPROVED, and reference its id. Mark an item paid to turn it into a blurred PPV unlock (minimum $5): ```bash # 1. find an approved image in your vault curl "https://www.dropfans.io/api/external/vault?limit=5" \ -H "Authorization: Bearer $DROPFANS_API_KEY" # 2. post it — free teaser first, paid unlock second curl -X POST "https://www.dropfans.io/api/external/posts" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "kind": "MEDIA", "caption": "can i show you??", "media": [ { "vaultItemId": "VAULT_ITEM_A", "isPaid": false, "order": 0 }, { "vaultItemId": "VAULT_ITEM_B", "isPaid": true, "price": 10, "order": 1 } ] }' ``` > [!NOTE] > Paid images posted through the API get a standard blur. To hand-tune the teaser blur, use the composer in the dashboard. ## Moderation Nothing here bypasses review. Captions run the prohibited-word filter on the way in, and images ride the NSFW pipeline. Anything still in review comes back `PENDING` and goes live by itself once it clears — **a normal outcome, not an error**. Poll until it flips: ```bash curl "https://www.dropfans.io/api/external/posts?status=PENDING" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` Handle the two rejections precisely: - `422 { "error": …, "matchedWord": "…" }` — the caption tripped the word filter. Rewrite the caption and retry **once**. - `429` — the daily cap of 5 posts per rolling 24 hours (the composer’s cap, enforced here too). The `Retry-After` header says how many seconds to wait. Stop; never retry a 429 in a loop. Bursts inside the day are fine — only volume is capped. ## Scheduling Pass `scheduledAt` (ISO-8601, at least 1 minute ahead, at most 30 days out) and the post publishes itself — moderation runs at submit time, so a clean scheduled post flips to `live` exactly on time. Cancel a scheduled post with `DELETE /posts/{id}` before it publishes. Next: [Earnings & balance](https://www.dropfans.io/developers/guides/earnings-and-balance.md) or [Agent prompt](https://www.dropfans.io/developers/build-with-ai/agent-prompt.md). --- Previous: [Links & Telegram deep links](https://www.dropfans.io/developers/guides/links-and-deep-links.md) · Next: [Earnings & balance](https://www.dropfans.io/developers/guides/earnings-and-balance.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # Earnings & balance > Mirror the creator’s dashboard numbers and payout buckets inside your own product. - Source: https://www.dropfans.io/developers/guides/earnings-and-balance - Section: Guides - OpenAPI: https://www.dropfans.io/developers/openapi.json Two read-only endpoints cover the money picture: [GET /earnings](https://www.dropfans.io/developers/reference/get-earnings.md) mirrors the dashboard’s revenue view, [GET /balance](https://www.dropfans.io/developers/reference/get-balance.md) shows where the payout money sits. ## Earnings ```bash tab="curl" curl "https://www.dropfans.io/api/external/earnings?startDate=2026-08-01&endDate=2026-08-19&tz=Europe/Stockholm" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ```javascript tab="Node" const params = new URLSearchParams({ startDate: '2026-08-01', endDate: '2026-08-19', tz: 'Europe/Stockholm', }); const res = await fetch('https://www.dropfans.io/api/external/earnings?' + params, { headers: { Authorization: 'Bearer ' + process.env.DROPFANS_API_KEY }, }); const { stats, chart, transactions } = await res.json(); ``` ```python tab="Python" import os, requests res = requests.get( "https://www.dropfans.io/api/external/earnings", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, params={"startDate": "2026-08-01", "endDate": "2026-08-19", "tz": "Europe/Stockholm"}, ) data = res.json() ``` - `startDate` and `endDate` are **required**, `YYYY-MM-DD`. `tz` is an IANA timezone; days bucket in that zone (invalid values silently fall back to UTC). Pass the creator’s zone and your "today" matches their dashboard. - **Bucketing**: spans up to 31 days chart by `day`, up to 90 by `week` (weeks start Monday), longer by `month`. The response tells you which via `chart.groupBy`. - **Net vs gross**: `stats.totalEarningsCents` is net (what the creator keeps), `stats.grossEarningsCents` is what buyers paid; `typeTotals` splits both by `drop` / `tip` / `subscription`. Chart `values` are gross. All cents. Refunds and chargebacks are excluded throughout. - **Transactions** are always the 50 most recent in the range — there is no pagination. Need older ones? Narrow the date range. ## Balance ```json { "currency": "USD", "pending": 412.5, "available": 180, "processing": 0, "paidOut": 12750.25 } ``` USD **dollars** (unlike earnings’ cents). The buckets: `pending` — earned but still inside the payout hold; `available` — released and payable; `processing` — inside a payout run right now; `paidOut` — lifetime total paid. Agency-managed accounts can go negative — render signed. See [Money & units](https://www.dropfans.io/developers/concepts/money-and-units.md) for the payout model. ## Caching advice Earnings queries aggregate a lot of rows — do not call them per page view. Cache per creator for 60 seconds or more, single-flight concurrent requests for the same range, and refresh on demand when you know something changed (a sale you saw via [check-status](https://www.dropfans.io/developers/reference/check-drop-status.md)). A dashboard that polls once a minute per creator stays comfortably inside every [rate tier](https://www.dropfans.io/developers/concepts/rate-limits.md). Next: [Money & units](https://www.dropfans.io/developers/concepts/money-and-units.md) or [Webhooks & polling](https://www.dropfans.io/developers/webhooks/overview.md). --- Previous: [Post to the For You feed](https://www.dropfans.io/developers/guides/post-to-feed.md) · Next: [Webhooks (coming soon) & polling today](https://www.dropfans.io/developers/webhooks/overview.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # Webhooks (coming soon) & polling today > Outbound webhooks for third-party apps are planned; until then, these polling patterns cover sales, video processing and moderation. - Source: https://www.dropfans.io/developers/webhooks/overview - Section: Webhooks - OpenAPI: https://www.dropfans.io/developers/openapi.json > [!NOTE] Webhooks are not available yet > Outbound webhooks for third-party apps are planned but not shipped. Everything in the first half of this page is a preview of the intended contract and is **subject to change** — build against the polling patterns below for now, and watch the [changelog](https://www.dropfans.io/developers/changelog.md). ## Planned events | Event | Fires when | | --- | --- | | `drop.paid` | a buyer completes checkout on a drop | | `tip.received` | a tip lands | | `subscription.started` | a fan subscribes | | `subscription.canceled` | a subscription ends | | `vault.item.moderated` | a vault item leaves PENDING | | `post.moderated` | a post leaves PENDING | Planned envelope — a JSON POST to your endpoint: ```json { "id": "evt_…", "type": "drop.paid", "timestamp": "2026-08-19T12:00:00Z", "data": { … } } ``` Deliveries would carry an HMAC signature header for verification. Again: names, shapes and semantics may all change before launch. ## Today: the three polling patterns Until webhooks ship, three loops cover the same ground. Budget them against your [rate tier](https://www.dropfans.io/developers/concepts/rate-limits.md) — the day windows are 5,000 requests (personal) and 50,000 per key (app). ### 1. Sales — check-status Poll [POST /drops/check-status](https://www.dropfans.io/developers/reference/check-drop-status.md) with the product ids you are waiting on — up to 200 per call, chunked. Check on demand (the buyer says "paid") plus a periodic sweep every 1–5 minutes. The maths: one sweep a minute is 1,440 requests/day per 200-drop chunk — fine on the app tier, a quarter of a personal day budget, so personal keys should sweep every 5 minutes instead. ### 2. Video processing — video-status While a video is processing after [upload](https://www.dropfans.io/developers/guides/upload-media.md), poll [POST /vault/video-status](https://www.dropfans.io/developers/reference/video-status.md) (up to 50 ids) every 10–15 seconds until `isReady` or `isFailed`. Processing is minutes, not hours — stop polling on any terminal state. ### 3. Moderation — posts and vault - Posts: [GET /posts?status=PENDING](https://www.dropfans.io/developers/reference/list-posts.md) or [GET /posts/{id}](https://www.dropfans.io/developers/reference/get-post.md) every 30–60 seconds while something is pending. - Vault items: [GET /vault?includePending=true](https://www.dropfans.io/developers/reference/list-vault.md) every few minutes after a batch upload, until nothing is PENDING. Back off when nothing is pending — a moderation poll with an empty pending set is a wasted request. All three loops together, run sensibly, fit inside the app tier with room to spare. Next: [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md) or [Sell a drop end-to-end](https://www.dropfans.io/developers/guides/sell-a-drop.md). --- Previous: [Earnings & balance](https://www.dropfans.io/developers/guides/earnings-and-balance.md) · Next: [Build with AI](https://www.dropfans.io/developers/build-with-ai/overview.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # Build with AI > Feed an agent the whole API in one shot: llms.txt, llms-full.txt, openapi.json, and a Markdown twin of every page. - Source: https://www.dropfans.io/developers/build-with-ai/overview - Section: Build with AI - OpenAPI: https://www.dropfans.io/developers/openapi.json These docs are published in machine formats alongside the HTML, so an assistant can ingest the whole API without scraping: | File | What it is | | --- | --- | | [/developers/llms.txt](https://www.dropfans.io/developers/llms.txt) | The documentation index — every page with a one-line description and its Markdown URL. Start here. | | [/developers/llms-full.txt](https://www.dropfans.io/developers/llms-full.txt) | Every page, full text, one file. For models with a large context window. | | [/developers/openapi.json](https://www.dropfans.io/developers/openapi.json) | The OpenAPI 3.1 contract — every operation, schema, error and code sample. Feed it to a client generator, Postman, or an agent. | | Any docs URL + `.md` | Every page has a Markdown twin — append `.md` to its URL. This page is at `/developers/build-with-ai/overview.md`. | Responses from `/developers` pages also carry `Link` discovery headers pointing at `llms.txt`, `llms-full.txt` and `openapi.json`, so tooling can find them without prior knowledge. ## Recipes - **Claude / ChatGPT** — paste the [agent prompt](https://www.dropfans.io/developers/build-with-ai/agent-prompt.md); it tells the model to fetch `openapi.json` itself, so it stays correct as the API grows. Keep the key in the tool's environment or a connector secret, not in the chat. - **Claude Code / other coding agents** — point it at `https://www.dropfans.io/developers/llms.txt` and let it pull the `.md` pages it needs; keep `DROPFANS_API_KEY` in the shell environment. - **Cursor** — add `https://www.dropfans.io/developers/llms-full.txt` as docs (`@docs`), then reference it while writing your integration. - **Codegen** — `openapi.json` validates as OpenAPI 3.1 and carries `x-codeSamples`, so generated clients and rendered references both come out usable. > [!WARNING] Never paste an API key into a shared or public chat > A key is full account access for that creator. Use an assistant that keeps secrets in its own environment, and rotate any key that ever lands in a transcript. No MCP server yet — when one exists it will be announced in the [changelog](https://www.dropfans.io/developers/changelog.md). Next: [Agent prompt](https://www.dropfans.io/developers/build-with-ai/agent-prompt.md) or [API reference](https://www.dropfans.io/developers/reference/overview.md). --- Previous: [Webhooks (coming soon) & polling today](https://www.dropfans.io/developers/webhooks/overview.md) · Next: [Agent prompt](https://www.dropfans.io/developers/build-with-ai/agent-prompt.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # Agent prompt > Copy-paste prompts that make an assistant post to the feed or sell drops correctly. - Source: https://www.dropfans.io/developers/build-with-ai/agent-prompt - Section: Build with AI - OpenAPI: https://www.dropfans.io/developers/openapi.json Two prompts, ready to paste into Claude, ChatGPT or any tool-using assistant. Both make the model fetch the OpenAPI contract itself, so they stay correct as the API grows. Replace `` privately — never in a shared chat. ## The posting agent Lets an assistant publish to the creator's For You feed: ```text You can post to my Dropfans For You feed. The API contract is at https://www.dropfans.io/developers/openapi.json — fetch it first and follow it exactly. The full documentation index is at https://www.dropfans.io/developers/llms.txt. Authenticate with: Authorization: Bearer House rules: - Captions must be short (under ~80 characters), lowercase, second person, present tense. Almost never use emoji. Ending on a cheap question works best. - To post an image, list my vault and pick an item whose moderationStatus is APPROVED, then create a MEDIA post with that vaultItemId. - A PENDING response is normal, not a failure. Poll GET /api/external/posts/{id} until it turns APPROVED. - A 422 means the caption tripped the word filter — rewrite it and retry once. - A 429 means we hit the daily cap of 5 posts — stop, don't retry in a loop. ``` ## The selling agent Lets an assistant package vault content into paid drops and report back sales: ```text You can sell content from my Dropfans vault. The API contract is at https://www.dropfans.io/developers/openapi.json — fetch it first and follow it exactly. The full documentation index is at https://www.dropfans.io/developers/llms.txt. Authenticate with: Authorization: Bearer The flow: 1. List my vault (GET /api/external/vault) and only use items whose moderationStatus is APPROVED. 2. Create a drop with POST /api/external/drops — at most 10 vault items per drop. The price is USD dollars: either 0 (free) or between $5 and $750. Never invent a price — ask me if unsure. 3. Give me the buyUrl from the response, or build the Telegram link by taking the telegram.buyTemplate from GET /api/external/links and substituting the productId. If the telegram block is null, use the web buyUrl only. 4. To check what sold, poll POST /api/external/drops/check-status with the productIds — at most 200 ids per call, chunk larger lists. Never below the $5 minimum on a paid drop, never above $750, and never paste my API key into a shared or public chat. ``` Next: [Build with AI](https://www.dropfans.io/developers/build-with-ai/overview.md) or [Post to the For You feed](https://www.dropfans.io/developers/guides/post-to-feed.md). --- Previous: [Build with AI](https://www.dropfans.io/developers/build-with-ai/overview.md) · Next: [API reference overview](https://www.dropfans.io/developers/reference/overview.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # API reference overview > Base URL, authentication, conventions and the shape of every response — read this before the endpoint pages. - Source: https://www.dropfans.io/developers/reference/overview - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json ``` https://www.dropfans.io ``` Every endpoint lives under `/api/external/` on that host. This page collects the conventions the endpoint pages assume. > [!TIP] > The whole contract is also published as [OpenAPI 3.1](https://www.dropfans.io/developers/openapi.json) — ready to feed to a client generator, Postman, or a coding agent. See [Build with AI](https://www.dropfans.io/developers/build-with-ai/overview.md). ## Authentication Every request carries `Authorization: Bearer dpfn_…`. A key is bound to one creator and grants the full surface — details in [Authentication & API keys](https://www.dropfans.io/developers/concepts/authentication.md). ## Requests JSON bodies with `Content-Type: application/json` everywhere except the two upload endpoints ([upload a vault item](https://www.dropfans.io/developers/reference/upload-vault-item.md), [attach previews](https://www.dropfans.io/developers/reference/attach-drop-previews.md)), which take `multipart/form-data`. Malformed JSON returns `400 { "error": "Invalid JSON body" }`. ## Ids and timestamps Ids are opaque strings (cuid format, e.g. `cmawq81x40001lb04xyz12abc`) — treat them as text, never parse them. Timestamps are ISO-8601 in UTC, e.g. `2026-08-19T12:00:00.000Z`. ## Pagination Two styles, both `page`/`limit` based: vault lists put `hasMore`/`total`/`page`/`limit` at the top level (limit defaults to the max, 50); post lists nest them under `pagination` (default 20). Iterate on `hasMore`, not on short pages. Two batch endpoints truncate oversized id arrays silently — details and caps in [Pagination & batch limits](https://www.dropfans.io/developers/concepts/pagination.md). ## Errors Baseline `{ "error": "…" }` prose; gateway errors add a stable `code` (`unauthorized`, `app_suspended`, `rate_limited`); the word filter adds `matchedWord` (posts, 422) or `field` + `matchedWord` (drops, 400). The full catalogue — including the three success envelopes — is in [Errors](https://www.dropfans.io/developers/concepts/errors.md). Each endpoint page lists its exact error rows. ## Rate-limit headers Every response reports the budget: | Header | Meaning | | --- | --- | | `X-RateLimit-Tier` | `personal`, `app` or `first_party` | | `X-RateLimit-Limit` / `-Remaining` / `-Reset` | the minute window | | `X-RateLimit-Limit-Day` / `-Remaining-Day` / `-Reset-Day` | the UTC-day window | | `Retry-After` | on `429`, seconds to wait | Tiers and defaults: [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md). ## Units Prices go **in** as USD dollars (drops: `0` or 5–750); earnings and sales come **out** as integer cents; balance is dollars. The per-endpoint table is in [Money & units](https://www.dropfans.io/developers/concepts/money-and-units.md). ## Versioning Changes are additive — new fields and new endpoints, announced in the [changelog](https://www.dropfans.io/developers/changelog.md). There is no version header. Ignore fields you do not recognise, and never assume a response has exactly the fields listed today. Next: [Get current creator](https://www.dropfans.io/developers/reference/get-me.md) or [Errors](https://www.dropfans.io/developers/concepts/errors.md). --- Previous: [Agent prompt](https://www.dropfans.io/developers/build-with-ai/agent-prompt.md) · Next: [Which creator this API key belongs to](https://www.dropfans.io/developers/reference/get-me.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/me — Which creator this API key belongs to > Returns the creator account behind the key, plus metadata about the key itself. - Source: https://www.dropfans.io/developers/reference/get-me - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Returns the creator account behind the key, plus metadata about the key itself. Call it once at startup to confirm the key works and to learn the creator's `id`, `username` and `accountType` — posting to the For You feed needs CREATOR or AGENCY, and every link surface needs a `username`. The `key` block tells you which integration the key was minted for and its rate-limit tier, matching the `X-RateLimit-Tier` header. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the creator's dashboard (Vault → API Connect). One key = one creator. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Responses ### 200 The key’s creator account. | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The creator’s Dropfans user id. Stable — safe to key your own records on. | | `username` | string \\| null | | The creator’s public @handle, without the @. Null until the creator picks one — several link surfaces (GET /api/external/links, the post `url`) need it. | | `name` | string \\| null | | Display name. Null when unset. | | `image` | string \\| null | | Profile image URL. Null when the creator has no avatar. | | `accountType` | `CONSUMER` \\| `CREATOR` \\| `AGENCY` | | Account class of the key’s owner. Posting to the For You feed requires CREATOR or AGENCY. | | `key` | object | | Metadata about the API key used on this request (added 2026-08-19). | | `key.name` | string | | The key’s label, chosen at mint (defaults to the app name, or "Personal"). | | `key.app` | object \\| null | | The integration this key was minted for. Null for a Personal key. | | `key.app.slug` | string | | Stable app identifier. | | `key.app.name` | string | | App display name. | | `key.tier` | `personal` \\| `app` \\| `first_party` | | Rate-limit tier of this key — the same value the X-RateLimit-Tier response header carries. | ```json { "id": "clx2f8a1b0001qw3k", "username": "valeria", "name": "Valeria", "image": "https://cdn.dropfans.io/valeria/profile.jpg", "accountType": "CREATOR", "key": { "name": "KVIQA", "app": { "slug": "kviqa", "name": "KVIQA" }, "tier": "app" } } ``` ## Errors | Status | Body | When | |---|---|---| | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | ## Notes The `accountType` and `key` fields were added with the public launch (2026-08-19); `id`, `username`, `name` and `image` are unchanged from the original response. ## Rate limiting Per-key fixed windows by tier (Personal 60/min · 5,000/day; approved apps 300/min · 50,000/day; Dropfans-operated integrations exempt). Read the live values from X-RateLimit-Tier, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and their -Day variants; a 429 carries Retry-After and `{"error":"Rate limit exceeded","code":"rate_limited"}`. See [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md). ## Code samples ### curl ```bash curl "https://www.dropfans.io/api/external/me" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/me`, { headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, }, }); console.log(await res.json()); ``` ### Python ```python import os import requests res = requests.get( "https://www.dropfans.io/api/external/me", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [API reference overview](https://www.dropfans.io/developers/reference/overview.md) · Next: [Read the creator’s timezone](https://www.dropfans.io/developers/reference/get-timezone.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/timezone — Read the creator’s timezone > Returns the creator's IANA timezone, defaulting to `"UTC"` when they never set one. - Source: https://www.dropfans.io/developers/reference/get-timezone - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Returns the creator's IANA timezone, defaulting to `"UTC"` when they never set one. Use it as the `tz` parameter on [GET /api/external/earnings](https://www.dropfans.io/developers/reference/get-earnings.md) so your day buckets line up with what the creator sees on their own dashboard. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the creator's dashboard (Vault → API Connect). One key = one creator. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Responses ### 200 The creator’s timezone. | Field | Type | Required | Description | |---|---|---|---| | `timezone` | string | yes | IANA timezone name (e.g. "Europe/Stockholm"). Defaults to "UTC" when the creator never set one. | ```json { "timezone": "Europe/Stockholm" } ``` ## Errors | Status | Body | When | |---|---|---| | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | ## Rate limiting Per-key fixed windows by tier (Personal 60/min · 5,000/day; approved apps 300/min · 50,000/day; Dropfans-operated integrations exempt). Read the live values from X-RateLimit-Tier, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and their -Day variants; a 429 carries Retry-After and `{"error":"Rate limit exceeded","code":"rate_limited"}`. See [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md). ## Code samples ### curl ```bash curl "https://www.dropfans.io/api/external/timezone" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/timezone`, { headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, }, }); console.log(await res.json()); ``` ### Python ```python import os import requests res = requests.get( "https://www.dropfans.io/api/external/timezone", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [Which creator this API key belongs to](https://www.dropfans.io/developers/reference/get-me.md) · Next: [Set the creator’s timezone](https://www.dropfans.io/developers/reference/update-timezone.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # PUT /api/external/timezone — Set the creator’s timezone > Sets the creator's timezone — the same setting their dashboard uses for day bucketing. - Source: https://www.dropfans.io/developers/reference/update-timezone - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Sets the creator's timezone — the same setting their dashboard uses for day bucketing. The value must be a valid IANA name (anything in `Intl.supportedValuesOf('timeZone')`). This changes what the creator sees on their own Dropfans dashboard too, not just your API reads — only call it when the creator asked for it. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the creator's dashboard (Vault → API Connect). One key = one creator. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Request body (application/json) | Field | Type | Required | Description | |---|---|---|---| | `timezone` | string | yes | IANA timezone name, e.g. "Europe/Stockholm". Abbreviations like "CET" are not accepted. | Example — Set to Stockholm time: ```json { "timezone": "Europe/Stockholm" } ``` ## Responses ### 200 The saved timezone, echoed. | Field | Type | Required | Description | |---|---|---|---| | `timezone` | string | yes | IANA timezone name (e.g. "Europe/Stockholm"). Defaults to "UTC" when the creator never set one. | ```json { "timezone": "Europe/Stockholm" } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"Invalid timezone"}` | Missing, non-string, or not a recognised IANA timezone name. | | 400 | `{"error":"Invalid JSON body"}` | The request body is not valid JSON. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | ## Rate limiting Per-key fixed windows by tier (Personal 60/min · 5,000/day; approved apps 300/min · 50,000/day; Dropfans-operated integrations exempt). Read the live values from X-RateLimit-Tier, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and their -Day variants; a 429 carries Retry-After and `{"error":"Rate limit exceeded","code":"rate_limited"}`. See [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md). ## Code samples ### curl ```bash curl -X PUT "https://www.dropfans.io/api/external/timezone" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "timezone": "Europe/Stockholm" }' ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/timezone`, { method: 'PUT', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ "timezone": "Europe/Stockholm" }), }); console.log(await res.json()); ``` ### Python ```python import os import requests res = requests.put( "https://www.dropfans.io/api/external/timezone", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, json={ "timezone": "Europe/Stockholm", }, ) print(res.json()) ``` --- Previous: [Read the creator’s timezone](https://www.dropfans.io/developers/reference/get-timezone.md) · Next: [The creator’s payout balance](https://www.dropfans.io/developers/reference/get-balance.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/balance — The creator’s payout balance > The creator's payout balance buckets — the same numbers their Payouts page shows. - Source: https://www.dropfans.io/developers/reference/get-balance - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json The creator's payout balance buckets — the same numbers their Payouts page shows. > [!WARNING] Dollars, not cents > This endpoint returns USD **dollars**. The earnings and check-status endpoints return **cents**. Do not mix the units. Bucket semantics: `pending` is revenue still inside the chargeback hold window; `available` has cleared the hold and pays out in the next batch; `processing` is inside a payout currently being executed; `paidOut` is the lifetime total already paid. `available` can be negative for agency accounts. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the creator's dashboard (Vault → API Connect). One key = one creator. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Responses ### 200 Balance buckets in USD dollars. | Field | Type | Required | Description | |---|---|---|---| | `currency` | `USD` | yes | Always "USD". | | `pending` | number | yes | USD **dollars** (not cents — unlike earnings and check-status). Earnings still inside the chargeback hold window; not payable yet. | | `available` | number | yes | USD dollars. Cleared the hold — payable in the next payout batch. Can be negative for agency accounts. | | `processing` | number | yes | USD dollars. Included in a payout that is currently PROCESSING. | | `paidOut` | number | yes | USD dollars. Lifetime total of completed payouts. | ```json { "currency": "USD", "pending": 120.5, "available": 342.1, "processing": 0, "paidOut": 1875 } ``` ## Errors | Status | Body | When | |---|---|---| | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 500 | `{"error":"Failed to fetch balance"}` | Balance computation failed — retry later. | ## Rate limiting Per-key fixed windows by tier (Personal 60/min · 5,000/day; approved apps 300/min · 50,000/day; Dropfans-operated integrations exempt). Read the live values from X-RateLimit-Tier, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and their -Day variants; a 429 carries Retry-After and `{"error":"Rate limit exceeded","code":"rate_limited"}`. See [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md). ## Code samples ### curl ```bash curl "https://www.dropfans.io/api/external/balance" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/balance`, { headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, }, }); console.log(await res.json()); ``` ### Python ```python import os import requests res = requests.get( "https://www.dropfans.io/api/external/balance", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [Set the creator’s timezone](https://www.dropfans.io/developers/reference/update-timezone.md) · Next: [List your vault items — the source of media for posts](https://www.dropfans.io/developers/reference/list-vault.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/vault — List your vault items — the source of media for posts > Returns APPROVED items by default. Use an item's `id` as `vaultItemId` when creating a MEDIA post. Pass includePending=true to also see items still in moderation (their media is stripped). - Source: https://www.dropfans.io/developers/reference/list-vault - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Returns APPROVED items by default. Use an item's `id` as `vaultItemId` when creating a MEDIA post. Pass includePending=true to also see items still in moderation (their media is stripped). Every drop and every MEDIA post is built from vault item ids, so this list is the starting point of almost every flow. Items are newest first. The response also carries all of the creator's folders — but note the folders' `itemCount` here respects the moderation filter, while [GET /api/external/vault/folders](https://www.dropfans.io/developers/reference/list-folders.md) counts every non-hidden item, so the two can disagree for the same folder. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the creator's dashboard (Vault → API Connect). One key = one creator. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Query parameters | Field | Type | Required | Description | |---|---|---|---| | `page` | integer | | Page number, 1-based. | | `limit` | integer | | Items per page. Hard cap 50 — larger values are clamped, not rejected. | | `folderId` | string | | `"all"` (default), `"unfiled"`, or a folder id from the folders list. | | `includePending` | boolean | | Pass the literal string `true` to also get PENDING and FLAGGED items (REJECTED never appears). Those items come back with `filePath: ""` and `downloadUrl: null` — thumbnail only — and each item gains a `moderationStatus` field. | ## Responses ### 200 Vault items, folders and pagination. | Field | Type | Required | Description | |---|---|---|---| | `items` | VaultItem[] | yes | The requested page, newest first. | | `folders` | Folder[] | yes | All of the creator’s folders (unpaginated), with counts under the current moderation filter. | | `hasMore` | boolean | yes | True when more pages exist for the current filter. | | `total` | integer | yes | Total items matching the filter. | | `page` | integer | yes | Echoed page number (1-based). | | `limit` | integer | yes | Echoed page size. | ```json { "items": [ { "id": "clxv1a2b30001item", "fileName": "beach-set-01.jpg", "filePath": "https://cdn.dropfans.io/valeria/vault/1721990000-ab12cd.jpg", "thumbnailPath": "https://cdn.dropfans.io/valeria/thumbnails/1721990000-ab12cd.jpg", "fileType": "image", "fileSize": 482113, "durationSeconds": null, "bunnyStreamId": null, "createdAt": "2026-08-01T10:15:00.000Z", "folderId": "clxf0ld3r0001abcd", "contentTags": [ "beach", "bikini" ], "downloadUrl": null } ], "folders": [ { "id": "clxf0ld3r0001abcd", "name": "Beach set", "itemCount": 12 } ], "hasMore": true, "total": 128, "page": 1, "limit": 50 } ``` ## Errors | Status | Body | When | |---|---|---| | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 500 | `{"error":"Failed to fetch vault items"}` | Query failed — retry later. | ## Notes Signed URLs in the response (`downloadUrl`, audio `filePath`) are valid ~12 hours — re-list rather than caching them longer. `downloadUrl` is the only fetchable video source (the Stream `filePath` is DRM-locked); it is null for videos uploaded before dual-store existed. `filePath` is the empty string `""` for any non-APPROVED item. ## Rate limiting Per-key fixed windows by tier (Personal 60/min · 5,000/day; approved apps 300/min · 50,000/day; Dropfans-operated integrations exempt). Read the live values from X-RateLimit-Tier, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and their -Day variants; a 429 carries Retry-After and `{"error":"Rate limit exceeded","code":"rate_limited"}`. See [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md). ## Code samples ### curl ```bash curl "https://www.dropfans.io/api/external/vault?page=1&folderId=all" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/vault?page=1&folderId=all`, { headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, }, }); console.log(await res.json()); ``` ### Python ```python import os import requests res = requests.get( "https://www.dropfans.io/api/external/vault", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, params={"page": 1, "folderId": "all"}, ) print(res.json()) ``` --- Previous: [The creator’s payout balance](https://www.dropfans.io/developers/reference/get-balance.md) · Next: [Upload an image, small video or voice message](https://www.dropfans.io/developers/reference/upload-vault-item.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # POST /api/external/vault — Upload an image, small video or voice message > Uploads one file into the creator's vault as `multipart/form-data`. - Source: https://www.dropfans.io/developers/reference/upload-vault-item - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Uploads one file into the creator's vault as `multipart/form-data`. Three kinds, three shapes: **images** need two parts — a pre-compressed `displayFile` and a `thumbnailFile` (both JPEG); **audio** (voice messages) sends one `file` part (≤20MB, ≤60 min); **video** sends one `file` part — but the whole request must stay under ~4MB (a platform body cap rejects bigger requests before the app runs), so for real videos use the three-step TUS flow starting at [POST /api/external/vault/video-upload](https://www.dropfans.io/developers/reference/start-video-upload.md) instead. Every upload enters the same moderation pipeline as a dashboard upload: images are scored immediately, videos asynchronously, audio goes to human review (or auto-approves for trusted creators). The item only shows up in the default vault list once APPROVED. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the creator's dashboard (Vault → API Connect). One key = one creator. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Request body (multipart/form-data) | Field | Type | Required | Description | |---|---|---|---| | `fileType` | `image` \\| `video` \\| `audio` | yes | What you are uploading. | | `originalName` | string | yes | The file’s name, stored as fileName. | | `folderId` | string | | Optional folder to file the item into. | | `durationSeconds` | integer | | Audio only — the voice message length in seconds (capped at 3600). | | `displayFile` | file | | Images only (required): the pre-compressed JPEG display copy. | | `thumbnailFile` | file | | Images only (required): the JPEG thumbnail. | | `file` | file | | Video/audio only (required): the media file. Audio ≤20MB; video effectively ≤~4MB here — use the TUS flow for anything bigger. | Example — Image upload (display + thumbnail parts): ```json { "fileType": "image", "originalName": "beach-set-01.jpg", "displayFile": "@photo.jpg", "thumbnailFile": "@photo-thumb.jpg" } ``` Example — Voice message: ```json { "fileType": "audio", "originalName": "voice-note.ogg", "durationSeconds": 42, "file": "@voice-note.ogg" } ``` ## Responses ### 200 Uploaded. The item starts PENDING moderation (not included in this shape — poll the list with includePending=true, or wait for it to appear in the default list). | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | Always true. | | `item` | VaultItemUploaded | yes | The new vault item. | ```json { "success": true, "item": { "id": "clxv1a2b30001item", "fileName": "beach-set-01.jpg", "filePath": "https://cdn.dropfans.io/valeria/vault/1721990000-ab12cd.jpg", "thumbnailPath": "https://cdn.dropfans.io/valeria/thumbnails/1721990000-ab12cd.jpg", "fileType": "image", "fileSize": 482113, "durationSeconds": null, "bunnyStreamId": null, "createdAt": "2026-08-01T10:15:00.000Z" } } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"Missing required fields: fileType and originalName"}` | Either required form field is absent. | | 400 | `{"error":"Only images, videos and audio are allowed"}` | `fileType` is anything other than image, video or audio. | | 400 | `{"error":"Missing displayFile or thumbnailFile for image upload"}` | Image upload without both file parts. | | 400 | `{"error":"Missing video file"}` | fileType=video without a `file` part. | | 400 | `{"error":"Missing audio file"}` | fileType=audio without a `file` part. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 413 | `{"error":"Audio too large"}` | Audio file over 20MB. | | 500 | `{"error":"Failed to upload file"}` | Storage or moderation kickoff failed — retry later. | ## Notes The success shape here differs from the video-complete endpoint's item (that one adds `moderationStatus`, `moderationTags` and `aiEnhanced` and omits `durationSeconds`). OPTIONS on this path is an unauthenticated CORS preflight for browser uploads. ## Rate limiting Per-key fixed windows by tier (Personal 60/min · 5,000/day; approved apps 300/min · 50,000/day; Dropfans-operated integrations exempt). Read the live values from X-RateLimit-Tier, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and their -Day variants; a 429 carries Retry-After and `{"error":"Rate limit exceeded","code":"rate_limited"}`. See [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md). ## Code samples ### curl ```bash curl -X POST "https://www.dropfans.io/api/external/vault" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -F "fileType=image" \ -F "originalName=beach-set-01.jpg" \ -F "displayFile=@photo.jpg" \ -F "thumbnailFile=@photo-thumb.jpg" ``` ### Node ```javascript import { readFile } from 'node:fs/promises'; const form = new FormData(); form.append('fileType', "image"); form.append('originalName', "beach-set-01.jpg"); form.append('displayFile', new Blob([await readFile('photo.jpg')]), 'photo.jpg'); form.append('thumbnailFile', new Blob([await readFile('photo-thumb.jpg')]), 'photo-thumb.jpg'); const res = await fetch(`https://www.dropfans.io/api/external/vault`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, }, body: form, }); console.log(await res.json()); ``` ### Python ```python import os import requests res = requests.post( "https://www.dropfans.io/api/external/vault", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, files={ "displayFile": open("photo.jpg", "rb"), "thumbnailFile": open("photo-thumb.jpg", "rb"), }, data={ "fileType": "image", "originalName": "beach-set-01.jpg", }, ) print(res.json()) ``` --- Previous: [List your vault items — the source of media for posts](https://www.dropfans.io/developers/reference/list-vault.md) · Next: [Delete (hide) a vault item](https://www.dropfans.io/developers/reference/delete-vault-item.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # DELETE /api/external/vault/{id} — Delete (hide) a vault item > Soft-deletes a vault item you own: nothing is removed from storage, the item is hidden and disappears from every list. - Source: https://www.dropfans.io/developers/reference/delete-vault-item - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Soft-deletes a vault item you own: nothing is removed from storage, the item is hidden and disappears from every list. Idempotent — deleting an already-hidden item is a success, so blind retries are harmless. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the creator's dashboard (Vault → API Connect). One key = one creator. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Path parameters | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The vault item id. | ## Responses ### 200 Hidden (or already hidden). | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | Always true. | ```json { "success": true } ``` ## Errors | Status | Body | When | |---|---|---| | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 404 | `{"error":"Vault item not found"}` | No item with that id on this creator’s account (other creators’ ids also 404 — never 403). | | 500 | `{"error":"Failed to delete vault item"}` | Write failed — retry later. | ## Rate limiting Per-key fixed windows by tier (Personal 60/min · 5,000/day; approved apps 300/min · 50,000/day; Dropfans-operated integrations exempt). Read the live values from X-RateLimit-Tier, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and their -Day variants; a 429 carries Retry-After and `{"error":"Rate limit exceeded","code":"rate_limited"}`. See [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md). ## Code samples ### curl ```bash curl -X DELETE "https://www.dropfans.io/api/external/vault/$VAULT_ITEM_ID" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const vaultItemId = '…'; // from an earlier response const res = await fetch(`https://www.dropfans.io/api/external/vault/${vaultItemId}`, { method: 'DELETE', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, }, }); console.log(await res.json()); ``` ### Python ```python import os import requests vault_item_id = "…" # from an earlier response res = requests.delete( f"https://www.dropfans.io/api/external/vault/{vault_item_id}", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [Upload an image, small video or voice message](https://www.dropfans.io/developers/reference/upload-vault-item.md) · Next: [Move a vault item to a folder](https://www.dropfans.io/developers/reference/move-vault-item.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # PATCH /api/external/vault/{id}/folder — Move a vault item to a folder > Files a vault item into a folder, or unfiles it back to "All". - Source: https://www.dropfans.io/developers/reference/move-vault-item - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Files a vault item into a folder, or unfiles it back to "All". Omitting `folderId` (or sending `null`, `""`, an empty body — even a malformed body) unfiles the item. That leniency is deliberate: `{}` is the documented unfile signal. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the creator's dashboard (Vault → API Connect). One key = one creator. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Path parameters | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The vault item id. | ## Request body (application/json) | Field | Type | Required | Description | |---|---|---|---| | `folderId` | string \\| null | | Destination folder id. Omit/null/"" to unfile. | Example — Move into a folder: ```json { "folderId": "clxf0ld3r0001abcd" } ``` Example — Move back to All: ```json {} ``` ## Responses ### 200 Moved. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | Always true. | ```json { "success": true } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"folderId must be a string"}` | `folderId` is present but not a string (e.g. a number). | | 400 | `{"error":"This folder is managed automatically and cannot receive items."}` | The destination is a system folder. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 404 | `{"error":"Vault item not found"}` | No such item on this account. | | 404 | `{"error":"Folder not found"}` | No such folder on this account. | | 500 | `{"error":"Failed to move vault item"}` | Write failed — retry later. | ## Rate limiting Per-key fixed windows by tier (Personal 60/min · 5,000/day; approved apps 300/min · 50,000/day; Dropfans-operated integrations exempt). Read the live values from X-RateLimit-Tier, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and their -Day variants; a 429 carries Retry-After and `{"error":"Rate limit exceeded","code":"rate_limited"}`. See [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md). ## Code samples ### curl ```bash curl -X PATCH "https://www.dropfans.io/api/external/vault/$VAULT_ITEM_ID/folder" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "folderId": "clxf0ld3r0001abcd" }' ``` ### Node ```javascript const vaultItemId = '…'; // from an earlier response const res = await fetch(`https://www.dropfans.io/api/external/vault/${vaultItemId}/folder`, { method: 'PATCH', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ "folderId": "clxf0ld3r0001abcd" }), }); console.log(await res.json()); ``` ### Python ```python import os import requests vault_item_id = "…" # from an earlier response res = requests.patch( f"https://www.dropfans.io/api/external/vault/{vault_item_id}/folder", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, json={ "folderId": "clxf0ld3r0001abcd", }, ) print(res.json()) ``` --- Previous: [Delete (hide) a vault item](https://www.dropfans.io/developers/reference/delete-vault-item.md) · Next: [Replace a vault item’s content tags](https://www.dropfans.io/developers/reference/set-vault-item-tags.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # PATCH /api/external/vault/{id}/tags — Replace a vault item’s content tags > Replaces (not merges) the content tags on a vault item. Send the full list every time. - Source: https://www.dropfans.io/developers/reference/set-vault-item-tags - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Replaces (not merges) the content tags on a vault item. Send the full list every time. Tags are trimmed, de-duplicated, silently truncated to 64 characters each, and capped at 50 tags — the response echoes what was actually stored, so compare it to what you sent. > [!NOTE] Field name > The body field is `contentTags`, not `tags`. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the creator's dashboard (Vault → API Connect). One key = one creator. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Path parameters | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The vault item id. | ## Request body (application/json) | Field | Type | Required | Description | |---|---|---|---| | `contentTags` | string[] | yes | The complete new tag list (≤50 kept). | Example — Set two tags: ```json { "contentTags": [ "beach", "bikini" ] } ``` ## Responses ### 200 Stored — `contentTags` echoes the normalised list. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | Always true. | | `contentTags` | string[] | yes | The tags after trimming, de-duplication and capping. | ```json { "success": true, "contentTags": [ "beach", "bikini" ] } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"Invalid JSON body"}` | The body is not valid JSON. | | 400 | `{"error":"contentTags must be an array of strings"}` | Missing, not an array, or contains non-strings. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 404 | `{"error":"Vault item not found"}` | No such item on this account. | ## Rate limiting Per-key fixed windows by tier (Personal 60/min · 5,000/day; approved apps 300/min · 50,000/day; Dropfans-operated integrations exempt). Read the live values from X-RateLimit-Tier, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and their -Day variants; a 429 carries Retry-After and `{"error":"Rate limit exceeded","code":"rate_limited"}`. See [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md). ## Code samples ### curl ```bash curl -X PATCH "https://www.dropfans.io/api/external/vault/$VAULT_ITEM_ID/tags" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "contentTags": [ "beach", "bikini" ] }' ``` ### Node ```javascript const vaultItemId = '…'; // from an earlier response const res = await fetch(`https://www.dropfans.io/api/external/vault/${vaultItemId}/tags`, { method: 'PATCH', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ "contentTags": [ "beach", "bikini" ] }), }); console.log(await res.json()); ``` ### Python ```python import os import requests vault_item_id = "…" # from an earlier response res = requests.patch( f"https://www.dropfans.io/api/external/vault/{vault_item_id}/tags", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, json={ "contentTags": [ "beach", "bikini", ], }, ) print(res.json()) ``` --- Previous: [Move a vault item to a folder](https://www.dropfans.io/developers/reference/move-vault-item.md) · Next: [List vault folders](https://www.dropfans.io/developers/reference/list-folders.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/vault/folders — List vault folders > Every folder the creator has, alphabetically. **Unpaginated** — you always get the full list. - Source: https://www.dropfans.io/developers/reference/list-folders - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Every folder the creator has, alphabetically. **Unpaginated** — you always get the full list. `itemCount` here counts every non-hidden item regardless of moderation status, so it can be higher than the count the vault list reports for the same folder (which respects the moderation filter). ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the creator's dashboard (Vault → API Connect). One key = one creator. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Responses ### 200 All folders. | Field | Type | Required | Description | |---|---|---|---| | `folders` | Folder[] | yes | Every folder, A→Z. | ```json { "folders": [ { "id": "clxf0ld3r0001abcd", "name": "Beach set", "itemCount": 14 } ] } ``` ## Errors | Status | Body | When | |---|---|---| | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 500 | `{"error":"Failed to fetch vault folders"}` | Query failed — retry later. | ## Rate limiting Per-key fixed windows by tier (Personal 60/min · 5,000/day; approved apps 300/min · 50,000/day; Dropfans-operated integrations exempt). Read the live values from X-RateLimit-Tier, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and their -Day variants; a 429 carries Retry-After and `{"error":"Rate limit exceeded","code":"rate_limited"}`. See [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md). ## Code samples ### curl ```bash curl "https://www.dropfans.io/api/external/vault/folders" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/vault/folders`, { headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, }, }); console.log(await res.json()); ``` ### Python ```python import os import requests res = requests.get( "https://www.dropfans.io/api/external/vault/folders", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [Replace a vault item’s content tags](https://www.dropfans.io/developers/reference/set-vault-item-tags.md) · Next: [Create a vault folder](https://www.dropfans.io/developers/reference/create-folder.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # POST /api/external/vault/folders — Create a vault folder > Creates a folder. Names are unique per creator (case-sensitive) and at most 50 characters. - Source: https://www.dropfans.io/developers/reference/create-folder - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Creates a folder. Names are unique per creator (case-sensitive) and at most 50 characters. The response is the **bare folder object** — not wrapped in `{success}` or `{folder}` like most other write endpoints. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the creator's dashboard (Vault → API Connect). One key = one creator. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Request body (application/json) | Field | Type | Required | Description | |---|---|---|---| | `name` | string | yes | Folder name, trimmed, 1–50 chars, unique per creator. | Example — Create a folder: ```json { "name": "Beach set" } ``` ## Responses ### 200 Created — the bare folder object. | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Folder id. | | `name` | string | yes | Folder name — unique per creator. | | `itemCount` | integer | yes | Items in the folder. NOTE: GET /api/external/vault counts only items visible at that call’s moderation filter, while GET /api/external/vault/folders counts every non-hidden item regardless of moderation status — the same folder can report two different counts. | ```json { "id": "clxf0ld3r0001abcd", "name": "Beach set", "itemCount": 0 } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"Invalid JSON body"}` | The body is not valid JSON. | | 400 | `{"error":"Folder name is required"}` | `name` missing or not a string. | | 400 | `{"error":"Folder name cannot be empty"}` | `name` is whitespace only. | | 400 | `{"error":"Folder name must be 50 characters or less"}` | Longer than 50 characters. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 409 | `{"error":"A folder with this name already exists"}` | Duplicate name (also returned on a create race). | | 500 | `{"error":"Failed to create vault folder"}` | Write failed — retry later. | ## Rate limiting Per-key fixed windows by tier (Personal 60/min · 5,000/day; approved apps 300/min · 50,000/day; Dropfans-operated integrations exempt). Read the live values from X-RateLimit-Tier, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and their -Day variants; a 429 carries Retry-After and `{"error":"Rate limit exceeded","code":"rate_limited"}`. See [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md). ## Code samples ### curl ```bash curl -X POST "https://www.dropfans.io/api/external/vault/folders" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Beach set" }' ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/vault/folders`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ "name": "Beach set" }), }); console.log(await res.json()); ``` ### Python ```python import os import requests res = requests.post( "https://www.dropfans.io/api/external/vault/folders", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, json={ "name": "Beach set", }, ) print(res.json()) ``` --- Previous: [List vault folders](https://www.dropfans.io/developers/reference/list-folders.md) · Next: [Delete a vault folder](https://www.dropfans.io/developers/reference/delete-folder.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # DELETE /api/external/vault/folders/{folderId} — Delete a vault folder > Deletes a folder you own. **Non-empty folders are allowed**: the items inside are preserved and fall back to "All" (unfiled) — nothing is removed from storage. System folders (compliance archives) cannot be deleted. - Source: https://www.dropfans.io/developers/reference/delete-folder - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Deletes a folder you own. **Non-empty folders are allowed**: the items inside are preserved and fall back to "All" (unfiled) — nothing is removed from storage. System folders (compliance archives) cannot be deleted. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the creator's dashboard (Vault → API Connect). One key = one creator. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Path parameters | Field | Type | Required | Description | |---|---|---|---| | `folderId` | string | yes | The folder id. | ## Responses ### 200 Deleted; contained items are now unfiled. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | Always true. | ```json { "success": true } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"This folder is managed automatically and cannot be deleted."}` | The folder is a system folder. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 404 | `{"error":"Folder not found"}` | No such folder on this account. | | 500 | `{"error":"Failed to delete vault folder"}` | Write failed — retry later. | ## Rate limiting Per-key fixed windows by tier (Personal 60/min · 5,000/day; approved apps 300/min · 50,000/day; Dropfans-operated integrations exempt). Read the live values from X-RateLimit-Tier, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and their -Day variants; a 429 carries Retry-After and `{"error":"Rate limit exceeded","code":"rate_limited"}`. See [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md). ## Code samples ### curl ```bash curl -X DELETE "https://www.dropfans.io/api/external/vault/folders/$FOLDER_ID" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const folderId = '…'; // from an earlier response const res = await fetch(`https://www.dropfans.io/api/external/vault/folders/${folderId}`, { method: 'DELETE', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, }, }); console.log(await res.json()); ``` ### Python ```python import os import requests folder_id = "…" # from an earlier response res = requests.delete( f"https://www.dropfans.io/api/external/vault/folders/{folder_id}", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [Create a vault folder](https://www.dropfans.io/developers/reference/create-folder.md) · Next: [Start a video upload (step 1 of 3)](https://www.dropfans.io/developers/reference/start-video-upload.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # POST /api/external/vault/video-upload — Start a video upload (step 1 of 3) > Starts a direct-to-CDN video upload and returns presigned TUS credentials. - Source: https://www.dropfans.io/developers/reference/start-video-upload - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Starts a direct-to-CDN video upload and returns presigned TUS credentials. Videos cannot ride through [POST /api/external/vault](https://www.dropfans.io/developers/reference/upload-vault-item.md) — the platform rejects request bodies over ~4MB before the app even runs. Instead: **(1)** call this to create the video and get credentials, **(2)** upload the raw bytes straight to the returned `tusEndpoint` with any TUS client, sending `AuthorizationSignature`, `AuthorizationExpire`, `VideoId` and `LibraryId` as TUS headers, **(3)** call [complete](https://www.dropfans.io/developers/reference/complete-video-upload.md) with the `completionToken`. The full sequence with code is in the [upload guide](https://www.dropfans.io/developers/guides/upload-media.md). The CDN API key itself is never exposed — only a sha256 signature valid ~6 hours. The completion token is valid 8 hours. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the creator's dashboard (Vault → API Connect). One key = one creator. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Request body (application/json) | Field | Type | Required | Description | |---|---|---|---| | `originalName` | string | yes | The video file name, stored as fileName. | | `fileSize` | integer | | Optional advisory byte count — obvious oversizes are rejected up front. The authoritative check runs at completion. Max 500MB. | Example — Start an upload: ```json { "originalName": "teaser.mp4", "fileSize": 52428800 } ``` ## Responses ### 200 Upload created — feed these credentials to your TUS client. | Field | Type | Required | Description | |---|---|---|---| | `videoId` | string | yes | The Bunny Stream GUID created for this upload. Send it back to the complete step, and use it as the TUS `VideoId` metadata. | | `tusEndpoint` | string | yes | The TUS upload endpoint (https://video.bunnycdn.com/tusupload). Upload the raw file bytes here with a TUS client. | | `libraryId` | string | yes | Bunny Stream library id — send as the `LibraryId` TUS header. | | `signature` | string | yes | Presigned sha256 — send as the `AuthorizationSignature` TUS header. The Stream API key itself is never exposed. | | `expires` | integer | yes | Unix timestamp (seconds) when the signature expires (~6 hours) — send as the `AuthorizationExpire` TUS header. | | `completionToken` | string | yes | HMAC token proving this upload was started by your key — required by the complete step. Valid 8 hours. | ```json { "videoId": "c2f7f9e2-1111-4222-b333-4d55e6f7a8b9", "tusEndpoint": "https://video.bunnycdn.com/tusupload", "libraryId": "572783", "signature": "9b2f…64 hex…c1a0", "expires": 1755640800, "completionToken": "eyJ…" } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"Missing required field: originalName"}` | `originalName` absent or blank. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 413 | `{"error":"Video too large. Max 500MB."}` | The advisory fileSize exceeds 500MB. | | 500 | `{"error":"Could not start the video upload"}` | CDN video creation failed — retry later. | ## Rate limiting Per-key fixed windows by tier (Personal 60/min · 5,000/day; approved apps 300/min · 50,000/day; Dropfans-operated integrations exempt). Read the live values from X-RateLimit-Tier, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and their -Day variants; a 429 carries Retry-After and `{"error":"Rate limit exceeded","code":"rate_limited"}`. See [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md). ## Code samples ### curl ```bash curl -X POST "https://www.dropfans.io/api/external/vault/video-upload" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "originalName": "teaser.mp4", "fileSize": 52428800 }' ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/vault/video-upload`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ "originalName": "teaser.mp4", "fileSize": 52428800 }), }); console.log(await res.json()); ``` ### Python ```python import os import requests res = requests.post( "https://www.dropfans.io/api/external/vault/video-upload", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, json={ "originalName": "teaser.mp4", "fileSize": 52428800, }, ) print(res.json()) ``` --- Previous: [Delete a vault folder](https://www.dropfans.io/developers/reference/delete-folder.md) · Next: [Finish a video upload (step 3 of 3)](https://www.dropfans.io/developers/reference/complete-video-upload.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # POST /api/external/vault/video-upload/complete — Finish a video upload (step 3 of 3) > Registers a finished TUS upload as a vault item and kicks moderation. - Source: https://www.dropfans.io/developers/reference/complete-video-upload - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Registers a finished TUS upload as a vault item and kicks moderation. **Idempotent**: a retried completion for an already-registered video returns the existing item instead of creating a duplicate — retry freely. **409 means "not finished yet, retry"**: the CDN can lag a few seconds after the TUS client reports done, so on a 409 back off (2s, 5s, 10s, 30s) and call again with the same body. The item starts `PENDING` and finalizes asynchronously (the NSFW pipeline plus transcoding). Poll [video-status](https://www.dropfans.io/developers/reference/video-status.md) with the `bunnyStreamId` to know when the video is playable, and the vault list for moderation. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the creator's dashboard (Vault → API Connect). One key = one creator. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Request body (application/json) | Field | Type | Required | Description | |---|---|---|---| | `videoId` | string | yes | The `videoId` from step 1. | | `originalName` | string | yes | Same file name you sent in step 1. | | `completionToken` | string | yes | The token from step 1 (valid 8h). Proves this key started the upload. | | `folderId` | string \\| null | | Optional folder — silently ignored if you don’t own it. | Example — Register the uploaded video: ```json { "videoId": "c2f7f9e2-1111-4222-b333-4d55e6f7a8b9", "originalName": "teaser.mp4", "completionToken": "eyJ…" } ``` ## Responses ### 200 Registered (or already registered — idempotent). | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | Always true. | | `item` | VideoVaultItem | yes | The new video vault item, starting PENDING. | ```json { "success": true, "item": { "id": "clxv9z8y70002item", "fileName": "teaser.mp4", "filePath": "https://vz-example.b-cdn.net/c2f7f9e2-…/playlist.m3u8?token=…", "thumbnailPath": "https://vz-example.b-cdn.net/c2f7f9e2-…/thumbnail.jpg?token=…", "fileType": "video", "fileSize": 52428800, "bunnyStreamId": "c2f7f9e2-1111-4222-b333-4d55e6f7a8b9", "moderationStatus": "PENDING", "moderationTags": [], "createdAt": "2026-08-01T10:20:00.000Z", "aiEnhanced": false } } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"Missing required fields: videoId and originalName"}` | Either field absent or blank. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"Invalid or expired upload token — please retry the upload"}` | The completionToken is wrong, for another key, or older than 8h — start over from step 1. | | 409 | `{"error":"The video upload has not finished — please retry."}` | The CDN has not finished receiving the bytes — back off and retry the same call. | | 413 | `{"error":"Video too large. Max 500MB."}` | The stored byte count exceeds the cap — the upload is discarded. | | 500 | `{"error":"Failed to register the uploaded video"}` | Registration failed — retry later. | ## Notes This item shape adds `moderationStatus`, `moderationTags` and `aiEnhanced` (and omits `durationSeconds`) compared to the plain upload endpoint. `filePath` here is signed for ~1 hour only. ## Rate limiting Per-key fixed windows by tier (Personal 60/min · 5,000/day; approved apps 300/min · 50,000/day; Dropfans-operated integrations exempt). Read the live values from X-RateLimit-Tier, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and their -Day variants; a 429 carries Retry-After and `{"error":"Rate limit exceeded","code":"rate_limited"}`. See [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md). ## Code samples ### curl ```bash curl -X POST "https://www.dropfans.io/api/external/vault/video-upload/complete" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "videoId": "c2f7f9e2-1111-4222-b333-4d55e6f7a8b9", "originalName": "teaser.mp4", "completionToken": "eyJ…" }' ``` ### Node ```javascript // Step 2 happens in your client with a TUS library (npm i tus-js-client), // then step 3 registers the finished upload. import * as tus from 'tus-js-client'; import { readFile } from 'node:fs/promises'; const creds = await ( // step 1 await fetch('https://www.dropfans.io/api/external/vault/video-upload', { method: 'POST', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ originalName: 'teaser.mp4' }), }) ).json(); const file = await readFile('teaser.mp4'); await new Promise((resolve, reject) => { // step 2 — raw bytes straight to the CDN new tus.Upload(file, { endpoint: creds.tusEndpoint, headers: { AuthorizationSignature: creds.signature, AuthorizationExpire: String(creds.expires), VideoId: creds.videoId, LibraryId: creds.libraryId, }, metadata: { filetype: 'video/mp4', title: 'teaser.mp4' }, onError: reject, onSuccess: resolve, }).start(); }); // step 3 — 409 means "not finished yet": back off and retry the same call for (const waitMs of [0, 2000, 5000, 10000, 30000]) { if (waitMs) await new Promise((r) => setTimeout(r, waitMs)); const res = await fetch( 'https://www.dropfans.io/api/external/vault/video-upload/complete', { method: 'POST', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ videoId: creds.videoId, originalName: 'teaser.mp4', completionToken: creds.completionToken, }), }, ); if (res.status !== 409) { console.log(await res.json()); break; } } ``` ### Python ```python import os import requests res = requests.post( "https://www.dropfans.io/api/external/vault/video-upload/complete", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, json={ "videoId": "c2f7f9e2-1111-4222-b333-4d55e6f7a8b9", "originalName": "teaser.mp4", "completionToken": "eyJ…", }, ) print(res.json()) ``` --- Previous: [Start a video upload (step 1 of 3)](https://www.dropfans.io/developers/reference/start-video-upload.md) · Next: [Check video transcoding status (batch)](https://www.dropfans.io/developers/reference/video-status.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # POST /api/external/vault/video-status — Check video transcoding status (batch) > Transcoding status + duration for a batch of video GUIDs (`bunnyStreamId` values from your vault items). - Source: https://www.dropfans.io/developers/reference/video-status - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Transcoding status + duration for a batch of video GUIDs (`bunnyStreamId` values from your vault items). A freshly uploaded video is not forwardable or playable until `isReady` — poll this while `isProcessing`. Reasonable interval: every 15–30 seconds while you wait on a specific video. > [!WARNING] Silent truncation and omission > At most 50 ids are processed per call — extras are **dropped without an error**, so chunk larger lists. Ids you don't own, and ids whose lookup errored, are **omitted from the response** rather than reported: treat a missing key as "not ready". ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the creator's dashboard (Vault → API Connect). One key = one creator. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Request body (application/json) | Field | Type | Required | Description | |---|---|---|---| | `videoIds` | string[] | yes | Stream GUIDs to check (≤50 — extras silently dropped). Non-strings are filtered out; an empty array returns {"statuses":{}}. | Example — Check two videos: ```json { "videoIds": [ "c2f7f9e2-1111-4222-b333-4d55e6f7a8b9", "0f3d9a11-2222-4333-a444-5e66f7a8b9c0" ] } ``` ## Responses ### 200 Status map keyed by the GUIDs you sent (unresolvable ids omitted). | Field | Type | Required | Description | |---|---|---|---| | `statuses` | VideoStatusMap | yes | GUID → status. | ```json { "statuses": { "c2f7f9e2-1111-4222-b333-4d55e6f7a8b9": { "isReady": true, "isProcessing": false, "isFailed": false, "length": 74 }, "0f3d9a11-2222-4333-a444-5e66f7a8b9c0": { "isReady": false, "isProcessing": true, "isFailed": false } } } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"Invalid JSON body"}` | The body is not valid JSON. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | ## Rate limiting Per-key fixed windows by tier (Personal 60/min · 5,000/day; approved apps 300/min · 50,000/day; Dropfans-operated integrations exempt). Read the live values from X-RateLimit-Tier, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and their -Day variants; a 429 carries Retry-After and `{"error":"Rate limit exceeded","code":"rate_limited"}`. See [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md). ## Code samples ### curl ```bash curl -X POST "https://www.dropfans.io/api/external/vault/video-status" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "videoIds": [ "c2f7f9e2-1111-4222-b333-4d55e6f7a8b9", "0f3d9a11-2222-4333-a444-5e66f7a8b9c0" ] }' ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/vault/video-status`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ "videoIds": [ "c2f7f9e2-1111-4222-b333-4d55e6f7a8b9", "0f3d9a11-2222-4333-a444-5e66f7a8b9c0" ] }), }); console.log(await res.json()); ``` ### Python ```python import os import requests res = requests.post( "https://www.dropfans.io/api/external/vault/video-status", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, json={ "videoIds": [ "c2f7f9e2-1111-4222-b333-4d55e6f7a8b9", "0f3d9a11-2222-4333-a444-5e66f7a8b9c0", ], }, ) print(res.json()) ``` --- Previous: [Finish a video upload (step 3 of 3)](https://www.dropfans.io/developers/reference/complete-video-upload.md) · Next: [Create a sellable drop from vault items](https://www.dropfans.io/developers/reference/create-drop.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # POST /api/external/drops — Create a sellable drop from vault items > Packages up to 10 vault items into a drop and returns a checkout URL. - Source: https://www.dropfans.io/developers/reference/create-drop - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Packages up to 10 vault items into a drop and returns a checkout URL. Price is USD **dollars**: either `0` (free) or between $5 and $750. Use APPROVED vault items — the drop inherits its moderation status from its media, so a drop built from approved items is sellable (and attachable to a post) immediately, while one containing PENDING items waits for review. Hand the buyer the returned `buyUrl`, or build a Telegram link from [GET /api/external/links](https://www.dropfans.io/developers/reference/get-links.md)' `telegram.buyTemplate`. > [!WARNING] `description` is validated but NOT stored > The `description` field runs through the prohibited-word filter and is then deliberately discarded — it is never shown anywhere on Dropfans. You get a 200 with no indication it was dropped. Treat it as a moderation input only. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the creator's dashboard (Vault → API Connect). One key = one creator. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Request body (application/json) | Field | Type | Required | Description | |---|---|---|---| | `name` | string | | Drop title, shown at checkout. Falsy values are stored as null. | | `description` | string | | Checked for prohibited words, then discarded — never persisted or displayed. | | `price` | number | yes | USD dollars. 0 = free; otherwise $5–$750. | | `allowDownload` | boolean | | Whether buyers may download the files after purchase. | | `vaultItemIds` | string[] | yes | 1–10 vault item ids, in display order. The first becomes the cover. | Example — A $25 three-item drop: ```json { "name": "Beach set — 6 photos", "price": 25, "vaultItemIds": [ "clxv1a2b30001item", "clxv1a2b30002item", "clxv1a2b30003item" ] } ``` ## Responses ### 200 Created. Keep the productId — sales polling, previews and post attachment all key on it. | Field | Type | Required | Description | |---|---|---|---| | `productId` | string | yes | The new drop’s id — keep it: check-status, GET /drops/{id}, previews and post attachment all key on it. | | `buyUrl` | string | yes | Web checkout URL to hand to the buyer. For a Telegram Mini App link, substitute the productId into telegram.buyTemplate from GET /api/external/links. | | `mediaCount` | integer | yes | How many vault items were attached. | ```json { "productId": "clxdr0p000001prod", "buyUrl": "https://www.dropfans.io/buy/clxdr0p000001prod", "mediaCount": 3 } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"Invalid price"}` | `price` missing, not a number, NaN or negative. | | 400 | `{"error":"The minimum price is $5. Set the price to free or at least $5."}` | Priced above 0 but below $5. | | 400 | `{"error":"The maximum price on Dropfans is $750. To request an increase, contact support@dropfans.io."}` | Priced above $750. | | 400 | `{"error":"Field \"name\" contains a prohibited word: \"…\"","field":"name","matchedWord":"…"}` | The name or description tripped the word filter — a three-field envelope unique to this endpoint (posts use a two-field 422 for the same class of failure). | | 400 | `{"error":"vaultItemIds must be a non-empty array"}` | Missing or empty media list. | | 400 | `{"error":"Maximum 10 media items allowed per drop"}` | More than 10 ids. | | 400 | `{"error":"Cannot use hidden vault items"}` | One of the items was deleted (hidden). | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"You do not own all selected vault items"}` | An id belongs to another creator. | | 404 | `{"error":"One or more vault items not found"}` | An id does not exist. | | 500 | `{"error":"Failed to create drop"}` | Write failed — retry later. | ## Notes The drop's moderation status is computed from its media at creation (REJECTED/FLAGGED wins, else PENDING if any item is pending, else APPROVED). Read it back with [GET /api/external/drops/{id}](https://www.dropfans.io/developers/reference/get-drop.md). ## Rate limiting Per-key fixed windows by tier (Personal 60/min · 5,000/day; approved apps 300/min · 50,000/day; Dropfans-operated integrations exempt). Read the live values from X-RateLimit-Tier, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and their -Day variants; a 429 carries Retry-After and `{"error":"Rate limit exceeded","code":"rate_limited"}`. See [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md). ## Code samples ### curl ```bash curl -X POST "https://www.dropfans.io/api/external/drops" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Beach set — 6 photos", "price": 25, "vaultItemIds": [ "clxv1a2b30001item", "clxv1a2b30002item", "clxv1a2b30003item" ] }' ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/drops`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ "name": "Beach set — 6 photos", "price": 25, "vaultItemIds": [ "clxv1a2b30001item", "clxv1a2b30002item", "clxv1a2b30003item" ] }), }); console.log(await res.json()); ``` ### Python ```python import os import requests res = requests.post( "https://www.dropfans.io/api/external/drops", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, json={ "name": "Beach set — 6 photos", "price": 25, "vaultItemIds": [ "clxv1a2b30001item", "clxv1a2b30002item", "clxv1a2b30003item", ], }, ) print(res.json()) ``` --- Previous: [Check video transcoding status (batch)](https://www.dropfans.io/developers/reference/video-status.md) · Next: [Read back a drop](https://www.dropfans.io/developers/reference/get-drop.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/drops/{id} — Read back a drop > One drop you created: price, moderation status (overall and per media item), checkout URL, whether previews are attached, and a sales summary. - Source: https://www.dropfans.io/developers/reference/get-drop - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json One drop you created: price, moderation status (overall and per media item), checkout URL, whether previews are attached, and a sales summary. `salesCount` counts paid orders **excluding refunds and chargebacks** — stricter than [check-status](https://www.dropfans.io/developers/reference/check-drop-status.md), which does not exclude them. Use this endpoint to verify a drop went APPROVED before attaching it to a post, and to see which media items still lack a baked preview. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the creator's dashboard (Vault → API Connect). One key = one creator. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Path parameters | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The product (drop) id from create-drop. | ## Responses ### 200 The drop. | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The product (drop) id. | | `name` | string \\| null | | Drop title. Null when created without a name. | | `price` | number | yes | USD **dollars**. 0 means free. | | `currency` | `USD` | yes | Always "USD". | | `status` | `PENDING` \\| `APPROVED` \\| `REJECTED` \\| `FLAGGED` | yes | The drop’s overall moderation status. A drop built from already-APPROVED vault items is APPROVED at creation. | | `moderationReason` | string \\| null | | Why the drop was rejected or flagged. Null otherwise. | | `buyUrl` | string | yes | The web checkout page for this drop. | | `allowDownload` | boolean | yes | Whether buyers may download the files after purchase. | | `mediaCount` | integer | yes | Number of media items attached. | | `media` | object[] | yes | Attached media in display order. | | `salesCount` | integer | yes | Paid orders, excluding refunds and chargebacks (stricter than check-status, which does NOT exclude refunds). | | `lastSaleAt` | string \\| null | | When the newest counted sale was paid. Null when never sold. | | `createdAt` | string | yes | Creation time. | ```json { "id": "clxdr0p000001prod", "name": "Beach set — 6 photos", "price": 25, "currency": "USD", "status": "APPROVED", "moderationReason": null, "buyUrl": "https://www.dropfans.io/buy/clxdr0p000001prod", "allowDownload": true, "mediaCount": 3, "media": [ { "vaultItemId": "clxv1a2b30001item", "order": 0, "fileType": "image", "moderationStatus": "APPROVED", "hasPreview": true }, { "vaultItemId": "clxv1a2b30002item", "order": 1, "fileType": "image", "moderationStatus": "APPROVED", "hasPreview": true }, { "vaultItemId": "clxv1a2b30003item", "order": 2, "fileType": "video", "moderationStatus": "APPROVED", "hasPreview": false } ], "salesCount": 2, "lastSaleAt": "2026-08-18T21:03:00.000Z", "createdAt": "2026-08-15T09:00:00.000Z" } ``` ## Errors | Status | Body | When | |---|---|---| | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 404 | `{"error":"Drop not found"}` | No such drop on this account (another creator’s drop also 404s — never 403). | ## Rate limiting Per-key fixed windows by tier (Personal 60/min · 5,000/day; approved apps 300/min · 50,000/day; Dropfans-operated integrations exempt). Read the live values from X-RateLimit-Tier, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and their -Day variants; a 429 carries Retry-After and `{"error":"Rate limit exceeded","code":"rate_limited"}`. See [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md). ## Code samples ### curl ```bash curl "https://www.dropfans.io/api/external/drops/$PRODUCT_ID" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const productId = '…'; // from an earlier response const res = await fetch(`https://www.dropfans.io/api/external/drops/${productId}`, { headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, }, }); console.log(await res.json()); ``` ### Python ```python import os import requests product_id = "…" # from an earlier response res = requests.get( f"https://www.dropfans.io/api/external/drops/{product_id}", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [Create a sellable drop from vault items](https://www.dropfans.io/developers/reference/create-drop.md) · Next: [Attach baked blur previews to a drop](https://www.dropfans.io/developers/reference/attach-drop-previews.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # POST /api/external/drops/{id}/previews — Attach baked blur previews to a drop > Uploads pre-blurred ("baked") JPEG teasers for a drop's media, so the checkout page shows the exact same blur your app showed the buyer elsewhere. - Source: https://www.dropfans.io/developers/reference/attach-drop-previews - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Uploads pre-blurred ("baked") JPEG teasers for a drop's media, so the checkout page shows the exact same blur your app showed the buyer elsewhere. The body is `multipart/form-data` with one part per media item, named by vault item id: `previewBlob_` (the baked JPEG) and optionally `blurMeta_` (a descriptor like `"partial"`, `"pixelated:59"` or `"full"`). Without previews, paid media falls back to a generic gaussian blur. > [!WARNING] Best-effort — skipped items fail silently > Parts that are missing, not `image/jpeg`, over 8MB, or that hit a storage error are **skipped without an error**. The only signal is the `updated` count — compare it to how many parts you sent. Repeatable: re-sending overwrites the stored preview. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the creator's dashboard (Vault → API Connect). One key = one creator. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Path parameters | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The product (drop) id. | ## Request body (multipart/form-data) Field names embed the vault item id — one previewBlob per media item you want a preview on. | Field | Type | Required | Description | |---|---|---|---| | `previewBlob_` | file | | The baked JPEG teaser for that media item. Must be image/jpeg, ≤8MB. | | `blurMeta_` | string | | Optional blur descriptor recorded alongside: "full", "partial" or "pixelated:". | Example — Baked previews for two media items: ```json { "previewBlob_clxv1a2b30001item": "@preview-1.jpg", "blurMeta_clxv1a2b30001item": "partial", "previewBlob_clxv1a2b30002item": "@preview-2.jpg", "blurMeta_clxv1a2b30002item": "pixelated:59" } ``` ## Responses ### 200 `updated` = how many previews were actually stored. If it is lower than the number of parts you sent, the difference was silently skipped. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | Always true. | | `updated` | integer | yes | Previews stored on this call. | ```json { "success": true, "updated": 2 } ``` ## Errors | Status | Body | When | |---|---|---| | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"You do not own this drop"}` | The drop belongs to another creator. | | 404 | `{"error":"Drop not found"}` | No such drop. | | 500 | `{"error":"Failed to attach previews"}` | Unexpected failure — retry later. | ## Rate limiting Per-key fixed windows by tier (Personal 60/min · 5,000/day; approved apps 300/min · 50,000/day; Dropfans-operated integrations exempt). Read the live values from X-RateLimit-Tier, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and their -Day variants; a 429 carries Retry-After and `{"error":"Rate limit exceeded","code":"rate_limited"}`. See [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md). ## Code samples ### curl ```bash curl -X POST "https://www.dropfans.io/api/external/drops/$PRODUCT_ID/previews" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -F "previewBlob_clxv1a2b30001item=@preview-1.jpg;type=image/jpeg" \ -F "blurMeta_clxv1a2b30001item=partial" \ -F "previewBlob_clxv1a2b30002item=@preview-2.jpg;type=image/jpeg" \ -F "blurMeta_clxv1a2b30002item=pixelated:59" ``` ### Node ```javascript import { readFile } from 'node:fs/promises'; const form = new FormData(); form.append( 'previewBlob_clxv1a2b30001item', new Blob([await readFile('preview-1.jpg')], { type: 'image/jpeg' }), 'preview-1.jpg', ); form.append('blurMeta_clxv1a2b30001item', 'partial'); const res = await fetch( `https://www.dropfans.io/api/external/drops/${productId}/previews`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}` }, body: form, }, ); console.log(await res.json()); // { success: true, updated: 1 } ``` ### Python ```python import os, requests res = requests.post( f"https://www.dropfans.io/api/external/drops/{product_id}/previews", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, files={ "previewBlob_clxv1a2b30001item": ("preview-1.jpg", open("preview-1.jpg", "rb"), "image/jpeg"), }, data={"blurMeta_clxv1a2b30001item": "partial"}, ) print(res.json()) # { "success": true, "updated": 1 } ``` --- Previous: [Read back a drop](https://www.dropfans.io/developers/reference/get-drop.md) · Next: [Check which drops sold (batch)](https://www.dropfans.io/developers/reference/check-drop-status.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # POST /api/external/drops/check-status — Check which drops sold (batch) > Sale info for a batch of product ids — the polling half of sale tracking. - Source: https://www.dropfans.io/developers/reference/check-drop-status - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Sale info for a batch of product ids — the polling half of sale tracking. Poll it periodically (every few minutes is plenty) with the productIds you are tracking. Amounts are gross buyer-paid totals in **cents**. > [!WARNING] Truncation, omission, refunds > At most 200 ids are processed per call — extras are **dropped without an error**, so chunk larger lists. Unsold and unknown ids are **omitted** from the response (only `paid: true` entries appear). And unlike earnings, this endpoint does **NOT exclude refunded or charged-back orders** — a refunded sale still reports paid. Reconcile against [GET /api/external/earnings](https://www.dropfans.io/developers/reference/get-earnings.md) for net truth. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the creator's dashboard (Vault → API Connect). One key = one creator. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Request body (application/json) | Field | Type | Required | Description | |---|---|---|---| | `productIds` | string[] | yes | Product ids to check (≤200 — extras silently dropped). Non-strings filtered out; an empty array returns {"sales":{}}. | Example — Check two drops: ```json { "productIds": [ "clxdr0p000001prod", "clxdr0p000002prod" ] } ``` ## Responses ### 200 Sales map — only sold products appear. Multiple paid orders on one product report the most recent. | Field | Type | Required | Description | |---|---|---|---| | `sales` | SalesMap | yes | productId → most recent sale. | ```json { "sales": { "clxdr0p000001prod": { "paid": true, "saleAmountCents": 2500, "buyerEmail": "buyer@example.com" } } } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"Invalid JSON body"}` | The body is not valid JSON. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 500 | `{"error":"Failed to check drop status"}` | Query failed — retry later. | ## Rate limiting Per-key fixed windows by tier (Personal 60/min · 5,000/day; approved apps 300/min · 50,000/day; Dropfans-operated integrations exempt). Read the live values from X-RateLimit-Tier, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and their -Day variants; a 429 carries Retry-After and `{"error":"Rate limit exceeded","code":"rate_limited"}`. See [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md). ## Code samples ### curl ```bash curl -X POST "https://www.dropfans.io/api/external/drops/check-status" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "productIds": [ "clxdr0p000001prod", "clxdr0p000002prod" ] }' ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/drops/check-status`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ "productIds": [ "clxdr0p000001prod", "clxdr0p000002prod" ] }), }); console.log(await res.json()); ``` ### Python ```python import os import requests res = requests.post( "https://www.dropfans.io/api/external/drops/check-status", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, json={ "productIds": [ "clxdr0p000001prod", "clxdr0p000002prod", ], }, ) print(res.json()) ``` --- Previous: [Attach baked blur previews to a drop](https://www.dropfans.io/developers/reference/attach-drop-previews.md) · Next: [Publish a post to the For You feed](https://www.dropfans.io/developers/reference/create-post.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # POST /api/external/posts — 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. - Source: https://www.dropfans.io/developers/reference/create-post - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json 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. 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. Account types: CREATOR, AGENCY only. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the creator's dashboard (Vault → API Connect). One key = one creator. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Request body (application/json) | Field | Type | Required | Description | |---|---|---|---| | `caption` | string | | Post text. Required for kind=TEXT. Silently truncated to 2000 characters. | | `kind` | `TEXT` \\| `MEDIA` \\| `SUBSCRIPTION` \\| `COMMUNITY` | | Post shape. DROP is not a request value — send productId instead. | | `productId` | string | | Attach an existing approved drop. Forces kind=DROP. Must belong to you. | | `media` | object[] | | Required when kind=MEDIA. | | `scheduledAt` | string | | ISO-8601. At least 1 minute ahead, at most 30 days. Omit to publish now. | Example — Text post: ```json { "caption": "im 5 min away, wyd?" } ``` Example — Free image from the vault: ```json { "kind": "MEDIA", "caption": "rate my fit 1-10", "media": [ { "vaultItemId": "clxv1a2b30001item", "isPaid": false, "order": 0 } ] } ``` Example — Free teaser + paid unlock, scheduled: ```json { "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 ### 201 Post created. Check `status`: APPROVED means it is already live, PENDING means it is still in review and will go live by itself once it clears. | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The new post id — poll GET /api/external/posts/{id} with it. | | `status` | `PENDING` \\| `APPROVED` | yes | PENDING is normal, not an error: media posts wait on the NSFW pipeline and a flagged caption waits on a human. | | `pending` | boolean | yes | Convenience mirror of status === "PENDING". | | `scheduledAt` | string \\| null | | Echoed schedule time, or null for immediate posts. | | `url` | string \\| null | | The creator’s profile URL (where the post appears once live). Null when the creator has no username yet. | ```json { "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"). | ## 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. ## Rate limiting Per-key fixed windows by tier (Personal 60/min · 5,000/day; approved apps 300/min · 50,000/day; Dropfans-operated integrations exempt). Read the live values from X-RateLimit-Tier, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and their -Day variants; a 429 carries Retry-After and `{"error":"Rate limit exceeded","code":"rate_limited"}`. See [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md). ## Code samples ### curl ```bash 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?" }' ``` ### Node ```javascript 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()); ``` ### Python ```python 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()) ``` --- Previous: [Check which drops sold (batch)](https://www.dropfans.io/developers/reference/check-drop-status.md) · Next: [List your posts and their moderation status](https://www.dropfans.io/developers/reference/list-posts.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/posts — List your posts and their moderation status > Your posts, newest first, plus the current posting limits — read `limits` from the response rather than hardcoding numbers. - Source: https://www.dropfans.io/developers/reference/list-posts - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Your posts, newest first, plus the current posting limits — read `limits` from the response rather than hardcoding numbers. The `status` filter takes one moderation state; unrecognised values are silently ignored (you get the unfiltered list, not an error). ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the creator's dashboard (Vault → API Connect). One key = one creator. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Query parameters | Field | Type | Required | Description | |---|---|---|---| | `page` | integer | | Page number, 1-based. | | `limit` | integer | | Posts per page (default 20, cap 50 — larger values clamped). | | `status` | `PENDING` \\| `APPROVED` \\| `REJECTED` \\| `FLAGGED` | | Filter by moderation state (case-insensitive). Unrecognised values are ignored. | ## Responses ### 200 Your posts, newest first, plus the current limits. | Field | Type | Required | Description | |---|---|---|---| | `posts` | Post[] | yes | Your posts, newest first. | | `pagination` | Pagination | yes | Paging info (nested style — the vault list uses top-level fields instead). | | `limits` | PostLimits | yes | The current posting limits, so a client never hardcodes them. | ```json { "posts": [ { "id": "clxp0st000001feed", "kind": "MEDIA", "caption": "rate my fit 1-10", "status": "APPROVED", "live": true, "scheduledAt": null, "publishedAt": "2026-08-18T19:00:00.000Z", "createdAt": "2026-08-18T18:59:40.000Z", "productId": null, "likes": 14, "comments": 3, "media": [ { "id": "clxpm3d1a0001post", "vaultItemId": "clxv1a2b30001item", "isPaid": false, "order": 0, "type": "image" } ] } ], "pagination": { "page": 1, "limit": 20, "total": 63, "hasMore": true }, "limits": { "postsPerDay": 5, "maxCaptionChars": 2000, "maxMediaPerPost": 10, "maxScheduleDays": 30, "minPaidPrice": 5 } } ``` ## Errors | Status | Body | When | |---|---|---| | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | ## Rate limiting Per-key fixed windows by tier (Personal 60/min · 5,000/day; approved apps 300/min · 50,000/day; Dropfans-operated integrations exempt). Read the live values from X-RateLimit-Tier, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and their -Day variants; a 429 carries Retry-After and `{"error":"Rate limit exceeded","code":"rate_limited"}`. See [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md). ## Code samples ### curl ```bash curl "https://www.dropfans.io/api/external/posts" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/posts`, { headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, }, }); console.log(await res.json()); ``` ### Python ```python import os import requests res = requests.get( "https://www.dropfans.io/api/external/posts", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [Publish a post to the For You feed](https://www.dropfans.io/developers/reference/create-post.md) · Next: [Check one post’s moderation status](https://www.dropfans.io/developers/reference/get-post.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/posts/{id} — Check one post’s moderation status > One post, as a **bare object** (not wrapped). The polling endpoint after a PENDING create: check `status` and `live`. - Source: https://www.dropfans.io/developers/reference/get-post - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json One post, as a **bare object** (not wrapped). The polling endpoint after a PENDING create: check `status` and `live`. A sensible poll cadence is every 30–60 seconds while PENDING; media moderation usually resolves within minutes. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the creator's dashboard (Vault → API Connect). One key = one creator. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Path parameters | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The post id. | ## Responses ### 200 The post. | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Post id. | | `kind` | `TEXT` \\| `DROP` \\| `SUBSCRIPTION` \\| `COMMUNITY` \\| `MEDIA` | yes | Post shape. DROP is derived from productId at creation — it is not an accepted request value. | | `caption` | string \\| null | | Post text. Null for caption-less media posts. | | `status` | `PENDING` \\| `APPROVED` \\| `REJECTED` \\| `FLAGGED` | yes | Moderation state. PENDING is normal for media posts and is not an error — poll until it becomes APPROVED. | | `live` | boolean | yes | True only when the post is APPROVED and its publish time has passed. | | `scheduledAt` | string \\| null | | When the post is scheduled to go live, or null for immediate posts. | | `publishedAt` | string | yes | Effective publish time. | | `createdAt` | string | yes | Creation time. | | `productId` | string \\| null | | The attached drop, for kind=DROP. Null otherwise. | | `likes` | integer | yes | Like count. | | `comments` | integer | yes | Comment count. | | `media` | PostMedia[] | yes | Media entries in display order. Empty for TEXT posts. | ```json { "id": "clxp0st000001feed", "kind": "MEDIA", "caption": "rate my fit 1-10", "status": "APPROVED", "live": true, "scheduledAt": null, "publishedAt": "2026-08-18T19:00:00.000Z", "createdAt": "2026-08-18T18:59:40.000Z", "productId": null, "likes": 14, "comments": 3, "media": [ { "id": "clxpm3d1a0001post", "vaultItemId": "clxv1a2b30001item", "isPaid": false, "order": 0, "type": "image" } ] } ``` ## Errors | Status | Body | When | |---|---|---| | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 404 | `{"error":"Post not found"}` | No such post on your account. Deliberately 404 (not 403) for another creator’s post — ids cannot be probed. | ## Rate limiting Per-key fixed windows by tier (Personal 60/min · 5,000/day; approved apps 300/min · 50,000/day; Dropfans-operated integrations exempt). Read the live values from X-RateLimit-Tier, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and their -Day variants; a 429 carries Retry-After and `{"error":"Rate limit exceeded","code":"rate_limited"}`. See [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md). ## Code samples ### curl ```bash curl "https://www.dropfans.io/api/external/posts/$POST_ID" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const postId = '…'; // from an earlier response const res = await fetch(`https://www.dropfans.io/api/external/posts/${postId}`, { headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, }, }); console.log(await res.json()); ``` ### Python ```python import os import requests post_id = "…" # from an earlier response res = requests.get( f"https://www.dropfans.io/api/external/posts/{post_id}", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [List your posts and their moderation status](https://www.dropfans.io/developers/reference/list-posts.md) · Next: [Delete a post (or cancel a scheduled one)](https://www.dropfans.io/developers/reference/delete-post.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # DELETE /api/external/posts/{id} — Delete a post (or cancel a scheduled one) > Hard-deletes one of your posts. Deleting a scheduled post before it goes live cancels it. - Source: https://www.dropfans.io/developers/reference/delete-post - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Hard-deletes one of your posts. Deleting a scheduled post before it goes live cancels it. Returns `{"ok":true}` — note this family uses `ok` where the vault endpoints use `success`. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the creator's dashboard (Vault → API Connect). One key = one creator. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Path parameters | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The post id. | ## Responses ### 200 Deleted. | Field | Type | Required | Description | |---|---|---|---| | `ok` | boolean | yes | Always true. (This endpoint family returns {ok} where the vault family returns {success} — historical, kept for compatibility.) | ```json { "ok": true } ``` ## Errors | Status | Body | When | |---|---|---| | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 404 | `{"error":"Post not found"}` | No such post on your account (another creator’s post also 404s). | ## Rate limiting Per-key fixed windows by tier (Personal 60/min · 5,000/day; approved apps 300/min · 50,000/day; Dropfans-operated integrations exempt). Read the live values from X-RateLimit-Tier, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and their -Day variants; a 429 carries Retry-After and `{"error":"Rate limit exceeded","code":"rate_limited"}`. See [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md). ## Code samples ### curl ```bash curl -X DELETE "https://www.dropfans.io/api/external/posts/$POST_ID" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const postId = '…'; // from an earlier response const res = await fetch(`https://www.dropfans.io/api/external/posts/${postId}`, { method: 'DELETE', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, }, }); console.log(await res.json()); ``` ### Python ```python import os import requests post_id = "…" # from an earlier response res = requests.delete( f"https://www.dropfans.io/api/external/posts/{post_id}", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [Check one post’s moderation status](https://www.dropfans.io/developers/reference/get-post.md) · Next: [Earnings stats, chart and recent transactions](https://www.dropfans.io/developers/reference/get-earnings.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/earnings — Earnings stats, chart and recent transactions > The same revenue the creator sees on their own dashboard: drop sales + tips + subscription payments, refunds and chargebacks excluded, bucketed by day in the caller's timezone. - Source: https://www.dropfans.io/developers/reference/get-earnings - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json The same revenue the creator sees on their own dashboard: drop sales + tips + subscription payments, refunds and chargebacks excluded, bucketed by day in the caller's timezone. Everything here is in **cents** (the balance endpoint is dollars). `stats.totalEarningsCents` is NET (after the platform fee); `grossEarningsCents` and the chart series are seller GROSS (buyer-paid minus tax, before the fee) so per-day merges against your own gross ledgers compare like for like. Pass `tz` = the creator's timezone (read it from [GET /api/external/timezone](https://www.dropfans.io/developers/reference/get-timezone.md)) or your day buckets won't match their dashboard. > [!NOTE] transactions is a fixed cap, not a page > `transactions` is always the newest 50 across all three sources — there is no pagination. To get a complete ledger, narrow the date window until fewer than 50 come back. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the creator's dashboard (Vault → API Connect). One key = one creator. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Query parameters | Field | Type | Required | Description | |---|---|---|---| | `startDate` | string | yes | Window start, YYYY-MM-DD, interpreted in `tz`. | | `endDate` | string | yes | Window end (inclusive), YYYY-MM-DD, interpreted in `tz`. | | `tz` | string | | IANA timezone for day bucketing. **Invalid values silently fall back to UTC** — no error. | ## Responses ### 200 Stats, chart and the newest transactions. All money in cents. | Field | Type | Required | Description | |---|---|---|---| | `stats` | EarningsStats | yes | Window totals. Cents throughout (the balance endpoint is dollars). | | `chart` | EarningsChart | yes | Bucketed GROSS series in the caller’s timezone. | | `transactions` | EarningsTransaction[] | yes | The newest 50 transactions across all three sources — a fixed cap, not a page. For a complete ledger, narrow the date window until fewer than 50 come back. | ```json { "stats": { "totalEarningsCents": 182050, "grossEarningsCents": 214180, "previousPeriodEarningsCents": 141200, "previousPeriodGrossEarningsCents": 166100, "transactionCount": 41, "avgTransactionCents": 4440, "uniqueCustomers": 28, "typeTotals": { "drop": { "grossCents": 150000, "netCents": 127500, "count": 30 }, "tip": { "grossCents": 44180, "netCents": 37550, "count": 8 }, "subscription": { "grossCents": 20000, "netCents": 17000, "count": 3 } } }, "chart": { "labels": [ "Aug 18", "Aug 19" ], "values": [ 12500, 9800 ], "dates": [ "2026-08-18", "2026-08-19" ], "groupBy": "day", "typedValues": { "drop": [ 10000, 7500 ], "tip": [ 2500, 2300 ], "subscription": [ 0, 0 ] } }, "transactions": [ { "id": "clx0rd3r000001sale", "productId": "clxdr0p000001prod", "productName": "Beach set — 6 photos", "amountCents": 2000, "grossAmountCents": 2500, "buyerEmail": "buyer@example.com", "buyerName": null, "paidAt": "2026-08-18T21:03:00.000Z", "type": "drop" } ] } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"startDate and endDate query params are required (YYYY-MM-DD)"}` | Either date param is missing. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 500 | `{"error":"Failed to fetch earnings"}` | Query failed — retry later. | ## Notes Chart bucketing: day for spans ≤31 days, week ≤90, month beyond. `transactions[].productId` only exists on type "drop" — use it to join a sale back to the drop your app created. Because refunds are excluded here but NOT in check-status, the two can legitimately disagree. ## Rate limiting Per-key fixed windows by tier (Personal 60/min · 5,000/day; approved apps 300/min · 50,000/day; Dropfans-operated integrations exempt). Read the live values from X-RateLimit-Tier, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and their -Day variants; a 429 carries Retry-After and `{"error":"Rate limit exceeded","code":"rate_limited"}`. See [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md). ## Code samples ### curl ```bash curl "https://www.dropfans.io/api/external/earnings?startDate=2026-08-01&endDate=2026-08-19&tz=Europe%2FStockholm" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/earnings?startDate=2026-08-01&endDate=2026-08-19&tz=Europe%2FStockholm`, { headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, }, }); console.log(await res.json()); ``` ### Python ```python import os import requests res = requests.get( "https://www.dropfans.io/api/external/earnings", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, params={"startDate": "2026-08-01", "endDate": "2026-08-19", "tz": "Europe/Stockholm"}, ) print(res.json()) ``` --- Previous: [Delete a post (or cancel a scheduled one)](https://www.dropfans.io/developers/reference/delete-post.md) · Next: [Canonical profile, tip, subscribe and buy links](https://www.dropfans.io/developers/reference/get-links.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/links — Canonical profile, tip, subscribe and buy links > Every shareable link for the creator behind the key, in one call — never template Dropfans URLs by hand. - Source: https://www.dropfans.io/developers/reference/get-links - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Every shareable link for the creator behind the key, in one call — never template Dropfans URLs by hand. The `web` block always exists. The `telegram` block (Mini App deep links) is **null when the shared Dropfans bot is not configured** — fall back to the web links. Fields ending in `Template` contain literal placeholders to substitute: `{usd}` (whole dollars, web tip), `{cents}` (Telegram tip — note the unit difference), `{productId}` (buy links). Tip prefill bounds: web `?tip=` opens the sheet prefilled when the amount is between $5 and $750; out-of-range amounts open the sheet unfilled rather than erroring. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the creator's dashboard (Vault → API Connect). One key = one creator. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Responses ### 200 The creator’s links. | Field | Type | Required | Description | |---|---|---|---| | `username` | string | yes | The creator’s @handle the links are built for. | | `web` | object | yes | Canonical web links. Templates contain literal placeholders — substitute before use. | | `web.profile` | string | yes | The creator’s public profile. | | `web.tip` | string | yes | Opens the profile with the tip sheet open. | | `web.tipTemplate` | string | yes | Tip link with the amount prefilled — replace `{usd}` with whole US dollars. Amounts outside the allowed range open the sheet unfilled. | | `web.subscribe` | string | yes | Opens the profile with the subscribe sheet open. | | `web.buyTemplate` | string | yes | Checkout link for any drop — replace `{productId}` with the id from create-drop. | | `telegram` | object \\| null | | Telegram Mini App deep links. **Null when the shared Dropfans bot is not configured** — fall back to the web links. | | `telegram.bot` | string | yes | The shared bot username, without @. | | `telegram.profile` | string | yes | Opens the creator’s Mini App profile. | | `telegram.tip` | string | yes | Opens the Mini App tip sheet. | | `telegram.tipTemplate` | string | yes | Tip prefilled — replace `{cents}` with the amount in **cents** (the web template takes dollars; this one takes cents). | | `telegram.subscribe` | string | yes | Opens the Mini App subscribe flow. | | `telegram.spin` | string | yes | Opens the Lucky Wheel (when enabled for the creator). | | `telegram.buyTemplate` | string | yes | Mini App checkout for any drop — replace `{productId}`. | ```json { "username": "valeria", "web": { "profile": "https://www.dropfans.io/u/valeria", "tip": "https://www.dropfans.io/u/valeria?tip=1", "tipTemplate": "https://www.dropfans.io/u/valeria?tip={usd}", "subscribe": "https://www.dropfans.io/u/valeria?subscribe=1", "buyTemplate": "https://www.dropfans.io/buy/{productId}" }, "telegram": { "bot": "DropfansBot", "profile": "https://t.me/DropfansBot/app?startapp=p_valeria", "tip": "https://t.me/DropfansBot/app?startapp=t_valeria", "tipTemplate": "https://t.me/DropfansBot/app?startapp=pt_{cents}_valeria", "subscribe": "https://t.me/DropfansBot/app?startapp=s_valeria", "spin": "https://t.me/DropfansBot/app?startapp=w_valeria", "buyTemplate": "https://t.me/DropfansBot/app?startapp=b_{productId}" } } ``` ## Errors | Status | Body | When | |---|---|---| | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 409 | `{"error":"Set a username first","code":"username_required"}` | The creator has no username yet — every link embeds it. Have the creator pick a handle in their Dropfans settings, then retry. | ## Notes There is no endpoint to create a checkout session or an arbitrary-amount payment link: buyers pay through these pages/Mini App flows. The tip templates are the closest thing to a "payment link" — the amount is prefilled, the buyer confirms on Dropfans. ## Rate limiting Per-key fixed windows by tier (Personal 60/min · 5,000/day; approved apps 300/min · 50,000/day; Dropfans-operated integrations exempt). Read the live values from X-RateLimit-Tier, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and their -Day variants; a 429 carries Retry-After and `{"error":"Rate limit exceeded","code":"rate_limited"}`. See [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md). ## Code samples ### curl ```bash curl "https://www.dropfans.io/api/external/links" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/links`, { headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, }, }); console.log(await res.json()); ``` ### Python ```python import os import requests res = requests.get( "https://www.dropfans.io/api/external/links", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [Earnings stats, chart and recent transactions](https://www.dropfans.io/developers/reference/get-earnings.md) · Next: [Read the Telegram notification status](https://www.dropfans.io/developers/reference/get-notifications.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/notifications — Read the Telegram notification status > Where the creator's sale notifications currently go: their saved handle, whether a personal chat is connected, and whether a group/channel is connected. - Source: https://www.dropfans.io/developers/reference/get-notifications - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Where the creator's sale notifications currently go: their saved handle, whether a personal chat is connected, and whether a group/channel is connected. Read-only and safe. Use it before any write in this group so you never clobber an existing setup. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the creator's dashboard (Vault → API Connect). One key = one creator. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Responses ### 200 The current connection state. | Field | Type | Required | Description | |---|---|---|---| | `telegramHandle` | string \\| null | | The creator’s saved Telegram @handle (without @), or null. | | `personalConnected` | boolean | yes | True when a personal chat id is registered — sale notifications DM the creator. | | `groupConnected` | boolean | yes | True when a group/channel is registered for notifications. | | `groupChatId` | string \\| null | | The registered group chat id (e.g. "-100…"), or null. | | `groupName` | string \\| null | | The registered group’s title, or null. | ```json { "telegramHandle": "valeria_tg", "personalConnected": true, "groupConnected": false, "groupChatId": null, "groupName": null } ``` ## Errors | Status | Body | When | |---|---|---| | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 404 | `{"error":"User not found"}` | The key’s account no longer exists. | ## Rate limiting Per-key fixed windows by tier (Personal 60/min · 5,000/day; approved apps 300/min · 50,000/day; Dropfans-operated integrations exempt). Read the live values from X-RateLimit-Tier, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and their -Day variants; a 429 carries Retry-After and `{"error":"Rate limit exceeded","code":"rate_limited"}`. See [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md). ## Code samples ### curl ```bash curl "https://www.dropfans.io/api/external/notifications" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/notifications`, { headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, }, }); console.log(await res.json()); ``` ### Python ```python import os import requests res = requests.get( "https://www.dropfans.io/api/external/notifications", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [Canonical profile, tip, subscribe and buy links](https://www.dropfans.io/developers/reference/get-links.md) · Next: [Update Telegram notification settings](https://www.dropfans.io/developers/reference/update-notifications.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # PUT /api/external/notifications — Update Telegram notification settings > An action-dispatch endpoint — the `action` field picks one of three operations: - Source: https://www.dropfans.io/developers/reference/update-notifications - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json An action-dispatch endpoint — the `action` field picks one of three operations: - `save-handle` — store the creator's Telegram @handle (`telegramHandle`, leading @ stripped). - `disconnect` — clear the personal chat, the group, or both (`type`: "personal" | "group" | "all"). - `add-group` — register a group/channel by chat id (`groupChatId`, e.g. "-100…"). The bot must already be a member: the id is verified against Telegram before saving, and private-chat ids are rejected. > [!WARNING] This rewrites the creator's live notification settings > The same fields power the creator's own Dropfans → Telegram sale notifications. Changing them here changes where the creator's notifications go — including notifications your app has nothing to do with. Only call this when the creator explicitly asked for it. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the creator's dashboard (Vault → API Connect). One key = one creator. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Request body (application/json) | Field | Type | Required | Description | |---|---|---|---| | `action` | `save-handle` \\| `disconnect` \\| `add-group` | yes | Which operation to perform. | | `telegramHandle` | string | | save-handle only: the @handle (leading @ is stripped). | | `type` | `personal` \\| `group` \\| `all` | | disconnect only: what to clear. | | `groupChatId` | string | | add-group only: the Telegram chat id of a group/channel the bot is in. | Example — Save the creator’s handle: ```json { "action": "save-handle", "telegramHandle": "valeria_tg" } ``` Example — Register a notification group: ```json { "action": "add-group", "groupChatId": "-1001234567890" } ``` Example — Disconnect everything: ```json { "action": "disconnect", "type": "all" } ``` ## Responses ### 200 Action applied. save-handle echoes `telegramHandle`; add-group echoes `groupName`; disconnect returns `{success:true}` alone. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | Always true. | | `telegramHandle` | string | | save-handle only: the stored handle (without @). | | `groupName` | string | | add-group only: the group’s title as Telegram reports it. | ```json { "success": true, "groupName": "Valeria sales" } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"Invalid JSON body"}` | The body is not valid JSON. | | 400 | `{"error":"telegramHandle is required"}` | save-handle without a handle. | | 400 | `{"error":"Invalid telegram handle"}` | save-handle with an empty handle after stripping @. | | 400 | `{"error":"type must be \"personal\", \"group\", or \"all\""}` | disconnect with a bad type. | | 400 | `{"error":"groupChatId is required"}` | add-group without a chat id. | | 400 | `{"error":"Could not find this group. Make sure the bot has been added to the group first."}` | Telegram does not know the chat, or the bot is not a member. | | 400 | `{"error":"This ID belongs to a private chat, not a group or channel."}` | add-group with a personal chat id. | | 400 | `{"error":"Unknown action"}` | `action` is none of the three. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | ## Rate limiting Per-key fixed windows by tier (Personal 60/min · 5,000/day; approved apps 300/min · 50,000/day; Dropfans-operated integrations exempt). Read the live values from X-RateLimit-Tier, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and their -Day variants; a 429 carries Retry-After and `{"error":"Rate limit exceeded","code":"rate_limited"}`. See [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md). ## Code samples ### curl ```bash curl -X PUT "https://www.dropfans.io/api/external/notifications" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "action": "save-handle", "telegramHandle": "valeria_tg" }' ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/notifications`, { method: 'PUT', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ "action": "save-handle", "telegramHandle": "valeria_tg" }), }); console.log(await res.json()); ``` ### Python ```python import os import requests res = requests.put( "https://www.dropfans.io/api/external/notifications", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, json={ "action": "save-handle", "telegramHandle": "valeria_tg", }, ) print(res.json()) ``` --- Previous: [Read the Telegram notification status](https://www.dropfans.io/developers/reference/get-notifications.md) · Next: [Register the creator’s personal notification chat](https://www.dropfans.io/developers/reference/register-telegram-chat.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # POST /api/external/register-telegram-chat — Register the creator’s personal notification chat > Stores a Telegram chat id as the creator's **personal** notification target — the DM their sale notifications go to. - Source: https://www.dropfans.io/developers/reference/register-telegram-chat - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Stores a Telegram chat id as the creator's **personal** notification target — the DM their sale notifications go to. Unlike add-group, the id is **not verified against Telegram** — a wrong id silently breaks the creator's notifications until corrected. Prefer letting the creator connect through the Dropfans dashboard; use this only when your app already knows the correct chat id (e.g. the creator is talking to your bot). > [!WARNING] This rewrites the creator's live notification settings > The same fields power the creator's own Dropfans → Telegram sale notifications. Changing them here changes where the creator's notifications go — including notifications your app has nothing to do with. Only call this when the creator explicitly asked for it. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the creator's dashboard (Vault → API Connect). One key = one creator. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Request body (application/json) | Field | Type | Required | Description | |---|---|---|---| | `telegramChatId` | string | yes | The Telegram chat id of the creator’s DM with the notification bot. Stored as-is, unverified. | Example — Register a personal chat: ```json { "telegramChatId": "123456789" } ``` ## Responses ### 200 Stored. | Field | Type | Required | Description | |---|---|---|---| | `success` | boolean | yes | Always true. | ```json { "success": true } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"Invalid JSON body"}` | The body is not valid JSON. | | 400 | `{"error":"telegramChatId is required and must be a string"}` | Missing or non-string chat id. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | ## Rate limiting Per-key fixed windows by tier (Personal 60/min · 5,000/day; approved apps 300/min · 50,000/day; Dropfans-operated integrations exempt). Read the live values from X-RateLimit-Tier, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset and their -Day variants; a 429 carries Retry-After and `{"error":"Rate limit exceeded","code":"rate_limited"}`. See [Rate limits](https://www.dropfans.io/developers/concepts/rate-limits.md). ## Code samples ### curl ```bash curl -X POST "https://www.dropfans.io/api/external/register-telegram-chat" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "telegramChatId": "123456789" }' ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/register-telegram-chat`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ "telegramChatId": "123456789" }), }); console.log(await res.json()); ``` ### Python ```python import os import requests res = requests.post( "https://www.dropfans.io/api/external/register-telegram-chat", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, json={ "telegramChatId": "123456789", }, ) print(res.json()) ``` --- Previous: [Update Telegram notification settings](https://www.dropfans.io/developers/reference/update-notifications.md) · Next: [Changelog](https://www.dropfans.io/developers/changelog.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # Changelog > Dated, additive changes to the API and these docs. - Source: https://www.dropfans.io/developers/changelog - Section: Resources - OpenAPI: https://www.dropfans.io/developers/openapi.json Changes to the API are additive — new fields, new endpoints — and land here with a date. A breaking change would get its own dated entry and advance warning before it happens. Check this page (or its [Markdown twin](https://www.dropfans.io/developers/changelog.md)) when something looks new. ## 2026-08-19 — Public launch - Apps & approval: third-party integrations can now apply for API access at /developers/apply. Approved apps appear in the key picker under Vault → API Connect. - Rate limits: every response now carries X-RateLimit-* headers (minute and day windows, per tier). Dropfans-operated integrations are exempt. - New endpoint: GET /api/external/drops/{id} — read back a drop you created, including per-item moderation status and sales count. - New endpoint: GET /api/external/links — canonical web and Telegram Mini App links (profile, tip, subscribe, buy) for the creator behind the key. - Web tip links: /u/?tip=1 opens the tip sheet; ?tip= opens it prefilled. - Malformed JSON on PUT /timezone, PUT /notifications and POST /register-telegram-chat now returns 400 {"error":"Invalid JSON body"} instead of an unstructured 500. - POST /api/external/posts now builds the returned url from the configured app URL instead of a hardcoded host. - Drops created through the API now carry the correct moderation status at creation — a drop built from approved vault items is born APPROVED and can be attached to a post immediately. - Full documentation: every endpoint is now documented at /developers, with an OpenAPI 3.1 spec, llms.txt / llms-full.txt editions and a Markdown twin of every page. Next: [API terms & acceptable use](https://www.dropfans.io/developers/api-terms.md) or [API reference](https://www.dropfans.io/developers/reference/overview.md). --- Previous: [Register the creator’s personal notification chat](https://www.dropfans.io/developers/reference/register-telegram-chat.md) · Next: [API terms & acceptable use](https://www.dropfans.io/developers/api-terms.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # API terms & acceptable use > What you may and may not do with a creator’s API key. - Source: https://www.dropfans.io/developers/api-terms - Section: Resources - OpenAPI: https://www.dropfans.io/developers/openapi.json Version 2026-08-19. Applying for API access, or using a key a creator gave you, means agreeing to these terms. They sit on top of the [Dropfans platform terms](https://terms.dropfans.io), which always apply. ## Keys - **One key per creator, obtained from that creator.** A key comes from the creator (or their agency) generating it in Vault → API Connect for your app — never from anywhere else. - **Store keys encrypted at rest**, server-side only. Do not log them, embed them in client code, or expose them in error messages. - **Never resell, share or pool keys.** A key connects one creator to one integration; passing it to a third party is a breach. - Delete a creator's key from your systems when they disconnect or ask you to. ## Content and moderation - **No moderation bypass.** Everything created through the API goes through the same review as the dashboard. Attempting to route around review — technically or by misrepresentation — ends the integration. - You are responsible for what your software submits on a creator's behalf; the creator's own platform obligations still apply. ## Data - `check-status` and `earnings` return buyer data (emails, names). **Handle it lawfully** — under GDPR or whichever privacy law applies to you — use it only to serve the creator it belongs to, and never sell it or merge it across creators. - Do not retain buyer data past what serving that creator requires. ## Conduct - **Respect the rate limits** and the `Retry-After` header. Working around limits with key multiplication or IP rotation is a breach. - Represent your app truthfully in your application and to the creators who connect it. ## Enforcement and changes Breaching these terms can get your app suspended — every request with an app-bound key then returns `403` with code `app_suspended` — or removed entirely. Material changes to these terms are announced in the [changelog](https://www.dropfans.io/developers/changelog.md) with a new version string; continued use after a change means acceptance. Questions: support@dropfans.io. Next: [Changelog](https://www.dropfans.io/developers/changelog.md) or [Apply for API access](https://www.dropfans.io/developers/apply). --- Previous: [Changelog](https://www.dropfans.io/developers/changelog.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- ## Agent prompts Copy-paste system prompts for tool-using assistants working this API. ### Posting agent ```text You can post to my Dropfans For You feed. The API contract is at https://www.dropfans.io/developers/openapi.json — fetch it first and follow it exactly. The full documentation index is at https://www.dropfans.io/developers/llms.txt. Authenticate with: Authorization: Bearer House rules: - Captions must be short (under ~80 characters), lowercase, second person, present tense. Almost never use emoji. Ending on a cheap question works best. - To post an image, list my vault and pick an item whose moderationStatus is APPROVED, then create a MEDIA post with that vaultItemId. - A PENDING response is normal, not a failure. Poll GET /api/external/posts/{id} until it turns APPROVED. - A 422 means the caption tripped the word filter — rewrite it and retry once. - A 429 means we hit the daily cap of 5 posts — stop, don't retry in a loop. ``` ### Selling agent ```text You can sell content from my Dropfans vault. The API contract is at https://www.dropfans.io/developers/openapi.json — fetch it first and follow it exactly. The full documentation index is at https://www.dropfans.io/developers/llms.txt. Authenticate with: Authorization: Bearer The flow: 1. List my vault (GET /api/external/vault) and only use items whose moderationStatus is APPROVED. 2. Create a drop with POST /api/external/drops — at most 10 vault items per drop. The price is USD dollars: either 0 (free) or between $5 and $750. Never invent a price — ask me if unsure. 3. Give me the buyUrl from the response, or build the Telegram link by taking the telegram.buyTemplate from GET /api/external/links and substituting the productId. If the telegram block is null, use the web buyUrl only. 4. To check what sold, poll POST /api/external/drops/check-status with the productIds — at most 200 ids per call, chunk larger lists. Never below the $5 minimum on a paid drop, never above $750, and never paste my API key into a shared or public chat. ```