# 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 acts for one 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). An agency’s own key can act for every creator it manages — see [Agencies](https://www.dropfans.io/developers/concepts/agencies.md). - **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/v1/`. 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/v1/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 **Other** for your own scripts (that makes a Personal key), 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/v1/me" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ```javascript tab="Node" const res = await fetch('https://www.dropfans.io/api/external/v1/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/v1/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 can connect once per managed creator the same way, or generate **one key as the agency itself** and act for every creator and AI persona they manage under `/api/external/v1/creators/{creatorId}/…`. See [Acting for a creator](https://www.dropfans.io/developers/concepts/agencies.md). ## Validate and store - Validate the pasted key immediately with [GET /api/external/v1/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 acts for one account and grants the full surface; agency keys can name a managed account. - 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 By default a key acts for **one account** — the one it belongs to — and grants the full surface for it: vault read and write, drops, posts, earnings, balance, profile, links and Telegram notification settings. There are **no scopes** and **no expiry** — a key works until it is revoked. Posts and drops created with a key are authored by the account it acts for. Two exceptions: an agency’s own key can act for any creator or AI persona the agency manages, and your app owner’s own key can act for the accounts your app onboarded — both by calling `/api/external/v1/creators/{creatorId}/…`. See [Acting for a creator](https://www.dropfans.io/developers/concepts/agencies.md). ## 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**. What an app may call is set per app, by category: see [API categories](https://www.dropfans.io/developers/concepts/api-categories.md) (Payouts and Onboarding start off). ## 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 (an unlisted app sits under **Other** there); 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=`. Some apps are approved as unlisted: they appear under **Other** in the picker instead of as a row of their own, and the connect link works the same. - 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: [API categories](https://www.dropfans.io/developers/concepts/api-categories.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # API categories > Each app has a switch per API category. Most are on; Payouts and Onboarding start off until we enable them for your app. - Source: https://www.dropfans.io/developers/concepts/api-categories - Section: Concepts - OpenAPI: https://www.dropfans.io/developers/openapi.json Every endpoint belongs to exactly one **API category**, named on its reference page. Each approved app has a switch per category. A call to a category that is off for your app returns: ```json { "error": "This app does not have the payouts API category enabled. Contact support to enable it.", "code": "api_category_disabled", "category": "payouts" } ``` with status `403`. The switches apply to every key bound to the app — keys themselves are not scoped — and to Dropfans-operated apps too. | Category | `category` | Default | Covers | | --- | --- | --- | --- | | Account | `account` | always on | GET /me, profile, timezone, subscription tier. Always on. | | Creator onboarding | `onboarding` | **off** | Creator sign-up from your page: POST/GET /accounts, KYC sessions. | | Agency roster | `agency` | on | The agency endpoints: roster, personas, invites, roster revenue. | | Vault | `vault` | on | Vault items, folders, tags, video uploads. | | Drops & checkout links | `drops` | on | Drops, previews, check-status, orders. | | For You posts | `posts` | on | For You posts. | | Links | `links` | on | Profile, tip, subscribe and buy links. | | Earnings & balance | `earnings` | on | Earnings, the earnings ledger, tips, GET /balance. | | Payouts | `payouts` | **off** | Payout summary and history, payout methods, the MassPay wallet, payout requests. | | Revenue splits | `splits` | on | Revenue splits between partners. | | Telegram notifications | `notifications` | on | Telegram sale notifications. | | App webhooks | `webhooks` | on | Your app’s webhook endpoint: set, rotate, test. | | CRM dashboard | `crm` | on | Your integration dashboard: GET /crm/app, stats, transactions, accounts. | | Chat | `chat` | **off** | OAuth (/v1) chat resources. Not available yet. | ## Getting a category switched on Write to support from the app owner’s account and say which category you need and why. Payouts can move a creator’s money, so we review that one most closely. [GET /crm/app](https://www.dropfans.io/developers/reference/get-crm-app.md) and [GET /me](https://www.dropfans.io/developers/reference/get-me.md) list your app’s categories with `enabled` for each, so you can hide features that are off. ## Personal keys A personal key (not bound to an app) has no switches: it can call every category for its own account, with two exceptions. App-level endpoints (onboarding, webhooks, the CRM dashboard) need your app owner’s key. Payout **writes** — payout requests, payout methods and the MassPay wallet setup — need an app key with Payouts on (403 `app_key_required`). Reading payouts works with a personal key. Next: [Apps & approval](https://www.dropfans.io/developers/concepts/apps-and-approval.md) or [Errors](https://www.dropfans.io/developers/concepts/errors.md). --- Previous: [Apps & approval](https://www.dropfans.io/developers/concepts/apps-and-approval.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), and use [webhooks](https://www.dropfans.io/developers/webhooks/overview.md) for sales instead of tight polling loops. Next: [Errors](https://www.dropfans.io/developers/concepts/errors.md) or [Webhooks & polling](https://www.dropfans.io/developers/webhooks/overview.md). --- Previous: [API categories](https://www.dropfans.io/developers/concepts/api-categories.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`), an expired video-upload token, or an `X-Dropfans-Account` the key cannot act for (`account_not_managed`, `permission_denied`). | | `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`, `api_category_disabled` with an extra `category` field — see [API categories](https://www.dropfans.io/developers/concepts/api-categories.md)), `429` (`rate_limited`), and `409` (`username_required` on links); the [agency header and endpoints](https://www.dropfans.io/developers/concepts/agencies.md) (`invalid_account_header`, `account_not_managed`, `permission_denied` with an extra `permission` field, `account_header_not_allowed`, `agency_key_required`); and the newer endpoints, which give most errors a code. 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, `total` and `sides` (out) | USD **dollars** | | [GET /payouts/summary](https://www.dropfans.io/developers/reference/get-payout-summary.md), [GET /payouts](https://www.dropfans.io/developers/reference/list-payouts.md), payout.* webhooks | everything `…Cents` (out) | integer **cents** | | [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). The [Payouts endpoints](https://www.dropfans.io/developers/reference/get-payout-summary.md) give the same balance in cents, the payout history, the masked payout methods and whether a payout can be requested now. ## 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: one key for every creator](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: [Acting for a creator: agencies and CRMs](https://www.dropfans.io/developers/concepts/agencies.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # Acting for a creator: agencies and CRMs > An agency’s own key runs every creator and AI persona it manages, and an app owner’s key every account its app onboarded: call /v1/creators/{creatorId}/…. - Source: https://www.dropfans.io/developers/concepts/agencies - Section: Concepts - OpenAPI: https://www.dropfans.io/developers/openapi.json One key can act for many accounts by naming the creator in the path, `/creators/{creatorId}/…`: - **An agency’s own key** acts for every creator who accepted the agency and every AI persona it runs. - **Your app owner’s own key** acts for every account your app created with [POST /accounts](https://www.dropfans.io/developers/reference/create-account.md) — as long as the account still holds the key we returned for it, bound to your app. Deleting that key (the creator can, in their dashboard) revokes your access. Put the creator in the path: every creator endpoint (vault, drops, posts, earnings, profile, links, payout reads, …) is also served at `/api/external/v1/creators/{creatorId}/`. Without it, the key acts for its own account, exactly as before. The agency endpoints, sign-up, webhooks and the CRM dashboard act as the agency or app itself and refuse a creator (400 `creator_scope_not_allowed`). ## 1. Get the right key The agency signs in to the Dropfans dashboard **as itself** (not switched into one of its creators), opens [Vault → API Connect](https://www.dropfans.io/dashboard/vault?apiConnect=1) and generates a key for your app (or a Personal key for its own scripts). That is the agency’s own key. A key the agency generates while switched into a creator is that creator’s key: it stays bound to that one account. For a CRM, use your app owner’s own key — the same one that calls [POST /accounts](https://www.dropfans.io/developers/reference/create-account.md). Keys an agency minted for its creators never act for anyone else. ## 2. Get the account ids [GET /api/external/v1/creators](https://www.dropfans.io/developers/reference/list-creators.md) lists every account your key can act for, with its `relationship` (`managed`, `ai_persona` or `onboarded`), your `permissions` on it, `active` (whether creator-scoped calls work for it right now) and its `creatorPath`. Store the ids — they never change. New personas come from [POST /agency/personas](https://www.dropfans.io/developers/reference/create-persona.md), new creators join through [an invite](https://www.dropfans.io/developers/reference/create-agency-invite.md), new CRM accounts from [POST /accounts](https://www.dropfans.io/developers/reference/create-account.md). ## 3. Call the creator path ```bash # Who am I acting as? curl "https://www.dropfans.io/api/external/v1/creators/cmh4v2q8k0003xy9z7w6v5u4t/me" \ -H "Authorization: Bearer $AGENCY_API_KEY" # Create a drop on that persona curl -X POST "https://www.dropfans.io/api/external/v1/creators/cmh4v2q8k0003xy9z7w6v5u4t/drops" \ -H "Authorization: Bearer $AGENCY_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"Beach set","price":25,"vaultItemIds":["clxv1a2b30001item"]}' ``` The request then behaves exactly as if it was sent with that account’s own key: the drop belongs to the persona, the sale is booked on the persona, the links are the persona’s. Every response carries an `X-Dropfans-Account` header with the id of the account it acted on — assert it if you juggle many accounts. [GET /me](https://www.dropfans.io/developers/reference/get-me.md) returns `key.via`: `agency` (and the agency in `key.managedBy`) or `app`. Send the account **id** only. Usernames are refused, because a username can change hands. Access is checked on every request — there is no session to set up or tear down — and a creator that cannot be honoured is always an error, never a silent fallback to your own account. ### The X-Dropfans-Account header (deprecated) Sending `X-Dropfans-Account: ` on the plain path does the same and keeps working. Responses to such calls carry `Deprecation: true` and a `Link: <…/v1/creators/{id}/…>; rel="successor-version"` header pointing at the path form. Sending both the path and a header that names another account is a 400 `account_mismatch`. ## Permissions An agency needs the matching permission on the account, the same switches as in the dashboard (AI personas have all of them): | Permission | Endpoints | | --- | --- | | `canManageVault` | vault: list, upload, folders, tags, video uploads | | `canManageDrops` | drops, orders, posts | | `canViewStats` | earnings, tips, balance, reading the subscription tier, payouts (read only) | | `canEditProfile` | profile and timezone changes, subscription tier, Telegram notifications | | none | GET /me, GET /timezone, GET /profile, GET /links | Your app owner’s key has every permission except co-performers on the accounts your app onboarded. Each reference page names the permission it needs. Requesting or cancelling a payout and changing payout methods are never possible for another account: those endpoints refuse a creator (`creator_scope_not_allowed` / `account_header_not_allowed`) and keys an agency minted for a managed account (`agency_key_not_allowed`), whatever the permissions. ## Errors | Status | `code` | When | | --- | --- | --- | | 400 | `account_mismatch` | The path names one creator and `X-Dropfans-Account` another. | | 400 | `creator_scope_not_allowed` | A `/v1/creators/{creatorId}/…` call to an endpoint that acts as the key’s own account, agency or app. | | 400 | `invalid_account_header` | The header is not an account id (e.g. a username). | | 403 | `account_not_managed` | The key cannot act for that account: no accepted management, not an account your app onboarded (or it no longer holds your app’s key), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `permission_denied` | The agency lacks the permission named in the `permission` field on that account. | | 400 | `account_header_not_allowed` | The header was sent to an endpoint that acts as the key’s own account, agency or app. | | 403 | `agency_key_required` | An agency endpoint was called with a key that is not a verified agency’s own key. | ## Webhooks An app that holds an agency’s own key also receives [webhook events](https://www.dropfans.io/developers/webhooks/overview.md) for every creator and persona that agency manages (accepted management). Those payloads carry `data.agencyId` next to `data.accountId`, so you can route them. An app is never sent the same event twice, even if it also holds that creator’s own key. ## Rate limits and money - Rate limits stay **per key**: one key acting for ten accounts shares one budget. Read the [rate-limit headers](https://www.dropfans.io/developers/concepts/rate-limits.md) and spread polling accordingly. - Buyers always pay on Dropfans: buy links, tip links and the subscribe sheet open the Dropfans checkout (web or Telegram Mini App), never yours. Use the links from [GET /links](https://www.dropfans.io/developers/reference/get-links.md) for each creator to get each account’s own. - [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/v1/vault?limit=10" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ```javascript tab="Node" const res = await fetch('https://www.dropfans.io/api/external/v1/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/v1/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/v1/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/v1/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/v1/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/v1/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/v1/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/v1/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/v1/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/v1/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: [Acting for a creator: agencies and CRMs](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/v1/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/v1/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/v1/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/v1/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/v1/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/v1/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/v1/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", "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 | | `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/v1/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/v1/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/v1/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/v1/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/v1/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/v1/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/v1/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/v1/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: [Broadcast one drop to many fans](https://www.dropfans.io/developers/guides/broadcast-one-drop.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # Broadcast one drop to many fans > Send the same drop to many fans with one checkout link each, and match every payment back to the fan who paid. - Source: https://www.dropfans.io/developers/guides/broadcast-one-drop - Section: Guides - OpenAPI: https://www.dropfans.io/developers/openapi.json One drop can be sold to any number of fans. Give each fan a personal checkout link that carries a reference of your choice, such as your id for the fan or the conversation. When the fan pays, the order comes back with that reference, so you know who paid and where to deliver the content. > [!NOTE] Same price only > One drop has one price. If fans should pay different amounts, create one drop per price. ## 1. Create the drop once ```bash curl -X POST "https://www.dropfans.io/api/external/v1/drops" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"Friday set","price":25,"vaultItemIds":["clxv1a2b30001item","clxv1a2b30002item"]}' ``` Keep the `productId`. ## 2. One link per fan Pick a reference per recipient: your own id for the conversation, 1–32 characters of `A-Z a-z 0-9 _ -`. On the web it goes in the short `m` parameter; in Telegram it follows the product id. Fill it into the templates from [GET /links](https://www.dropfans.io/developers/reference/get-links.md): | Where | Link | | --- | --- | | Web | `https://www.dropfans.io/buy/{productId}?m={externalRef}` | | Telegram Mini App | `https://t.me//app?startapp=b_{productId}_{externalRef}` | Telegram limits the whole start parameter to 64 characters, so with a 25-character product id the 32-character reference always fits. The reference survives the whole checkout: card, saved card, 3-D Secure and wallet credits. ## 3. Match the payment Each paid order carries the reference as `externalRef`: - **Push:** set a [webhook](https://www.dropfans.io/developers/webhooks/overview.md) and handle `drop.paid`. `data.externalRef` tells you which fan paid. Use `data.orderId` as your idempotency key. - **Poll:** [GET /api/external/v1/orders?externalRef=…](https://www.dropfans.io/developers/reference/list-orders.md) answers "did this fan pay?", and `?productId=…&since=…` sweeps a whole broadcast. ```javascript tab="Node" // drop.paid handler (after verifying the signature) const { orderId, externalRef, productId, buyer } = event.data; if (await alreadyDelivered(orderId)) return; const chat = await findConversationByRef(externalRef); // your lookup await deliverMedia(chat, productId); await markDelivered(orderId); ``` ## Good to know - **A fan who already owns the drop** gets it unlocked without paying again, so no new order is created. Keep showing them the content. - **The reference is not proof of identity.** A fan can edit the link, and would then pay to deliver to someone else's chat. For extra safety, compare `buyer.telegramUserId` (set when they paid inside Telegram) with the chat you expect. - **Refunds and chargebacks** arrive as `drop.refunded` with the same `orderId` and `externalRef`. ## Tips work the same way Send each fan a tip link with your reference and match the tip when it is paid. Fill the `tipWithRefTemplate` from [GET /links](https://www.dropfans.io/developers/reference/get-links.md): | Where | Link | | --- | --- | | Web | `https://www.dropfans.io/u/{username}?tip={usd}&m={externalRef}` (`{usd}` = `1` lets the fan pick) | | Telegram Mini App | `https://t.me//app?startapp=tm_{cents}_{externalRef}_{username}` (`{cents}` = `0` lets the fan pick) | In the Telegram link the reference comes before the username, which may contain underscores, so **there it is 1–20 characters of `A-Z a-z 0-9 -` with no underscore**. The paid tip arrives as `tip.paid` (with the fan's `message`), or poll [GET /api/external/v1/tips?externalRef=…](https://www.dropfans.io/developers/reference/list-tips.md). Refunds arrive as `tip.refunded`. Next: [Webhooks & polling](https://www.dropfans.io/developers/webhooks/overview.md) or [Creator sign-up from your platform](https://www.dropfans.io/developers/guides/creator-signup.md). --- Previous: [Earnings & balance](https://www.dropfans.io/developers/guides/earnings-and-balance.md) · Next: [Creator sign-up from your platform](https://www.dropfans.io/developers/guides/creator-signup.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # Creator sign-up from your platform > Let creators open their account from your own page: one API call creates it and returns the ID-check link to redirect them to. - Source: https://www.dropfans.io/developers/guides/creator-signup - Section: Guides - OpenAPI: https://www.dropfans.io/developers/openapi.json Approved CRM apps can create creator accounts server-side. The creator never fills in our sign-up form: you collect their details, call [POST /api/external/v1/accounts](https://www.dropfans.io/developers/reference/create-account.md) with a key your app owner holds, and redirect them to the ID check we return. > [!NOTE] Ask us first > Creator sign-up is enabled per app. Ask us to enable it for yours. Call it with a key your app owner holds: their own, or one their agency minted for its personas. ## The flow 1. **Your page** collects the profile (email, handle, display name, bio, country of residence, avatar, banner, subscription price) and shows our Terms of Service, Privacy Policy and 2257 statement with an acceptance checkbox. 2. **Your server** calls create-account. Pass the creator's **browser** IP and user agent from the moment they accepted. 3. **Redirect** the creator to `kycUrl` (a full-page redirect or a new tab, never an iframe). 4. **They come back** to your `kycReturnUrl` with `?kyc=approved|declined|in_review`. Show the matching screen, but don't unlock anything yet. 5. **The decision** arrives as the `account.kyc_updated` [webhook](https://www.dropfans.io/developers/webhooks/overview.md). With `kycStatus: "approved"` the account is `active` and can sell and get paid. ```bash curl -X POST "https://www.dropfans.io/api/external/v1/accounts" \ -H "Authorization: Bearer $APP_OWNER_KEY" \ -H "Content-Type: application/json" \ -d '{ "email": "valeria@example.com", "username": "valeria", "displayName": "Valeria", "residencyCountry": "SE", "avatarUrl": "https://cdn.example.com/valeria/avatar.jpg", "subscription": { "priceUsd": 9.99, "interval": "monthly" }, "legalAccepted": true, "acceptedFromIp": "203.0.113.7", "acceptedUserAgent": "Mozilla/5.0 …", "kycReturnUrl": "https://crm.example.com/onboarding/done" }' ``` ## What you get back - `accountId`: keep it. [GET /accounts/{id}](https://www.dropfans.io/developers/reference/get-account.md) shows the status at any time. - `apiKey`: a key for the new account, bound to your app. Upload media and create drops for the creator with it straight away. Drops stay unsellable until the ID check is approved. The key is shown once. - `kycUrl`: the ID check, valid for 24 hours. Get a fresh one with [POST /accounts/{id}/kyc-session](https://www.dropfans.io/developers/reference/create-kyc-session.md), for example after a decline. ## Sign-in and email Without a `password`, we email the creator a link to set one (valid 7 days). With one, the welcome email just points them to sign in. Either way the creator can always use their dashboard directly. ## Commission Create links with your app's key: [GET /links](https://www.dropfans.io/developers/reference/get-links.md) for the profile, tip and subscribe links, and [POST /links](https://www.dropfans.io/developers/reference/create-link.md) for a drop, a fixed tip amount or a per-fan reference. Each is a plain `https://www.dropfans.io/buy/{id}` (`startapp={id}` in Telegram), and your app is stored with the id. A drop bought, a tip sent or a subscription started through one of your links earns your app owner a commission **for life**, including every renewal of that subscription: **5% of the sale** unless agreed otherwise. It is paid out of our platform fee, never taken from the creator, and lands in the app owner's balance with the rest of their earnings. Drops your app creates with its key count as yours through any of their links, including the plain `/buy/{productId}`. Other organic sales earn nothing, and so do links created with a personal (non-app) key, even for an account you manage. Always generate links with your app's key. Older template links with a per-fan `externalRef` still count, but only when your app is the only one that has ever generated links for the account, its own key works the account, and the account has no personal key. If your app's owner is the creator's managing agency, there is no commission: the agency share already pays for those sales. Next: [Broadcast one drop to many fans](https://www.dropfans.io/developers/guides/broadcast-one-drop.md) or [Webhooks & polling](https://www.dropfans.io/developers/webhooks/overview.md). --- Previous: [Broadcast one drop to many fans](https://www.dropfans.io/developers/guides/broadcast-one-drop.md) · Next: [Build your own CRM dashboard](https://www.dropfans.io/developers/guides/crm-dashboard.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # Build your own CRM dashboard > Pull the numbers your integration dashboard shows (revenue, commission, sales and connected accounts) into your own dashboard. - Source: https://www.dropfans.io/developers/guides/crm-dashboard - Section: Guides - OpenAPI: https://www.dropfans.io/developers/openapi.json Your integration dashboard on DropFans (Dashboard → Developer) has three tabs: **Dashboard**, **Transactions** and **Accounts**. Each one has an endpoint that returns the same numbers, so your own dashboard can show what ours shows. | Dashboard tab | Endpoint | |---|---| | Dashboard | [GET /api/external/v1/crm/stats](https://www.dropfans.io/developers/reference/get-crm-stats.md) | | Transactions | [GET /api/external/v1/crm/transactions](https://www.dropfans.io/developers/reference/list-crm-transactions.md) | | Accounts | [GET /api/external/v1/crm/accounts](https://www.dropfans.io/developers/reference/list-crm-accounts.md) | | Header (name, commission) | [GET /api/external/v1/crm/app](https://www.dropfans.io/developers/reference/get-crm-app.md) | > [!NOTE] Owner-held key > Call these with a key your app owner holds: their own, or one their agency minted for its personas. They always answer for the key's own app. Never send `X-Dropfans-Account`. Personal keys and keys a creator minted for themselves get 403. ## Units - Every amount is an **integer number of US cents** (`revenueCents: 4500` = $45.00), **excluding tax** and **before our platform fee**. - `earningsCents` / `crmEarnedCents` is the commission **booked** for your app owner on the sale. It is paid out with the owner's other earnings, so "booked" is not "already paid". - Revenue and commission leave out refunded and charged-back sales. Transactions list them by default with their `status`, like the dashboard does. ## Charts: stats ```bash curl "https://www.dropfans.io/api/external/v1/crm/stats?startDate=2026-10-01&endDate=2026-10-30&tz=America/New_York" \ -H "Authorization: Bearer $APP_OWNER_KEY" ``` `totals` holds the headline tiles: revenue, commission, sales, newly connected accounts, and how many accounts are connected right now. `series` holds one bucket per hour, day, week or month, oldest first, with every empty bucket included as zero, so you can plot it as is. - Days are cut in `tz` (any IANA name; Europe/Stockholm by default, like our dashboard). Each bucket's `start` is its exact UTC instant. - Leave `granularity` out to get the dashboard's automatic choice, or ask for `hour` (up to 3 days), `day`, `week` or `month`. A range is at most 366 days. ## Tables: transactions ```js async function allSales(since) { const out = []; let cursor = null; do { const q = new URLSearchParams({ since, limit: '100', ...(cursor ? { cursor } : {}) }); const res = await fetch(`https://www.dropfans.io/api/external/v1/crm/transactions?${q}`, { headers: { Authorization: `Bearer ${process.env.APP_OWNER_KEY}` }, }); const page = await res.json(); out.push(...page.transactions); cursor = page.nextCursor; } while (cursor); return out; } ``` Newest first, across all your accounts. Filter by `type`, by instants (`since` / `until`) or by calendar days (`startDate` / `endDate` in `tz`). Keep the other parameters unchanged while you follow `nextCursor`. For incremental syncs, store the newest `paidAt` you have and pass it as `since` next time. Upsert by `id`, because a sale's `status` can later change to `refunded` or `charged_back`. ## Accounts [GET /crm/accounts](https://www.dropfans.io/developers/reference/list-crm-accounts.md) lists every account your app has generated links for, all-time: whether it is still `connected` to your app, how much it has sold through your links (`viaLinksCents`) and in total (`allTimeGrossCents`), and your commission. Agencies come with their creators as `children`. Use `flat=true` for one row per selling account, with `agencyId`. An account is connected to the CRM that generated its links most recently. When another CRM did so after you, `movedAway.at` tells you when. We never tell you which CRM it was. Next: [Webhooks & polling](https://www.dropfans.io/developers/webhooks/overview.md) for real-time sales, or [Creator sign-up from your platform](https://www.dropfans.io/developers/guides/creator-signup.md). --- Previous: [Creator sign-up from your platform](https://www.dropfans.io/developers/guides/creator-signup.md) · Next: [Webhooks & polling](https://www.dropfans.io/developers/webhooks/overview.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # Webhooks & polling > Signed webhooks tell your app about sales, refunds, payouts and ID-check results as they happen; polling endpoints cover the same ground as a fallback. - Source: https://www.dropfans.io/developers/webhooks/overview - Section: Webhooks - OpenAPI: https://www.dropfans.io/developers/openapi.json Webhooks are per **app**. Set one https endpoint for your app with [PUT /api/external/v1/webhooks](https://www.dropfans.io/developers/reference/set-webhook.md) using a key your app owner holds (their own, or one their agency minted for its personas). Events about every creator who holds a key for your app (and every account you [created](https://www.dropfans.io/developers/reference/create-account.md)) are then POSTed to it. **Agencies.** If your app holds an [agency’s own key](https://www.dropfans.io/developers/concepts/agencies.md), you also receive the events of every creator and AI persona that agency manages (accepted management). Those payloads carry `data.agencyId` (the agency) in addition to `data.accountId` (the creator), so one endpoint can route the whole roster. An app is never sent the same event twice — if it also holds that creator’s own key, it gets one delivery, without `agencyId`. ## Events | Event | Fires when | `data` | | --- | --- | --- | | `drop.paid` | a buyer completes checkout on a drop | an [Order](https://www.dropfans.io/developers/reference/list-orders.md), including your per-fan `externalRef` | | `drop.refunded` | a paid drop order is refunded or charged back | the Order plus `refundedAt` and `reason` (`refund` or `chargeback`) | | `tip.paid` | a fan's tip settles | a [Tip](https://www.dropfans.io/developers/reference/list-tips.md), including your per-fan `externalRef` and the fan's `message` | | `tip.refunded` | a paid tip is refunded or charged back | the Tip plus `refundedAt` and `reason` (`refund` or `chargeback`) | | `subscription.paid` | a subscription charge is booked — the first charge and every renewal — and once when a free trial starts | a subscription charge (see below) | | `subscription.canceled` | renewal is turned off; the fan keeps access until `accessUntil` | `subscriptionId`, `accountId`, `canceledAt`, `accessUntil`, `canceledBy` (`subscriber`, `creator` (the creator blocked the fan), `support`, `processor`, `account_deleted`), `subscriber` | | `subscription.ended` | access ends | `subscriptionId`, `accountId`, `endedAt`, `reason` (`expired`, `ended_by_support`, `refunded`, `account_deleted`), `subscriber` | | `account.kyc_updated` | an account you created moves through the ID check | `accountId`, `username`, `kycStatus` (`in_review`, `approved`, `declined`), `sellerStatus` | | `payout.requested` | the account requests a payout (dashboard or [API](https://www.dropfans.io/developers/reference/create-payout-request.md)) — one event per side | a [Payout](https://www.dropfans.io/developers/reference/get-payout.md) row, `kind: "request"`, plus `accountId` | | `payout.processing` | a payout is sent to the provider | a Payout row (`kind: "payout"`) plus `accountId` | | `payout.paid` | a payout is paid (sent and confirmed, settled by an admin, or recorded as a manual payout) | a Payout row (`kind` `payout` or `manual`; `requestId` names the request it fulfilled) plus `accountId` | | `payout.failed` | a payout fails at the provider, or a payout request is rejected | a Payout row (`status` `failed` or `rejected`) plus `accountId` | | `ping` | you call [POST /webhooks/test](https://www.dropfans.io/developers/reference/test-webhook.md) | `message` | ### Subscription events `subscription.paid` carries one charge: `subscriptionId`, `accountId` (the creator), `chargeId`, `transactionId`, `kind` (`trial_start`, `first_charge`, `renewal`), `trial`, `amountCents`, `netAmountCents`, `taxCents`, `currency`, `paidAt`, `periodEnd` (access runs until then), `trialEndsAt`, `paidWith` (`card` or `credits`) and `subscriber` (`id`, `email`, `telegramUserId`, `telegramUsername`). A trial start has `kind: "trial_start"`, `trial: true` and `amountCents: 0`; the first paying charge follows as its own `subscription.paid`. `transactionId` is unique per charge (the processor’s id, or the credits debit), so dedupe charges on it. It, `chargeId`, `netAmountCents` and `taxCents` are `null` on a first charge we hear about before its payment record; a trial start has no `transactionId` either. `subscriptionId` stays the same across renewals. A fan who cancels sends `subscription.canceled` at once and `subscription.ended` when the paid time runs out. Support ending a subscription now, a refund or a deleted account send both together. Free tiers send none of these. ### Payout events `data` is exactly what [GET /api/external/v1/payouts/{id}](https://www.dropfans.io/developers/reference/get-payout.md) returns for that row, as it is when the event is sent, plus `accountId`; money in integer cents, destinations masked. Each transition is sent once; a payout that an admin retries reports `payout.processing` (and then `payout.paid` or `payout.failed`) again for its new attempt. A request that is paid is followed by `payout.paid` for the payout that fulfilled it — match them with `requestId`. Requests cancelled by the account send no event. Apps that know an account through an [agency’s own key](https://www.dropfans.io/developers/concepts/agencies.md) only receive its payout events when the agency holds `canViewStats` on it. Apps whose webhook has no event filter (the default: everything) receive these new events automatically — ignore types you do not handle. ## Envelope Every delivery is a JSON `POST`: ```json { "id": "evt_2b7c91d0e4a5f6b7c8d9e0f1", "type": "drop.paid", "created": "2026-10-17T16:20:01.000Z", "data": { "orderId": "clxor0001order", "productId": "clxdr0p000001prod", "accountId": "clx2f8a1b0001qw3k", "externalRef": "tg_48213377", "amountCents": 2750, "currency": "USD", "paidAt": "2026-10-17T16:20:00.000Z", "paidWith": "card", "status": "paid", "buyer": { "id": "clxbu0001buyer", "email": "buyer@example.com", "telegramUserId": "48213377", "telegramUsername": "fan_one" } } } ``` Headers: | Header | Value | | --- | --- | | `DropFans-Event-Id` | the event id (same as `id`) — dedupe on it | | `DropFans-Event` | the event type | | `DropFans-Signature` | `t=,v1=` | ## Verify the signature The signature is an HMAC-SHA256, keyed with your `whsec_…` secret, over `.`. Compute it on the **raw** bytes, before any JSON parsing. Compare in constant time, and reject timestamps more than 5 minutes old. ```javascript tab="Node" import crypto from 'node:crypto'; export function verify(rawBody, header, secret) { const parts = Object.fromEntries(header.split(',').map((p) => p.split('='))); const t = Number(parts.t); if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex'); const a = Buffer.from(expected); const b = Buffer.from(parts.v1 ?? ''); return a.length === b.length && crypto.timingSafeEqual(a, b); } ``` ```python tab="Python" import hashlib, hmac, time def verify(raw_body: bytes, header: str, secret: str) -> bool: parts = dict(p.split("=", 1) for p in header.split(",")) t = int(parts.get("t", 0)) if not t or abs(time.time() - t) > 300: return False expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, parts.get("v1", "")) ``` ## Delivery and retries - Answer any **2xx** within 5 seconds to acknowledge. Do the work afterwards (queue it). Redirects are not followed. - Anything else is retried with backoff (about 1 min, 5 min, 15 min, 1 h, 3 h, then every 6 h), up to 8 attempts in total. After that the event is marked failed. - Events can arrive more than once and out of order. Dedupe on `DropFans-Event-Id` (or on `orderId` for sales, `transactionId` for subscription charges). - [GET /api/external/v1/webhooks](https://www.dropfans.io/developers/reference/get-webhook.md) shows your last 20 deliveries with status codes and errors. ## Polling (fallback) Webhooks can be missed if your endpoint is down for longer than the retry window. Keep a slow sweep as a safety net and budget it against your [rate tier](https://www.dropfans.io/developers/concepts/rate-limits.md): 5,000 requests a day (personal) and 50,000 per key (app). ### 1. Sales — orders and check-status [GET /api/external/v1/orders](https://www.dropfans.io/developers/reference/list-orders.md) with `since` = your last sweep returns every paid order with its `externalRef`. [POST /drops/check-status](https://www.dropfans.io/developers/reference/check-drop-status.md) (up to 200 product ids per call) does the same per product. With webhooks on, one sweep every 15–60 minutes is plenty. ### 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`. ### 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. Next: [Broadcast one drop to many fans](https://www.dropfans.io/developers/guides/broadcast-one-drop.md) or [Creator sign-up from your platform](https://www.dropfans.io/developers/guides/creator-signup.md). --- Previous: [Build your own CRM dashboard](https://www.dropfans.io/developers/guides/crm-dashboard.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 & polling](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/v1/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/v1/vault) and only use items whose moderationStatus is APPROVED. 2. Create a drop with POST /api/external/v1/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/v1/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/v1/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: [OAuth apps](https://www.dropfans.io/developers/api-v1/oauth-apps.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # OAuth apps > Register an OAuth app, send creators, fans or agencies through the authorization-code flow with PKCE, and keep their tokens fresh with rotating refresh tokens. - Source: https://www.dropfans.io/developers/api-v1/oauth-apps - Section: API v1 (OAuth apps) - OpenAPI: https://www.dropfans.io/developers/openapi.json API v1 at `https://api.dropfans.io` is authorized with OAuth 2.0 access tokens instead of API keys: the account (a creator, a fan or an agency) signs in on Dropfans, approves the scopes your app asks for, and your app receives a token that acts for that account. The authorization server is `https://auth.dropfans.io`, with OpenID Connect on top. ## Register your app 1. [Apply for API access](https://www.dropfans.io/developers/apply) and wait for the app to be approved. 2. Ask the Dropfans team for an OAuth client for that app, with the redirect URIs you will use (exact https URLs; `http://localhost` is fine for development) and the scopes it needs. 3. You receive a **client id** and a **client secret**. The secret is shown once: store it server-side only. If it leaks, ask for a new one; the old one stops working immediately. Each API category of your app (chat, posts, vault, …) has its own switch on our side, so a scope you were granted can still answer 404 while its category is off for your app. ## Endpoints | Endpoint | Purpose | | --- | --- | | `GET https://auth.dropfans.io/oauth2/auth` | Authorization: the account signs in and approves your scopes | | `POST https://auth.dropfans.io/oauth2/token` | Exchange a code, or refresh a token | | `POST https://auth.dropfans.io/oauth2/revoke` | Revoke an access or refresh token (and its whole grant) | | `GET https://auth.dropfans.io/userinfo` | OpenID Connect claims of the token’s account | | `GET https://auth.dropfans.io/.well-known/openid-configuration` | Discovery document | | `GET https://auth.dropfans.io/.well-known/jwks.json` | Keys that sign the `id_token` (RS256) | | `GET https://auth.dropfans.io/oauth2/sessions/logout` | End-session endpoint | Read the endpoints from discovery rather than hard-coding them where your OAuth library supports it. ## 1. Send the account to the authorization endpoint PKCE with **S256 is required** for every client: create a random `code_verifier`, send `code_challenge = BASE64URL(SHA256(code_verifier))`. Requests without it, or with `plain`, are refused. ```http GET https://auth.dropfans.io/oauth2/auth?response_type=code &client_id=YOUR_CLIENT_ID &redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback &scope=openid%20offline_access%20read%3Aself%20read%3Achat &state=RANDOM_STATE &code_challenge=BASE64URL_SHA256_OF_VERIFIER &code_challenge_method=S256 ``` `redirect_uri` must match a registered URI exactly. `scope` is space-separated; scopes your client is not allowed, or that the account type cannot hold, are left out of the grant (check `scope` in the token response). The account comes back to your `redirect_uri` with `code` and your `state`, or with `error` and `error_description`. The code is single-use and expires after 60 seconds. Presenting a code twice revokes every token it produced. ## 2. Exchange the code Authenticate the client with HTTP Basic (`client_secret_basic`: `client_id:client_secret`, base64). It is the only client authentication method. ```bash curl -X POST https://auth.dropfans.io/oauth2/token \ -u "$CLIENT_ID:$CLIENT_SECRET" \ -d grant_type=authorization_code \ -d code="$CODE" \ -d redirect_uri=https://app.example.com/callback \ -d code_verifier="$CODE_VERIFIER" ``` ```json { "access_token": "…", "token_type": "Bearer", "expires_in": 3600, "scope": "openid offline_access read:self read:chat", "refresh_token": "…", "id_token": "eyJ…" } ``` `refresh_token` is only issued when the grant includes `offline_access` (or `offline`); `id_token` only with `openid`. Its `sub` is the account’s UUID, the same `uuid` that [`GET /users/me`](https://www.dropfans.io/developers/api-v1/resources.md) returns. ## 3. Call the API ```bash curl https://api.dropfans.io/v1/users/me -H "Authorization: Bearer $ACCESS_TOKEN" ``` See [API v1 overview](https://www.dropfans.io/developers/api-v1/overview.md) for paging, versions, errors and limits. ## 4. Refresh ```bash curl -X POST https://auth.dropfans.io/oauth2/token -u "$CLIENT_ID:$CLIENT_SECRET" \ -d grant_type=refresh_token -d refresh_token="$REFRESH_TOKEN" ``` Refresh tokens **rotate**: every refresh returns a new access token and a new refresh token, and the one you sent stops working. Store the new pair before using it. If the same refresh token is presented again within 60 seconds (a retried request), you get the same new pair back; after that, reuse is treated as theft and **the whole grant is revoked**. The account then has to authorize your app again. ## Token lifetimes | Token | Lifetime | | --- | --- | | Authorization code | 60 seconds, single use | | Access token | 60 minutes | | Refresh token | 30 days from issue; each refresh starts a new one | | `id_token` | 60 minutes | ## Revoke ```bash curl -X POST https://auth.dropfans.io/oauth2/revoke -u "$CLIENT_ID:$CLIENT_SECRET" -d token="$TOKEN" ``` Revoking either token ends the grant. Webhook deliveries for that account stop as soon as no live grant of your app is left. Accounts can also disconnect your app themselves. ## Userinfo `GET /userinfo` with the access token returns `sub` (the account UUID), plus `preferred_username`, `name`, `picture`, `email` and `email_verified` when the grant includes `read:self`. ## Scopes System scopes: `openid`, `offline_access`, `offline` (`openid` adds an `id_token`; `offline_access` or `offline` adds a refresh token). API scopes, and which account types can hold them: | Scope | Grants | Fan | Creator | Agency | | --- | --- | --- | --- | --- | | `read:self` | Read your profile and account details | ✓ | ✓ | ✓ | | `read:chat` | Read your chats and messages | ✓ | ✓ | ✓ | | `write:chat` | Send messages and manage chats | ✓ | ✓ | ✓ | | `read:experience` | Read fan-facing experiences (not served yet) | ✓ | ✓ | ✓ | | `write:experience` | Publish or unpublish fan-facing experiences (not served yet) | — | ✓ | ✓ | | `read:fan` | Read your fans and their activity | — | ✓ | ✓ | | `read:post` | Read your posts, comments and likes | — | ✓ | ✓ | | `write:post` | Create, edit and delete posts | — | ✓ | ✓ | | `read:media` | Read your vault media | — | ✓ | ✓ | | `write:media` | Upload and manage vault media and folders | — | ✓ | ✓ | | `read:creator` | Read creator profiles and the creators you manage | — | ✓ | ✓ | | `write:creator` | Change creator profile and settings | — | ✓ | ✓ | | `read:insights` | Read earnings and audience insights | — | ✓ | ✓ | | `read:tracking_links` | Read tracking links and who joined through them | — | ✓ | ✓ | | `write:tracking_links` | Create and delete tracking links | — | ✓ | ✓ | | `read:agency` | Read your agency and its team | — | — | ✓ | | `write:agency` | Manage agency invites and team members | — | — | ✓ | Each API v1 operation needs every scope listed for it in the [resource list](https://www.dropfans.io/developers/api-v1/resources.md); a token without them gets `403 {"error":"Insufficient scopes"}`. ## Errors The authorization and token endpoints answer in the standard OAuth form `{"error": "invalid_grant", "error_description": "…"}` (`invalid_request`, `invalid_client`, `invalid_grant`, `invalid_scope`, `unsupported_grant_type`, `access_denied`, …). The token endpoint allows 120 requests a minute per client and 60 per IP address. --- Previous: [Agent prompt](https://www.dropfans.io/developers/build-with-ai/agent-prompt.md) · Next: [API v1 overview](https://www.dropfans.io/developers/api-v1/overview.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # API v1 overview > Base URL, versions, cursor and page paging, acting for a creator as an agency, error shapes, rate limits, money and ids for API v1 with OAuth tokens. - Source: https://www.dropfans.io/developers/api-v1/overview - Section: API v1 (OAuth apps) - OpenAPI: https://www.dropfans.io/developers/openapi.json API v1 serves creators’ accounts, audience, insights, vault media, posts, tracking links, webhooks and chat to [OAuth apps](https://www.dropfans.io/developers/api-v1/oauth-apps.md). Every request carries `Authorization: Bearer `. The base URL is `https://api.dropfans.io`. ## Paths and paging | Prefix | Paging | List response | | --- | --- | --- | | `/v1/…` | `cursor` + `limit` | `{"data": […], "nextCursor": "…" \| null}` | | `/v0/…` and unprefixed (`/users/me`) | `page` (from 1) + `size` | `{"data": […], "pagination": {"page": 1, "size": 20, "hasMore": true}}` | The three prefixes serve the same operations; pick one style and keep it. On `/v1`, pass `nextCursor` back as `cursor` until it is `null`. A cursor is opaque: do not build or edit one. ## Version header Send the API version in a request header whose name ends in `-API-Version`, for example `X-Dropfans-API-Version: 2025-06-26`. Without one you get `2025-06-26`, the current version. An unknown version is `400 {"error": "Unsupported API version", "message": "…"}`. ## Acting for a creator (agencies) A token acts for the account that authorized it. An agency’s token can also act for each creator it manages: prefix the path with `/creators/{creatorUserUuid}`, for example `GET /v1/creators/{creatorUserUuid}/posts`. Which operations have this form, and the extra scopes it needs (usually `read:creator`), is in the [resource list](https://www.dropfans.io/developers/api-v1/resources.md). The agency needs the matching permission on that creator; otherwise the call is `403 {"message": "Forbidden"}`. `GET /v1/creators` lists the creators your token can act for. ## Errors | Status | Body | When | | --- | --- | --- | | 400 | `{"errors": ["…"]}` | Validation failed (one message per problem) | | 400 | `{"message": "Invalid UUID format …"}` | A path parameter is not a UUID | | 401 | `{"error": "Unauthorized"}` | Token missing, expired or revoked | | 403 | `{"error": "Insufficient scopes"}` | The token lacks a scope the operation needs | | 403 | `{"message": "Forbidden"}` | A creator path you cannot act for | | 404 | `{"message": "Resource not found"}` | Unknown path, an id that is not yours, or a category that is off for your app | | 429 | `{"error": "Too many requests"}` + `Retry-After` | Rate limit reached | ## Rate limits 200 requests per 60 seconds per (account, app); a call in the `/creators/{creatorUserUuid}` form counts against that creator’s own budget as well, so one busy creator does not starve the rest of a roster. Every response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`. ## Money, ids and times - **Money** is an integer number of USD cents (`1999` = $19.99). - **Ids** are UUIDs: accounts, posts, media, chats and every other object. Internal ids never appear. - **Times** are ISO 8601 in UTC. ## Onboarding creators `POST /v1/onboarding/creators` creates a creator account and returns `kycUrl`. Send the creator to it in their own browser: they accept the Dropfans Terms of Service there, and the acceptance is recorded with their IP address and user agent, never your server’s. Then they continue to ID verification. If your app already showed the Terms and collected the acceptance in the creator’s browser, you may add `acceptedFromIp` and `acceptedUserAgent` (that browser’s IP address and user agent) to the request body. The acceptance is then recorded at once and `kycUrl` goes straight to ID verification. ## Chat The chat operations (`/chats/**`, `/agencies/chats/**`, online subscribers) and the chat webhook topics are available once chat is enabled for your app. Until then they answer `404 {"message": "Resource not found"}`, like any path that does not exist. Next: [Resource list](https://www.dropfans.io/developers/api-v1/resources.md) · [Webhooks](https://www.dropfans.io/developers/api-v1/webhooks.md). --- Previous: [OAuth apps](https://www.dropfans.io/developers/api-v1/oauth-apps.md) · Next: [API v1 resource list](https://www.dropfans.io/developers/api-v1/resources.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # API v1 resource list > Every API v1 operation that is served today, with the scopes it needs and whether it also has the agency creator-path form. - Source: https://www.dropfans.io/developers/api-v1/resources - Section: API v1 (OAuth apps) - OpenAPI: https://www.dropfans.io/developers/openapi.json Generated from the routes themselves, so it lists exactly what is served. Paths are relative to `https://api.dropfans.io/v1`; every operation is also served under `/v0` and unprefixed (page/size paging) unless marked “v1 only”. “Creator path” means the operation is also served at `/creators/{creatorUserUuid}` for an agency acting for a creator, with the extra scopes shown. Paths that are not listed answer `404 {"message": "Resource not found"}`. 109 operations. ## Account and audience | Method | Path | Scopes | Also | | --- | --- | --- | --- | | `GET` | `/creators/{creatorUserUuid}/account` | `read:creator` | creator path only | | `GET` | `/creators/{creatorUserUuid}/blocks` | `read:creator` `read:chat` `read:fan` | creator path only | | `POST` | `/creators/{creatorUserUuid}/blocks/{userUuid}` | `write:chat` `read:creator` | creator path only | | `DELETE` | `/creators/{creatorUserUuid}/blocks/{userUuid}` | `write:chat` `read:creator` | creator path only | | `PATCH` | `/creators/{creatorUserUuid}/subscription-price` | `write:creator` | creator path only | | `GET` | `/followers` | `read:fan` | creator path (+ `read:creator`) | | `GET` | `/subscribers` | `read:fan` | creator path (+ `read:creator`) | | `GET` | `/subscribers/{userUuid}` | `read:fan` | creator path (+ `read:creator`) | | `GET` | `/subscribers/{userUuid}/history` | `read:fan` | creator path (+ `read:creator`); v1 only | | `GET` | `/users/account` | `read:self` | — | | `GET` | `/users/me` | `read:self` | — | | `PATCH` | `/users/me/subscription-price` | `write:creator` | — | ## Agencies and creators | Method | Path | Scopes | Also | | --- | --- | --- | --- | | `POST` | `/agencies/creator-invites` | `write:agency` | — | | `GET` | `/agencies/earnings` | `read:agency` `read:creator` | — | | `GET` | `/agencies/followers-history` | `read:agency` `read:creator` | — | | `GET` | `/agencies/subscribers` | `read:agency` `read:creator` | — | | `GET` | `/agencies/subscribers-history` | `read:agency` `read:creator` | — | | `GET` | `/creators` | `read:creator` | — | ## Insights | Method | Path | Scopes | Also | | --- | --- | --- | --- | | `GET` | `/insights/earnings` | `read:insights` | creator path (+ `read:creator`) | | `GET` | `/insights/earnings/summary` | `read:insights` | creator path (+ `read:creator`) | | `GET` | `/insights/fans` | `read:insights` `read:fan` | creator path (+ `read:creator`) | | `GET` | `/insights/fans/{userUuid}` | `read:insights` `read:fan` | creator path (+ `read:creator`) | | `POST` | `/insights/fans/batch` | `read:insights` `read:fan` | creator path (+ `read:creator`) | | `POST` | `/insights/fans/profiles` | `read:fan` | — | | `GET` | `/insights/spending` | `read:insights` | creator path (+ `read:creator`) | | `GET` | `/insights/subscribers` | `read:insights` | creator path (+ `read:creator`) | | `GET` | `/insights/top-spenders` | `read:insights` `read:fan` | creator path (+ `read:creator`) | ## Media and vault | Method | Path | Scopes | Also | | --- | --- | --- | --- | | `GET` | `/media` | `read:media` | creator path | | `GET` | `/media/{uuid}` | `read:media` | creator path | | `PATCH` | `/media/{uuid}` | `write:media` | creator path | | `DELETE` | `/media/{uuid}` | `write:media` | creator path | | `GET` | `/media/bulk` | `read:media` | creator path | | `POST` | `/media/uploads` | `write:media` | creator path | | `PATCH` | `/media/uploads/{uploadId}` | `write:media` | creator path | | `GET` | `/media/uploads/{uploadId}/parts/{partNumber}/url` | `write:media` | creator path | | `GET` | `/media/uploads/{uploadId}/parts/urls` | `write:media` | creator path | | `GET` | `/vault/folders` | `read:media` | creator path | | `POST` | `/vault/folders` | `write:media` | creator path | | `GET` | `/vault/folders/{folderName}` | `read:media` | creator path | | `PATCH` | `/vault/folders/{folderName}` | `write:media` | creator path | | `DELETE` | `/vault/folders/{folderName}` | `write:media` | creator path | | `GET` | `/vault/folders/{folderName}/media` | `read:media` | creator path | | `POST` | `/vault/folders/{folderName}/media` | `write:media` | creator path | | `PATCH` | `/vault/folders/{folderName}/media/{mediaUuid}` | `write:media` | — | | `DELETE` | `/vault/folders/{folderName}/media/{mediaUuid}` | `write:media` | creator path | ## Posts | Method | Path | Scopes | Also | | --- | --- | --- | --- | | `POST` | `/agencies/posts` | `write:post` `write:creator` | — | | `GET` | `/posts` | `read:post` | creator path | | `POST` | `/posts` | `write:post` | creator path | | `GET` | `/posts/{uuid}` | `read:post` | creator path | | `PATCH` | `/posts/{uuid}` | `write:post` | creator path | | `DELETE` | `/posts/{uuid}` | `write:post` | creator path | | `GET` | `/posts/{uuid}/comments` | `read:post` | creator path | | `POST` | `/posts/{uuid}/comments` | `write:post` | creator path | | `DELETE` | `/posts/{uuid}/comments/{commentUuid}` | `write:post` | creator path | | `GET` | `/posts/{uuid}/likes` | `read:post` | creator path | ## Media links | Method | Path | Scopes | Also | | --- | --- | --- | --- | | `GET` | `/media-links` | `read:creator` | — | | `POST` | `/media-links` | `write:creator` | — | | `DELETE` | `/media-links/{uuid}` | `write:creator` | — | ## Tracking links | Method | Path | Scopes | Also | | --- | --- | --- | --- | | `GET` | `/tracking-links` | `read:tracking_links` | creator path | | `POST` | `/tracking-links` | `write:tracking_links` | creator path | | `DELETE` | `/tracking-links/{uuid}` | `write:tracking_links` | creator path | | `GET` | `/tracking-links/{uuid}/users` | `read:tracking_links` | creator path | ## Onboarding | Method | Path | Scopes | Also | | --- | --- | --- | --- | | `POST` | `/onboarding/creators` | `write:creator` | — | ## Webhooks | Method | Path | Scopes | Also | | --- | --- | --- | --- | | `GET` | `/webhooks/subscriptions` | `read:self` | — | | `POST` | `/webhooks/subscriptions` | `read:self` | — | | `DELETE` | `/webhooks/subscriptions/{id}` | `read:self` | — | | `POST` | `/webhooks/test` | `read:self` | — | ## Chat (available once chat is enabled) | Method | Path | Scopes | Also | | --- | --- | --- | --- | | `GET` | `/agencies/chats` | `read:agency` `read:chat` | — | | `POST` | `/agencies/chats/messages/batch` | `read:agency` `read:chat` | — | | `GET` | `/chats` | `read:chat` | creator path (+ `read:creator`) | | `POST` | `/chats` | `write:chat` | creator path (+ `read:creator`) | | `GET` | `/chats/{userUuid}` | `read:chat` | — | | `PATCH` | `/chats/{userUuid}` | `write:chat` | creator path (+ `read:creator`) | | `GET` | `/chats/{userUuid}/media` | `read:chat` | creator path (+ `read:creator`) | | `POST` | `/chats/{userUuid}/message` | `write:chat` | creator path (+ `read:creator`) | | `GET` | `/chats/{userUuid}/messages` | `read:chat` | creator path (+ `read:creator`) | | `GET` | `/chats/{userUuid}/messages/{messageUuid}` | `read:chat` | — | | `DELETE` | `/chats/{userUuid}/messages/{messageUuid}` | `write:chat` | creator path (+ `read:creator`) | | `GET` | `/chats/{userUuid}/messages/{messageUuid}/media` | `read:chat` | creator path (+ `read:creator`) | | `GET` | `/chats/{userUuid}/notes` | `read:chat` `read:fan` | creator path (+ `read:creator`) | | `PUT` | `/chats/{userUuid}/notes` | `write:chat` | creator path (+ `read:creator`) | | `POST` | `/chats/{userUuid}/typing` | `write:chat` | — | | `GET` | `/chats/automated-messages` | `read:creator` | creator path | | `PUT` | `/chats/automated-messages/{trigger}` | `write:creator` | creator path | | `DELETE` | `/chats/automated-messages/{trigger}` | `write:creator` | creator path | | `GET` | `/chats/lists/custom` | `read:chat` `read:fan` | creator path (+ `read:creator`) | | `POST` | `/chats/lists/custom` | `write:chat` | creator path (+ `read:creator`) | | `GET` | `/chats/lists/custom/{uuid}` | `read:chat` `read:fan` | creator path (+ `read:creator`) | | `PATCH` | `/chats/lists/custom/{uuid}` | `write:chat` | creator path (+ `read:creator`) | | `DELETE` | `/chats/lists/custom/{uuid}` | `write:chat` | creator path (+ `read:creator`) | | `POST` | `/chats/lists/custom/{uuid}/members` | `write:chat` | creator path (+ `read:creator`) | | `DELETE` | `/chats/lists/custom/{uuid}/members/{userUuid}` | `write:chat` | creator path (+ `read:creator`) | | `GET` | `/chats/lists/smart` | `read:chat` `read:fan` | creator path (+ `read:creator`) | | `GET` | `/chats/lists/smart/{uuid}` | `read:chat` `read:fan` | creator path (+ `read:creator`) | | `GET` | `/chats/mass-messages` | `read:chat` | creator path (+ `read:creator`) | | `POST` | `/chats/mass-messages` | `write:chat` `read:fan` | creator path (+ `read:creator`) | | `PATCH` | `/chats/mass-messages/{messageUuid}` | `write:chat` | creator path (+ `read:creator`) | | `DELETE` | `/chats/mass-messages/{messageUuid}` | `write:chat` | creator path (+ `read:creator`) | | `POST` | `/chats/messages/batch` | `read:chat` | creator path (+ `read:creator`) | | `POST` | `/chats/statuses` | `read:chat` | creator path (+ `read:creator`) | | `GET` | `/chats/templates` | `read:chat` | creator path (+ `read:creator`) | | `POST` | `/chats/templates` | `write:chat` | — | | `GET` | `/chats/templates/{templateUuid}` | `read:chat` | creator path (+ `read:creator`) | | `DELETE` | `/chats/templates/{templateUuid}` | `write:chat` | — | | `GET` | `/chats/templates/folders` | `read:chat` | creator path (+ `read:creator`) | | `POST` | `/chats/templates/folders` | `write:chat` | — | | `PATCH` | `/chats/templates/folders/{folderUuid}` | `write:chat` | — | | `GET` | `/chats/unread` | `read:chat` | creator path (+ `read:creator`) | | `GET` | `/creators/{creatorUserUuid}/subscribers/online` | `read:creator` `read:fan` | creator path only | --- Previous: [API v1 overview](https://www.dropfans.io/developers/api-v1/overview.md) · Next: [API v1 webhooks](https://www.dropfans.io/developers/api-v1/webhooks.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # API v1 webhooks > Subscribe a creator’s account to webhook topics with its OAuth token, verify the HMAC-SHA256 signature on every delivery, and test a topic on demand. - Source: https://www.dropfans.io/developers/api-v1/webhooks - Section: API v1 (OAuth apps) - OpenAPI: https://www.dropfans.io/developers/openapi.json Webhook subscriptions belong to one account and one app: subscribe with that account’s access token, and the topics you chose are POSTed to your URL whenever they happen on the account, for as long as your app holds a live grant with the topic’s scope. ## Subscribe ```bash curl -X POST https://api.dropfans.io/v1/webhooks/subscriptions \ -H "Authorization: Bearer $ACCESS_TOKEN" -H "Content-Type: application/json" \ -d '{"url": "https://app.example.com/hooks", "events": ["creator.payment.succeeded", "creator.follow.created"]}' ``` Answers `201 {"id": "…", "signingSecret": "whsec_…"}`. The signing secret is returned **once**: store it. Before the subscription is saved we send an unsigned verification request, `{"type": "webhook.verification", "timestamp": "…"}`, to your URL; any 2xx within 8 seconds passes (redirects are not followed). The URL must be public https. The token needs `read:self` plus the scope of every topic. Up to 50 topics per subscription and 20 subscriptions per (account, app). `GET /v1/webhooks/subscriptions` lists them and `DELETE /v1/webhooks/subscriptions/{id}` removes one. ## Delivery Each delivery is a JSON `POST` with the topic in the `X-Fanvue-Topic` header and the signature in the signature header, `X-Fanvue-Signature`. Envelope topics have this body: ```json { "id": "3f1c2a9e-5b7d-4c8e-9f10-2a3b4c5d6e7f", "type": "creator.follow.created", "timestamp": "2026-11-01T12:00:00.000Z", "data": { "object": "follow", "…": "…" } } ``` `data` uses snake_case keys; money is integer cents. `id` identifies the occurrence and never changes across retries, so use it to de-duplicate; `timestamp` is set on each attempt. Legacy topics send their own flat camelCase body with an `eventId` instead of the envelope. Answer with any 2xx quickly and do the work afterwards. Other answers and timeouts are retried with backoff, up to 8 attempts in all. Deliveries stop when the subscription is deleted, the grant is revoked, or the topic’s scope or category is no longer allowed for your app. ## Verify the signature The signature header has the form `t=,v0=`, where `` is HMAC-SHA256 with your signing secret over the string `"."`. Compute it over the raw request bytes (before any JSON parsing), compare in constant time, and reject timestamps more than about five minutes away from your clock. ```js tab="Node" import crypto from 'node:crypto'; function verify(rawBody, header, secret) { const m = /^t=(\d+),v0=([0-9a-f]{64})$/.exec(header || ''); if (!m) return false; const want = crypto.createHmac('sha256', secret).update(`${m[1]}.${rawBody}`).digest('hex'); const fresh = Math.abs(Date.now() / 1000 - Number(m[1])) < 300; return fresh && crypto.timingSafeEqual(Buffer.from(want), Buffer.from(m[2])); } ``` ```python tab="Python" import hashlib, hmac, re, time def verify(raw_body: bytes, header: str, secret: str) -> bool: m = re.fullmatch(r"t=(\d+),v0=([0-9a-f]{64})", header or "") if not m: return False want = hmac.new(secret.encode(), m[1].encode() + b"." + raw_body, hashlib.sha256).hexdigest() return abs(time.time() - int(m[1])) < 300 and hmac.compare_digest(want, m[2]) ``` ## Test a topic `POST /v1/webhooks/test` with `{"event": ""}` sends a sample of that topic (ids prefixed `TEST-`) to your subscriptions that include it: `202 {"id": "…", "event": "…"}`. It is `404` when no subscription of yours includes the topic, and `400` for legacy topics and for topics that cannot be tested yet (including chat topics while chat is not enabled for your app). 30 calls a minute. ## Topics Topics delivered today: | Topic | Scope | Format | | --- | --- | --- | | `payout.paid` | `read:creator` | envelope | | `payout.failed` | `read:creator` | envelope | | `follow.new` | `read:creator` | legacy flat body | | `message.read` (chat) | `read:chat` | legacy flat body | | `message.received` (chat) | `read:chat` | legacy flat body | | `purchase.new` | `read:creator` | legacy flat body | | `subscription.new` | `read:creator` | legacy flat body | | `subscription.renewed` | `read:creator` | legacy flat body | | `subscription.cancelled` | `read:creator` | legacy flat body | | `subscription.expired` | `read:creator` | legacy flat body | | `tip.new` | `read:creator` | legacy flat body | | `creator.payment.succeeded` | `read:creator` | envelope | | `creator.subscription.activated` | `read:creator` | envelope | | `creator.subscription.renewed` | `read:creator` | envelope | | `creator.subscription.deactivated` | `read:creator` | envelope | | `creator.subscription.cancel_at_period_end_changed` | `read:creator` | envelope | | `creator.post.liked` | `read:post` | envelope | | `creator.post.commented` | `read:post` | envelope | | `creator.message.deleted` (chat) | `read:chat` | envelope | | `creator.refund.created` | `read:creator` | envelope | | `creator.dispute.created` | `read:creator` | envelope | | `creator.follow.created` | `read:creator` | envelope | | `creator.message.received` (chat) | `read:chat` | envelope | | `creator.message.sent` (chat) | `read:chat` | envelope | | `creator.message.mass_sent` (chat) | `read:chat` | envelope | | `creator.message.read` (chat) | `read:chat` | envelope | The subscription endpoint also accepts the other published topic names, so an app can register its full list without errors, but nothing is delivered for them yet. Chat topics are delivered once chat is enabled for your app. Ignore topics you do not handle. --- Previous: [API v1 resource list](https://www.dropfans.io/developers/api-v1/resources.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/v1/` on that host. The unversioned paths (`/api/external/…` without `v1`) are permanent aliases of the same endpoints, so existing integrations keep working. 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 acts for one account and grants the full surface — details in [Authentication & API keys](https://www.dropfans.io/developers/concepts/authentication.md). An agency’s own key (for a creator or AI persona it manages) and your app owner’s own key (for an account your app onboarded) may call any endpoint outside the Agencies and CRM groups under `/api/external/v1/creators/{creatorId}/…` to act for that account; the older `X-Dropfans-Account: ` header still works but is deprecated. Every response echoes the account acted on in the `X-Dropfans-Account` response header. See [Acting for a creator](https://www.dropfans.io/developers/concepts/agencies.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: [API v1 webhooks](https://www.dropfans.io/developers/api-v1/webhooks.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/v1/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 Also served at `/api/external/me` (the unversioned alias). 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. Called for a creator (`/v1/creators/{creatorId}/me`, or the deprecated `X-Dropfans-Account`), it describes that account instead: `key.via` is `agency` (with `key.viaAgency: true` and the agency in `key.managedBy`) or `app` for an account your app onboarded — a cheap way to check you point where you think. `isAgency: true` on the agency's own key means it can use the [agency endpoints](https://www.dropfans.io/developers/concepts/agencies.md). ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `GET /api/external/v1/creators/{creatorId}/me` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators). The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## Responses ### 200 The key’s creator account. | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The Dropfans user id of the account this request acted on — the key’s own account, or the managed account named in `X-Dropfans-Account`. 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/v1/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 account acted on. Posting to the For You feed requires CREATOR or AGENCY. | | `sellerStatus` | string | | Internal seller state, for support (PENDING_KYC, KYC_IN_REVIEW, KYC_REJECTED, VERIFIED_CREATOR, VERIFIED_AGENCY). Added 2026-10-15. | | `isAgency` | boolean | | True when the account is a verified agency (sellerStatus VERIFIED_AGENCY) — its own key can use the /api/external/v1/agency endpoints and `X-Dropfans-Account`. Added 2026-10-15. | | `kycStatus` | `not_started` \\| `session_created` \\| `in_review` \\| `processing` \\| `approved` \\| `rejected` \\| `covered_by_agency` | | Coarse ID-check state. `covered_by_agency` = an AI persona whose verification rides on its agency’s. Added 2026-10-15. | | `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. | | `key.managedBy` | object \\| null | | The agency managing the account through this key: the key’s holder when `viaAgency` is true, else the agency that minted this key for the creator. Null for a creator’s own key. | | `key.managedBy.id` | string | | The agency’s user id. | | `key.managedBy.username` | string \\| null | | The agency’s @handle. | | `key.managedBy.name` | string \\| null | | The agency’s display name. | | `key.viaAgency` | boolean | | True when an agency’s own key is acting for a managed account — via /api/external/v1/creators/{creatorId}/… or `X-Dropfans-Account` (added 2026-10-26). | | `key.via` | `self` \\| `agency` \\| `app` | | How the key reached this account: `self` = its own account, `agency` = an agency acting for a managed creator, `app` = an app owner acting for an account its app onboarded (added 2026-11-01). | | `key.apiCategories` | ApiCategoryState[] \\| null | | The key’s app’s API category switches (see /developers/concepts/api-categories). Null for a Personal key, which has none. Added 2026-11-01. | ```json { "id": "clx2f8a1b0001qw3k", "username": "valeria", "name": "Valeria", "image": "https://cdn.dropfans.io/valeria/profile.jpg", "accountType": "CREATOR", "sellerStatus": "VERIFIED_CREATOR", "isAgency": false, "kycStatus": "approved", "key": { "name": "KVIQA", "app": { "slug": "kviqa", "name": "KVIQA" }, "tier": "app", "managedBy": null, "viaAgency": false } } ``` ## Errors | Status | Body | When | |---|---|---| | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | ## Notes The `accountType` and `key` fields were added with the public launch (2026-08-19); `sellerStatus`, `isAgency`, `kycStatus` and `key.managedBy` on 2026-10-15; `key.viaAgency` on 2026-10-26; `key.via` and `key.apiCategories` on 2026-11-01. `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/v1/me" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/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/v1/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 public profile](https://www.dropfans.io/developers/reference/get-profile.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/v1/profile — Read the creator’s public profile > The creator's public profile: username, display name, bio, visibility, contact handles and images. - Source: https://www.dropfans.io/developers/reference/get-profile - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/profile` (the unversioned alias). The creator's public profile: username, display name, bio, visibility, contact handles and images. Image URLs are signed and valid for 24 hours — fetch them when you need them, don't store them. Change any of it with [PATCH /api/external/v1/profile](https://www.dropfans.io/developers/reference/update-profile.md). ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `GET /api/external/v1/creators/{creatorId}/profile` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators). The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## Responses ### 200 The profile. | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The account’s user id. | | `username` | string \\| null | | Public @handle, without the @. | | `displayName` | string \\| null | | Display name. | | `bio` | string \\| null | | Profile bio. | | `profileVisible` | boolean | yes | Whether the public profile is visible. | | `location` | string \\| null | | Free-text location. | | `websiteUrl` | string \\| null | | Website link. | | `twitterHandle` | string \\| null | | X / Twitter handle. | | `instagramHandle` | string \\| null | | Instagram handle. | | `tiktokHandle` | string \\| null | | TikTok handle. | | `snapchatHandle` | string \\| null | | Snapchat handle. | | `threadsHandle` | string \\| null | | Threads handle. | | `youtubeHandle` | string \\| null | | YouTube handle. | | `telegramHandle` | string \\| null | | Telegram handle. | | `whatsappNumber` | string \\| null | | WhatsApp number. | | `imessageHandle` | string \\| null | | iMessage handle. | | `primaryContactPlatform` | string \\| null | | Which contact platform the profile highlights. | | `avatarUrl` | string \\| null | | Profile image. Signed URL, valid 24 hours — do not store it. | | `bannerUrl` | string \\| null | | Banner image. Signed URL, valid 24 hours — do not store it. | | `avatarModerationStatus` | string \\| null | | PENDING, APPROVED, REJECTED or FLAGGED for the profile image (a new image shows once approved); null when there is none. | | `bannerModerationStatus` | string \\| null | | PENDING, APPROVED, REJECTED or FLAGGED for the banner image; null when there is none. | ```json { "id": "cmh4v2q8k0003xy9z7w6v5u4t", "username": "luna_ai", "displayName": "Luna", "bio": "Your favourite AI muse ✨", "profileVisible": true, "location": null, "websiteUrl": null, "twitterHandle": null, "instagramHandle": "luna.ai", "tiktokHandle": null, "snapchatHandle": null, "threadsHandle": null, "youtubeHandle": null, "telegramHandle": "luna_ai", "whatsappNumber": null, "imessageHandle": null, "primaryContactPlatform": "telegram", "avatarUrl": "https://cdn.dropfans.io/luna_ai/profile.jpg?token=…", "bannerUrl": null, "avatarModerationStatus": "APPROVED", "bannerModerationStatus": null } ``` ## Errors | Status | Body | When | |---|---|---| | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 404 | `{"error":"Account not found"}` | The account no longer exists. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | ## 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/v1/profile" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/profile`, { 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/v1/profile", 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: [Update the creator’s public profile](https://www.dropfans.io/developers/reference/update-profile.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # PATCH /api/external/v1/profile — Update the creator’s public profile > Partial update of the creator's public profile — send only the fields that change; `null` clears a field. Returns the whole profile. - Source: https://www.dropfans.io/developers/reference/update-profile - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/profile` (the unversioned alias). Partial update of the creator's public profile — send only the fields that change; `null` clears a field. Returns the whole profile. Same rules as the dashboard: every text field runs the prohibited-word filter, usernames must be free (the old one is kept in the account's history), and unknown fields are **refused, not ignored**. `avatarUrl` / `bannerUrl` take a public https image URL: Dropfans downloads, stores and moderates it, so a new image shows once approved (watch `avatarModerationStatus`). This changes what fans see on Dropfans — only call it when the creator asked for it. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `PATCH /api/external/v1/creators/{creatorId}/profile` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canEditProfile`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## Request body (application/json) | Field | Type | Required | Description | |---|---|---|---| | `username` | string | | New @handle: 3–30 of lowercase letters, digits, `_` and `-`. | | `displayName` | string | | Display name. Cannot be empty or null. | | `bio` | string \\| null | | Profile bio. | | `profileVisible` | boolean | | Show or hide the public profile. | | `location` | string \\| null | | Free-text location. | | `websiteUrl` | string \\| null | | Website link. | | `twitterHandle` | string \\| null | | X / Twitter handle. | | `instagramHandle` | string \\| null | | Instagram handle. | | `tiktokHandle` | string \\| null | | TikTok handle. | | `snapchatHandle` | string \\| null | | Snapchat handle. | | `threadsHandle` | string \\| null | | Threads handle. | | `youtubeHandle` | string \\| null | | YouTube handle. | | `telegramHandle` | string \\| null | | Telegram handle. | | `whatsappNumber` | string \\| null | | WhatsApp number. | | `imessageHandle` | string \\| null | | iMessage handle. | | `primaryContactPlatform` | string \\| null | | Which contact platform the profile highlights. | | `avatarUrl` | string \\| null | | Public https URL of the new profile image (Dropfans fetches and moderates it). null removes the image. | | `bannerUrl` | string \\| null | | Public https URL of the new banner image. null removes it. | Example — New bio and avatar: ```json { "bio": "Your favourite AI muse ✨", "avatarUrl": "https://cdn.example.com/luna/avatar.jpg" } ``` ## Responses ### 200 The updated profile. | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The account’s user id. | | `username` | string \\| null | | Public @handle, without the @. | | `displayName` | string \\| null | | Display name. | | `bio` | string \\| null | | Profile bio. | | `profileVisible` | boolean | yes | Whether the public profile is visible. | | `location` | string \\| null | | Free-text location. | | `websiteUrl` | string \\| null | | Website link. | | `twitterHandle` | string \\| null | | X / Twitter handle. | | `instagramHandle` | string \\| null | | Instagram handle. | | `tiktokHandle` | string \\| null | | TikTok handle. | | `snapchatHandle` | string \\| null | | Snapchat handle. | | `threadsHandle` | string \\| null | | Threads handle. | | `youtubeHandle` | string \\| null | | YouTube handle. | | `telegramHandle` | string \\| null | | Telegram handle. | | `whatsappNumber` | string \\| null | | WhatsApp number. | | `imessageHandle` | string \\| null | | iMessage handle. | | `primaryContactPlatform` | string \\| null | | Which contact platform the profile highlights. | | `avatarUrl` | string \\| null | | Profile image. Signed URL, valid 24 hours — do not store it. | | `bannerUrl` | string \\| null | | Banner image. Signed URL, valid 24 hours — do not store it. | | `avatarModerationStatus` | string \\| null | | PENDING, APPROVED, REJECTED or FLAGGED for the profile image (a new image shows once approved); null when there is none. | | `bannerModerationStatus` | string \\| null | | PENDING, APPROVED, REJECTED or FLAGGED for the banner image; null when there is none. | ```json { "id": "cmh4v2q8k0003xy9z7w6v5u4t", "username": "luna_ai", "displayName": "Luna", "bio": "Your favourite AI muse ✨", "profileVisible": true, "location": null, "websiteUrl": null, "twitterHandle": null, "instagramHandle": "luna.ai", "tiktokHandle": null, "snapchatHandle": null, "threadsHandle": null, "youtubeHandle": null, "telegramHandle": "luna_ai", "whatsappNumber": null, "imessageHandle": null, "primaryContactPlatform": "telegram", "avatarUrl": "https://cdn.dropfans.io/luna_ai/profile.jpg?token=…", "bannerUrl": null, "avatarModerationStatus": "APPROVED", "bannerModerationStatus": null } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"Unknown profile field(s): nickname","code":"unknown_field","field":"nickname"}` | A field that is not in the list above. | | 400 | `{"error":"bio is at most 2000 characters.","code":"invalid_field","field":"bio"}` | Wrong type, too long, an empty displayName, or an image URL that is not https. | | 400 | `{"error":"Field \"bio\" contains a prohibited word: \"…\"","code":"prohibited_word","field":"bio"}` | A text field tripped the word filter. | | 400 | `{"error":"Username must be 3–30 characters: lowercase letters, digits, underscores, hyphens.","code":"invalid_username","field":"username"}` | Malformed or reserved username. | | 400 | `{"error":"avatarUrl could not be fetched as an image.","code":"image_unreachable","field":"avatarUrl"}` | The image URL did not return an image. Earlier fields in the same call are already saved. | | 400 | `{"error":"Body must be a JSON object"}` | The body is not a JSON object. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"Profile editing is restricted for this account.","code":"profile_locked"}` | Profile editing is locked for this account. | | 409 | `{"error":"Username is already taken.","code":"username_taken","field":"username"}` | Another account has that username. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canEditProfile"}` | For a managed creator: the agency lacks `canEditProfile` on that account. | ## Notes Images are applied last: when `avatarUrl` or `bannerUrl` fails with `image_unreachable`, the other fields in the same request have already been saved. ## 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/v1/profile" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "bio": "Your favourite AI muse ✨", "avatarUrl": "https://cdn.example.com/luna/avatar.jpg" }' ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/profile`, { method: 'PATCH', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ "bio": "Your favourite AI muse ✨", "avatarUrl": "https://cdn.example.com/luna/avatar.jpg" }), }); console.log(await res.json()); ``` ### Python ```python import os import requests res = requests.patch( "https://www.dropfans.io/api/external/v1/profile", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, json={ "bio": "Your favourite AI muse ✨", "avatarUrl": "https://cdn.example.com/luna/avatar.jpg", }, ) print(res.json()) ``` --- Previous: [Read the creator’s public profile](https://www.dropfans.io/developers/reference/get-profile.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/v1/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 Also served at `/api/external/timezone` (the unversioned alias). Returns the creator's IANA timezone, defaulting to `"UTC"` when they never set one. Use it as the `tz` parameter on [GET /api/external/v1/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 dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `GET /api/external/v1/creators/{creatorId}/timezone` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators). The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## 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. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | ## 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/v1/timezone" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/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/v1/timezone", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [Update the creator’s public profile](https://www.dropfans.io/developers/reference/update-profile.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/v1/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 Also served at `/api/external/timezone` (the unversioned alias). 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 dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `PUT /api/external/v1/creators/{creatorId}/timezone` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canEditProfile`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## 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. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canEditProfile"}` | For a managed creator: the agency lacks `canEditProfile` on that 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 PUT "https://www.dropfans.io/api/external/v1/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/v1/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/v1/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: [Read the creator’s subscription tier](https://www.dropfans.io/developers/reference/get-subscription.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/v1/subscription — Read the creator’s subscription tier > The creator's subscription tier — price, interval, trial and whether it takes new sign-ups — plus the web link that opens the subscribe sheet. `tier` is null when the creator has no subscription set up. - Source: https://www.dropfans.io/developers/reference/get-subscription - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/subscription` (the unversioned alias). The creator's subscription tier — price, interval, trial and whether it takes new sign-ups — plus the web link that opens the subscribe sheet. `tier` is null when the creator has no subscription set up. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `GET /api/external/v1/creators/{creatorId}/subscription` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canViewStats`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## Responses ### 200 The tier and subscribe link. | Field | Type | Required | Description | |---|---|---|---| | `tier` | SubscriptionTier \\| null | | The subscription tier, or null when none is set up. | | `subscribeUrl` | string \\| null | | The web link that opens the subscribe sheet. Null until the account has a username. (Only in GET.) | ```json { "tier": { "priceUsd": 9.99, "currency": "USD", "interval": "monthly", "trialDays": 0, "description": "All my sets, first.", "pitchMessage": null, "isActive": true }, "subscribeUrl": "https://www.dropfans.io/u/luna_ai?subscribe=1" } ``` ## Errors | Status | Body | When | |---|---|---| | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canViewStats"}` | For a managed creator: the agency lacks `canViewStats` on that 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 "https://www.dropfans.io/api/external/v1/subscription" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/subscription`, { 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/v1/subscription", 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: [Set the creator’s subscription tier](https://www.dropfans.io/developers/reference/update-subscription.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # PUT /api/external/v1/subscription — Set the creator’s subscription tier > Creates or replaces the creator's subscription tier. This is a full replace: fields you leave out go back to their defaults (`trialDays` 0, no description or pitch, active). - Source: https://www.dropfans.io/developers/reference/update-subscription - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/subscription` (the unversioned alias). Creates or replaces the creator's subscription tier. This is a full replace: fields you leave out go back to their defaults (`trialDays` 0, no description or pitch, active). Same rules as the dashboard: `priceUsd` is USD **dollars**, either `0` (free) or between $5 and $750. `isActive: false` pauses new sign-ups. Existing subscribers keep the price they signed up at. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `PUT /api/external/v1/creators/{creatorId}/subscription` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canEditProfile`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## Request body (application/json) | Field | Type | Required | Description | |---|---|---|---| | `priceUsd` | number | yes | USD dollars per interval. 0 = free; otherwise $5–$750. Rounded to cents. | | `interval` | `monthly` \\| `weekly` | yes | Billing interval. | | `trialDays` | integer | | Free trial length in days. | | `description` | string | | What subscribers get (cut at 500 characters). | | `pitchMessage` | string | | Short pitch shown in the subscribe sheet (cut at 500 characters). | | `isActive` | boolean | | false pauses new sign-ups. | Example — $9.99 a month: ```json { "priceUsd": 9.99, "interval": "monthly", "description": "All my sets, first." } ``` ## Responses ### 200 The saved tier. | Field | Type | Required | Description | |---|---|---|---| | `tier` | SubscriptionTier | yes | The tier now in effect. | ```json { "tier": { "priceUsd": 9.99, "currency": "USD", "interval": "monthly", "trialDays": 0, "description": "All my sets, first.", "pitchMessage": null, "isActive": true } } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"priceUsd must be 0 (free) or at least 5"}` | `priceUsd` missing, not a number 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":"interval must be 'monthly' or 'weekly'"}` | Missing or unknown interval. | | 400 | `{"error":"trialDays must be a whole number from 0 to 30"}` | Bad trial length. | | 400 | `{"error":"isActive must be true or false"}` | `isActive` is not a boolean. | | 400 | `{"error":"Invalid JSON body"}` | The body is not valid JSON. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canEditProfile"}` | For a managed creator: the agency lacks `canEditProfile` on that 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 PUT "https://www.dropfans.io/api/external/v1/subscription" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "priceUsd": 9.99, "interval": "monthly", "description": "All my sets, first." }' ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/subscription`, { method: 'PUT', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ "priceUsd": 9.99, "interval": "monthly", "description": "All my sets, first." }), }); console.log(await res.json()); ``` ### Python ```python import os import requests res = requests.put( "https://www.dropfans.io/api/external/v1/subscription", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, json={ "priceUsd": 9.99, "interval": "monthly", "description": "All my sets, first.", }, ) print(res.json()) ``` --- Previous: [Read the creator’s subscription tier](https://www.dropfans.io/developers/reference/get-subscription.md) · Next: [The account’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/v1/balance — The account’s payout balance > The account's payout balance buckets in USD dollars: the creator side at the top level, plus `total` and `sides` for everything the account owns. - Source: https://www.dropfans.io/developers/reference/get-balance - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/balance` (the unversioned alias). The account's payout balance buckets in USD dollars: the creator side at the top level, plus `total` and `sides` for everything the account owns. The top-level buckets are the creator side (own sales), unchanged since this endpoint launched. `total` is the full payable balance — the same numbers the Payouts page shows and a payout request pays out: creator side + agency side (income from managed creators and AI personas) + referral commission + partner shares. `sides` breaks `total` down. > [!WARNING] Dollars, not cents > This endpoint returns USD **dollars**. The earnings, check-status and payouts endpoints return **cents**. Do not mix the units. For cents, eligibility and payout status use [GET /payouts/summary](https://www.dropfans.io/developers/reference/get-payout-summary.md). > [!NOTE] Added on 2026-10-30 > `total` and `sides` are new. The top-level fields did not change, so existing integrations keep working; read `total` for an agency, referral or partner account. Bucket semantics: `pending` is revenue still inside the chargeback hold window; `available` has cleared the hold and is what a payout request pays out; `processing` is inside a payout currently being executed; `paidOut` is the lifetime total already paid. `available` can be negative (a refund of money already paid out is netted against future earnings). ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `GET /api/external/v1/creators/{creatorId}/balance` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canViewStats`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## 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). Creator side (own sales) still inside the chargeback hold window; not payable yet. For everything the account owns, read `total`. | | `available` | number | yes | USD dollars, creator side. Cleared the hold. Can be negative. | | `processing` | number | yes | USD dollars, creator side. Included in a payout that is currently being sent. | | `paidOut` | number | yes | USD dollars, creator side. Lifetime total of completed payouts. | | `total` | BalanceBuckets | yes | Added 2026-10-30. The full payable balance, USD dollars: the sum of all four `sides` — what the Payouts page shows and a payout request pays out (`total.available`). | | `sides` | object | yes | Added 2026-10-30. The four buckets per side, USD dollars; they sum to `total`. `sides.creator` equals the top-level buckets. | | `sides.creator` | BalanceBuckets | yes | Own sales (creatorPortion). | | `sides.agency` | BalanceBuckets | yes | Income from managed creators / AI personas (agencyPortion). | | `sides.affiliate` | BalanceBuckets | yes | Referral commission. | | `sides.share` | BalanceBuckets | yes | Partner shares of other accounts’ sales. | ```json { "currency": "USD", "pending": 120.5, "available": 300.1, "processing": 0, "paidOut": 1755, "total": { "pending": 120.5, "available": 342.1, "processing": 0, "paidOut": 1875 }, "sides": { "creator": { "pending": 120.5, "available": 300.1, "processing": 0, "paidOut": 1755 }, "agency": { "pending": 0, "available": 42, "processing": 0, "paidOut": 120 }, "affiliate": { "pending": 0, "available": 0, "processing": 0, "paidOut": 0 }, "share": { "pending": 0, "available": 0, "processing": 0, "paidOut": 0 } } } ``` ## 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. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canViewStats"}` | For a managed creator: the agency lacks `canViewStats` on that account. | | 403 | `{"error":"This app does not have the earnings API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"earnings"}` | Your app has the Earnings & balance [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/balance" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/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/v1/balance", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [Set the creator’s subscription tier](https://www.dropfans.io/developers/reference/update-subscription.md) · Next: [List the creators and personas you manage](https://www.dropfans.io/developers/reference/list-agency-creators.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/v1/agency/creators — List the creators and personas you manage > Every creator and AI persona the agency manages, with the account `id` to send as `X-Dropfans-Account` and the permissions the agency holds on each. - Source: https://www.dropfans.io/developers/reference/list-agency-creators - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/agency/creators` (the unversioned alias). Every creator and AI persona the agency manages, with the account `id` to send as `X-Dropfans-Account` and the permissions the agency holds on each. Call it with the agency's own key. Then act for any account by sending the same key plus `X-Dropfans-Account: ` to the regular endpoints — see [Agencies: one key for every creator](https://www.dropfans.io/developers/concepts/agencies.md). `active` tells you whether the header works for that account right now. By default only ACCEPTED relationships come back. `?status=all` adds ended creators and retired personas, e.g. to find a persona to [reactivate](https://www.dropfans.io/developers/reference/reactivate-persona.md). ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. Needs a verified agency's own key, sent without `X-Dropfans-Account` — these endpoints act as the agency itself. ## Query parameters | Field | Type | Required | Description | |---|---|---|---| | `status` | `all` | | Pass `all` to include ended and retired accounts. Omit for active (ACCEPTED) only. | ## Responses ### 200 The agency and its managed accounts. | Field | Type | Required | Description | |---|---|---|---| | `agency` | object | yes | The agency holding the key. | | `agency.id` | string | yes | The agency’s user id. | | `agency.username` | string \\| null | | The agency’s @handle. | | `agency.name` | string \\| null | | The agency’s display name. | | `creators` | AgencyCreator[] | yes | Managed creators and AI personas, oldest relationship first. | ```json { "agency": { "id": "cmh1agc0q0001ab7k2p9q4r8s", "username": "starlight_agency", "name": "Starlight Agency" }, "creators": [ { "id": "cmh4v2q8k0003xy9z7w6v5u4t", "managedCreatorId": "cmh4v2q8k0004mc1relation", "username": "luna_ai", "name": "Luna", "image": "https://cdn.dropfans.io/luna_ai/profile.jpg", "kind": "ai_persona", "status": "ACCEPTED", "active": true, "kycCoveredByAgency": true, "sellerStatus": "VERIFIED_CREATOR", "permissions": { "canEditProfile": true, "canManageDrops": true, "canManageVault": true, "canViewStats": true, "canManageCoPerformers": false }, "profileUrl": "https://www.dropfans.io/u/luna_ai", "since": "2026-10-01T09:00:00.000Z" }, { "id": "cmh3k9p2a0002cd5e6f7g8h9j", "managedCreatorId": "cmh3k9p2a0005mc2relation", "username": "valeria", "name": "Valeria", "image": null, "kind": "creator", "status": "ACCEPTED", "active": true, "kycCoveredByAgency": false, "sellerStatus": "VERIFIED_CREATOR", "permissions": { "canEditProfile": false, "canManageDrops": true, "canManageVault": true, "canViewStats": true, "canManageCoPerformers": false }, "profileUrl": "https://www.dropfans.io/u/valeria", "since": "2026-09-12T14:30:00.000Z" } ] } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"Agency endpoints act as the agency itself — send them without X-Dropfans-Account.","code":"account_header_not_allowed"}` | The request carried `X-Dropfans-Account`. Agency endpoints always act as the agency. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"This endpoint needs a verified agency's own API key.","code":"agency_key_required"}` | The key is not a verified agency’s own key — a creator’s key, or a key an agency minted for one of its creators. | | 400 | `{"error":"This endpoint acts as the key’s own account or app — call it without /creators/{creatorId}.","code":"creator_scope_not_allowed"}` | Called under /api/external/v1/creators/{creatorId}/ — this endpoint always acts as the key’s own account, agency or app. | | 403 | `{"error":"This app does not have the agency API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"agency"}` | Your app has the Agency roster [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/agency/creators" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/agency/creators`, { 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/v1/agency/creators", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [The account’s payout balance](https://www.dropfans.io/developers/reference/get-balance.md) · Next: [Create an AI persona](https://www.dropfans.io/developers/reference/create-persona.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # POST /api/external/v1/agency/personas — Create an AI persona > Creates an AI persona run by the agency and returns its account `id` — use it in `X-Dropfans-Account` straight away to fill its vault, create drops and post. - Source: https://www.dropfans.io/developers/reference/create-persona - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/agency/personas` (the unversioned alias). Creates an AI persona run by the agency and returns its account `id` — use it in `X-Dropfans-Account` straight away to fill its vault, create drops and post. Needs an agency in Agency Mode (an AI operator) whose ID check is approved. The persona is covered by the agency's ID check — it has no ID check of its own — and is marked as AI-generated on Dropfans. At most 8 personas can be active at once. Besides `username` and `displayName` you can send any [profile field](https://www.dropfans.io/developers/reference/update-profile.md) in the same call. The persona is created either way: a refused profile field comes back as `profileError` instead of failing the call. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. Needs a verified agency's own key, sent without `X-Dropfans-Account` — these endpoints act as the agency itself. ## Request body (application/json) | Field | Type | Required | Description | |---|---|---|---| | `username` | string | yes | 3–30 of lowercase letters, digits, `_` and `-`. Must be free and not reserved. | | `displayName` | string | | Display name. Defaults to the username. | | `bio` | string | | Profile bio. Every other PATCH /api/external/v1/profile field (avatarUrl, bannerUrl, social handles, profileVisible, …) is accepted here too. | Example — A persona with a bio and avatar: ```json { "username": "luna_ai", "displayName": "Luna", "bio": "Your favourite AI muse ✨", "avatarUrl": "https://cdn.example.com/luna/avatar.jpg" } ``` ## Responses ### 201 Created. | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The persona’s user id — use it in `X-Dropfans-Account`. | | `managedCreatorId` | string | yes | Id of the agency ↔ persona relationship. | | `username` | string | yes | The persona’s @handle. | | `name` | string \\| null | | Display name (defaults to the username). | | `kind` | `ai_persona` | yes | Always `ai_persona`. | | `profileUrl` | string | yes | Public profile URL. | | `profileError` | string | | Present only when an extra profile field in the request was refused. The persona was still created — fix the field and send it with PATCH /api/external/v1/profile. | ```json { "id": "cmh4v2q8k0003xy9z7w6v5u4t", "managedCreatorId": "cmh4v2q8k0004mc1relation", "username": "luna_ai", "name": "Luna", "kind": "ai_persona", "profileUrl": "https://www.dropfans.io/u/luna_ai" } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"Agency endpoints act as the agency itself — send them without X-Dropfans-Account.","code":"account_header_not_allowed"}` | The request carried `X-Dropfans-Account`. Agency endpoints always act as the agency. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"This endpoint needs a verified agency's own API key.","code":"agency_key_required"}` | The key is not a verified agency’s own key — a creator’s key, or a key an agency minted for one of its creators. | | 400 | `{"error":"Username must be 3–30 characters: lowercase letters, digits, underscores, hyphens."}` | Missing or malformed `username`. | | 400 | `{"error":"That username is reserved — pick another."}` | Reserved username. | | 403 | `{"error":"Agency Mode required.","code":"agency_mode_required"}` | The agency is not in Agency Mode (AI operator). | | 403 | `{"error":"Complete identity verification before adding AI personas.","code":"kyc_required"}` | The agency’s ID check is not approved. | | 409 | `{"error":"You've reached the maximum of 8 AI personas. Retire one before adding another.","code":"persona_limit"}` | 8 personas are already active. | | 409 | `{"error":"Username is already taken.","code":"username_taken"}` | Another account has that username. | | 500 | `{"error":"Failed to create persona."}` | Unexpected failure — retry later. | | 400 | `{"error":"This endpoint acts as the key’s own account or app — call it without /creators/{creatorId}.","code":"creator_scope_not_allowed"}` | Called under /api/external/v1/creators/{creatorId}/ — this endpoint always acts as the key’s own account, agency or app. | | 403 | `{"error":"This app does not have the agency API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"agency"}` | Your app has the Agency roster [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/agency/personas" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "username": "luna_ai", "displayName": "Luna", "bio": "Your favourite AI muse ✨", "avatarUrl": "https://cdn.example.com/luna/avatar.jpg" }' ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/agency/personas`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ "username": "luna_ai", "displayName": "Luna", "bio": "Your favourite AI muse ✨", "avatarUrl": "https://cdn.example.com/luna/avatar.jpg" }), }); console.log(await res.json()); ``` ### Python ```python import os import requests res = requests.post( "https://www.dropfans.io/api/external/v1/agency/personas", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, json={ "username": "luna_ai", "displayName": "Luna", "bio": "Your favourite AI muse ✨", "avatarUrl": "https://cdn.example.com/luna/avatar.jpg", }, ) print(res.json()) ``` --- Previous: [List the creators and personas you manage](https://www.dropfans.io/developers/reference/list-agency-creators.md) · Next: [Rename an AI persona](https://www.dropfans.io/developers/reference/rename-persona.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # PATCH /api/external/v1/agency/personas/{id} — Rename an AI persona > Changes an active persona's `username` (same rules as a creator's own username change; the old handle is kept in its history). - Source: https://www.dropfans.io/developers/reference/rename-persona - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/agency/personas/{id}` (the unversioned alias). Changes an active persona's `username` (same rules as a creator's own username change; the old handle is kept in its history). Everything else on the persona's profile goes through [PATCH /api/external/v1/profile](https://www.dropfans.io/developers/reference/update-profile.md) with `X-Dropfans-Account`. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. Needs a verified agency's own key, sent without `X-Dropfans-Account` — these endpoints act as the agency itself. ## Path parameters | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The persona’s account id (`id` from GET /api/external/v1/agency/creators). | ## Request body (application/json) | Field | Type | Required | Description | |---|---|---|---| | `username` | string | yes | The new username. | Example — Rename: ```json { "username": "luna_official" } ``` ## Responses ### 200 The saved username. | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The persona’s account id. | | `username` | string | yes | The username now in effect. | ```json { "id": "cmh4v2q8k0003xy9z7w6v5u4t", "username": "luna_official" } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"Agency endpoints act as the agency itself — send them without X-Dropfans-Account.","code":"account_header_not_allowed"}` | The request carried `X-Dropfans-Account`. Agency endpoints always act as the agency. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"This endpoint needs a verified agency's own API key.","code":"agency_key_required"}` | The key is not a verified agency’s own key — a creator’s key, or a key an agency minted for one of its creators. | | 400 | `{"error":"Username must be 3–30 characters: lowercase letters, digits, underscores, hyphens."}` | Missing, malformed or reserved username. | | 404 | `{"error":"Persona not found."}` | Not an active persona of this agency. | | 409 | `{"error":"Username is already taken.","code":"username_taken"}` | Another account has that username. | | 400 | `{"error":"This endpoint acts as the key’s own account or app — call it without /creators/{creatorId}.","code":"creator_scope_not_allowed"}` | Called under /api/external/v1/creators/{creatorId}/ — this endpoint always acts as the key’s own account, agency or app. | | 403 | `{"error":"This app does not have the agency API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"agency"}` | Your app has the Agency roster [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/agency/personas/$ACCOUNT_ID" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "username": "luna_official" }' ``` ### Node ```javascript const accountId = '…'; // from an earlier response const res = await fetch(`https://www.dropfans.io/api/external/v1/agency/personas/${accountId}`, { method: 'PATCH', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ "username": "luna_official" }), }); console.log(await res.json()); ``` ### Python ```python import os import requests account_id = "…" # from an earlier response res = requests.patch( f"https://www.dropfans.io/api/external/v1/agency/personas/{account_id}", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, json={ "username": "luna_official", }, ) print(res.json()) ``` --- Previous: [Create an AI persona](https://www.dropfans.io/developers/reference/create-persona.md) · Next: [Retire an AI persona](https://www.dropfans.io/developers/reference/retire-persona.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # DELETE /api/external/v1/agency/personas/{id} — Retire an AI persona > Retires a persona: its profile goes offline, the relationship becomes DEPARTED and any keys the agency minted for it stop working. - Source: https://www.dropfans.io/developers/reference/retire-persona - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/agency/personas/{id}` (the unversioned alias). Retires a persona: its profile goes offline, the relationship becomes DEPARTED and any keys the agency minted for it stop working. Nothing is deleted — sales history and subscribers are kept — and it can be brought back with [reactivate](https://www.dropfans.io/developers/reference/reactivate-persona.md). Retiring frees a slot under the 8-persona cap. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. Needs a verified agency's own key, sent without `X-Dropfans-Account` — these endpoints act as the agency itself. ## Path parameters | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The persona’s account id (`id` from GET /api/external/v1/agency/creators). | ## Responses ### 200 Retired. | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The persona’s account id. | | `status` | `retired` | yes | Always "retired". | ```json { "id": "cmh4v2q8k0003xy9z7w6v5u4t", "status": "retired" } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"Agency endpoints act as the agency itself — send them without X-Dropfans-Account.","code":"account_header_not_allowed"}` | The request carried `X-Dropfans-Account`. Agency endpoints always act as the agency. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"This endpoint needs a verified agency's own API key.","code":"agency_key_required"}` | The key is not a verified agency’s own key — a creator’s key, or a key an agency minted for one of its creators. | | 404 | `{"error":"Persona not found."}` | No AI persona with this id under this agency. | | 500 | `{"error":"Failed to retire persona."}` | Unexpected failure — retry later. | | 400 | `{"error":"This endpoint acts as the key’s own account or app — call it without /creators/{creatorId}.","code":"creator_scope_not_allowed"}` | Called under /api/external/v1/creators/{creatorId}/ — this endpoint always acts as the key’s own account, agency or app. | | 403 | `{"error":"This app does not have the agency API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"agency"}` | Your app has the Agency roster [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/agency/personas/$ACCOUNT_ID" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const accountId = '…'; // from an earlier response const res = await fetch(`https://www.dropfans.io/api/external/v1/agency/personas/${accountId}`, { method: 'DELETE', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, }, }); console.log(await res.json()); ``` ### Python ```python import os import requests account_id = "…" # from an earlier response res = requests.delete( f"https://www.dropfans.io/api/external/v1/agency/personas/{account_id}", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [Rename an AI persona](https://www.dropfans.io/developers/reference/rename-persona.md) · Next: [Reactivate a retired AI persona](https://www.dropfans.io/developers/reference/reactivate-persona.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # POST /api/external/v1/agency/personas/{id}/reactivate — Reactivate a retired AI persona > Brings a retired persona back online under the agency, with the same rules as the dashboard: not while another agency runs it, not over the 8-persona cap, and never for an account Dropfans suspended. - Source: https://www.dropfans.io/developers/reference/reactivate-persona - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/agency/personas/{id}/reactivate` (the unversioned alias). Brings a retired persona back online under the agency, with the same rules as the dashboard: not while another agency runs it, not over the 8-persona cap, and never for an account Dropfans suspended. Find retired personas with [GET /api/external/v1/agency/creators?status=all](https://www.dropfans.io/developers/reference/list-agency-creators.md). ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. Needs a verified agency's own key, sent without `X-Dropfans-Account` — these endpoints act as the agency itself. ## Path parameters | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The persona’s account id (`id` from GET /api/external/v1/agency/creators). | ## Responses ### 200 Active again. | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The persona’s account id. | | `status` | `active` | yes | Always "active". | ```json { "id": "cmh4v2q8k0003xy9z7w6v5u4t", "status": "active" } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"Agency endpoints act as the agency itself — send them without X-Dropfans-Account.","code":"account_header_not_allowed"}` | The request carried `X-Dropfans-Account`. Agency endpoints always act as the agency. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"This endpoint needs a verified agency's own API key.","code":"agency_key_required"}` | The key is not a verified agency’s own key — a creator’s key, or a key an agency minted for one of its creators. | | 404 | `{"error":"Persona not found."}` | No AI persona with this id was ever run by this agency. | | 409 | `{"error":"This persona is already active."}` | Nothing to do. | | 409 | `{"error":"You've reached the maximum of 8 AI personas. Retire one before reactivating another."}` | 8 personas are already active. | | 409 | `{"error":"@other_agency runs this persona now."}` | Another agency runs the persona now. | | 409 | `{"error":"This persona account no longer exists."}` | The account was deleted or banned. | | 409 | `{"error":"This persona was suspended by Dropfans — contact support."}` | Suspended by Dropfans, not retired by the agency. | | 500 | `{"error":"Could not reactivate the persona."}` | Unexpected failure — retry later. | | 400 | `{"error":"This endpoint acts as the key’s own account or app — call it without /creators/{creatorId}.","code":"creator_scope_not_allowed"}` | Called under /api/external/v1/creators/{creatorId}/ — this endpoint always acts as the key’s own account, agency or app. | | 403 | `{"error":"This app does not have the agency API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"agency"}` | Your app has the Agency roster [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/agency/personas/$ACCOUNT_ID/reactivate" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const accountId = '…'; // from an earlier response const res = await fetch(`https://www.dropfans.io/api/external/v1/agency/personas/${accountId}/reactivate`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, }, }); console.log(await res.json()); ``` ### Python ```python import os import requests account_id = "…" # from an earlier response res = requests.post( f"https://www.dropfans.io/api/external/v1/agency/personas/{account_id}/reactivate", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [Retire an AI persona](https://www.dropfans.io/developers/reference/retire-persona.md) · Next: [Invite a creator to your agency](https://www.dropfans.io/developers/reference/create-agency-invite.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # POST /api/external/v1/agency/invites — Invite a creator to your agency > Creates a join link that invites a real creator to be managed by the agency. The link is valid for 72 hours; with `email`, Dropfans also emails it. - Source: https://www.dropfans.io/developers/reference/create-agency-invite - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/agency/invites` (the unversioned alias). Creates a join link that invites a real creator to be managed by the agency. The link is valid for 72 hours; with `email`, Dropfans also emails it. The creator accepts while signed in to their own Dropfans account and agrees to the agency's permissions there — an agency can never attach a creator without their consent. Once accepted, the creator shows up in [GET /api/external/v1/agency/creators](https://www.dropfans.io/developers/reference/list-agency-creators.md). ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. Needs a verified agency's own key, sent without `X-Dropfans-Account` — these endpoints act as the agency itself. ## Request body (application/json) | Field | Type | Required | Description | |---|---|---|---| | `email` | string | | Optional: Dropfans emails the link to this address. | | `message` | string | | Optional personal note shown with the invite (longer text is cut at 500 characters). | Example — Email an invite: ```json { "email": "valeria@example.com", "message": "Hi Valeria — excited to work with you!" } ``` ## Responses ### 201 The invite. | Field | Type | Required | Description | |---|---|---|---| | `inviteId` | string | yes | The invite’s id. | | `inviteLink` | string | yes | Join link for the creator. They accept while signed in to their own Dropfans account. | | `expiresAt` | string | yes | When the link stops working (72 hours after creation). | | `emailSent` | boolean | yes | True when Dropfans emailed the link to `email`. | | `emailError` | string | | Present when `email` was given but sending failed — the invite still exists; share `inviteLink` yourself. | ```json { "inviteId": "cmh5inv0001ab7k2p9q4r8s", "inviteLink": "https://www.dropfans.io/agency/join/3f9c…", "expiresAt": "2026-10-29T12:00:00.000Z", "emailSent": true } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"Agency endpoints act as the agency itself — send them without X-Dropfans-Account.","code":"account_header_not_allowed"}` | The request carried `X-Dropfans-Account`. Agency endpoints always act as the agency. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"This endpoint needs a verified agency's own API key.","code":"agency_key_required"}` | The key is not a verified agency’s own key — a creator’s key, or a key an agency minted for one of its creators. | | 400 | `{"error":"email is not a valid address","code":"invalid_email"}` | Malformed `email`. | | 400 | `{"error":"Invalid JSON body"}` | The body is not valid JSON. | | 400 | `{"error":"This endpoint acts as the key’s own account or app — call it without /creators/{creatorId}.","code":"creator_scope_not_allowed"}` | Called under /api/external/v1/creators/{creatorId}/ — this endpoint always acts as the key’s own account, agency or app. | | 403 | `{"error":"This app does not have the agency API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"agency"}` | Your app has the Agency roster [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## Notes When the email fails to send, the call still returns 201 with `emailSent: false` and an `emailError` — share `inviteLink` yourself. ## 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/v1/agency/invites" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "email": "valeria@example.com", "message": "Hi Valeria — excited to work with you!" }' ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/agency/invites`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ "email": "valeria@example.com", "message": "Hi Valeria — excited to work with you!" }), }); console.log(await res.json()); ``` ### Python ```python import os import requests res = requests.post( "https://www.dropfans.io/api/external/v1/agency/invites", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, json={ "email": "valeria@example.com", "message": "Hi Valeria — excited to work with you!", }, ) print(res.json()) ``` --- Previous: [Reactivate a retired AI persona](https://www.dropfans.io/developers/reference/reactivate-persona.md) · Next: [List your agency’s invites](https://www.dropfans.io/developers/reference/list-agency-invites.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/v1/agency/invites — List your agency’s invites > The agency’s creator invites, newest first, each with its status: `pending`, `accepted`, `expired` or `revoked`. - Source: https://www.dropfans.io/developers/reference/list-agency-invites - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/agency/invites` (the unversioned alias). The agency’s creator invites, newest first, each with its status: `pending`, `accepted`, `expired` or `revoked`. The join link itself is not returned again — only a hash of it is stored. Revoke a pending invite with [DELETE /agency/invites/{id}](https://www.dropfans.io/developers/reference/revoke-agency-invite.md). ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. Needs a verified agency's own key, sent without `X-Dropfans-Account` — these endpoints act as the agency itself. ## Query parameters | Field | Type | Required | Description | |---|---|---|---| | `status` | `pending` \\| `accepted` \\| `expired` \\| `revoked` | | Only invites with this status. | | `limit` | integer | | Page size. | | `cursor` | string | | `nextCursor` from the previous page. | ## Responses ### 200 One page of invites. | Field | Type | Required | Description | |---|---|---|---| | `data` | AgencyInviteRow[] | yes | The invites. | | `nextCursor` | string \\| null | | Pass as `cursor` for the next page; null on the last page. | ```json { "data": [ { "id": "cmh5inv0001ab7k2p9q4r8s", "status": "pending", "email": "valeria@example.com", "message": null, "createdAt": "2026-10-26T12:00:00.000Z", "expiresAt": "2026-10-29T12:00:00.000Z", "acceptedAt": null, "acceptedBy": null } ], "nextCursor": null } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"Agency endpoints act as the agency itself — send them without X-Dropfans-Account.","code":"account_header_not_allowed"}` | The request carried `X-Dropfans-Account`. Agency endpoints always act as the agency. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"This endpoint needs a verified agency's own API key.","code":"agency_key_required"}` | The key is not a verified agency’s own key — a creator’s key, or a key an agency minted for one of its creators. | | 400 | `{"error":"status must be one of pending, accepted, expired, revoked","code":"invalid_status"}` | Bad `status` (also `invalid_limit`). | | 400 | `{"error":"This endpoint acts as the key’s own account or app — call it without /creators/{creatorId}.","code":"creator_scope_not_allowed"}` | Called under /api/external/v1/creators/{creatorId}/ — this endpoint always acts as the key’s own account, agency or app. | | 403 | `{"error":"This app does not have the agency API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"agency"}` | Your app has the Agency roster [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/agency/invites" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/agency/invites`, { 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/v1/agency/invites", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [Invite a creator to your agency](https://www.dropfans.io/developers/reference/create-agency-invite.md) · Next: [Revoke an invite](https://www.dropfans.io/developers/reference/revoke-agency-invite.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # DELETE /api/external/v1/agency/invites/{id} — Revoke an invite > Revokes a pending invite: its join link stops working at once. Returns the invite with `status: revoked`. - Source: https://www.dropfans.io/developers/reference/revoke-agency-invite - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/agency/invites/{id}` (the unversioned alias). Revokes a pending invite: its join link stops working at once. Returns the invite with `status: revoked`. An accepted invite cannot be revoked — end the management with [DELETE /agency/creators/{id}](https://www.dropfans.io/developers/reference/end-management.md) instead. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. Needs a verified agency's own key, sent without `X-Dropfans-Account` — these endpoints act as the agency itself. ## Path parameters | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The invite id (`inviteId` / `id`). | ## Responses ### 200 The revoked invite. | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The invite’s id (`inviteId` when it was created). | | `status` | `pending` \\| `accepted` \\| `expired` \\| `revoked` | yes | `accepted` once a creator used the link (it stays accepted), `revoked` after DELETE, `expired` 72 hours after creation, else `pending`. | | `email` | string \\| null | | The address it was emailed to, or null. | | `message` | string \\| null | | The personal note, or null. | | `createdAt` | string | yes | When the invite was created. | | `expiresAt` | string | yes | When the link stops working. | | `acceptedAt` | string \\| null | | When a creator accepted it, or null. | | `acceptedBy` | string \\| null | | The account id that accepted it, or null. | ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"Agency endpoints act as the agency itself — send them without X-Dropfans-Account.","code":"account_header_not_allowed"}` | The request carried `X-Dropfans-Account`. Agency endpoints always act as the agency. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"This endpoint needs a verified agency's own API key.","code":"agency_key_required"}` | The key is not a verified agency’s own key — a creator’s key, or a key an agency minted for one of its creators. | | 404 | `{"error":"Invite not found","code":"invite_not_found"}` | No such invite for this agency (another agency’s invite gets the same answer). | | 409 | `{"error":"This invite was already accepted.","code":"invite_already_used"}` | A creator already used the link (`invite_already_revoked` when it was revoked before). | | 400 | `{"error":"This endpoint acts as the key’s own account or app — call it without /creators/{creatorId}.","code":"creator_scope_not_allowed"}` | Called under /api/external/v1/creators/{creatorId}/ — this endpoint always acts as the key’s own account, agency or app. | | 403 | `{"error":"This app does not have the agency API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"agency"}` | Your app has the Agency roster [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/agency/invites/$ACCOUNT_ID" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const accountId = '…'; // from an earlier response const res = await fetch(`https://www.dropfans.io/api/external/v1/agency/invites/${accountId}`, { method: 'DELETE', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, }, }); console.log(await res.json()); ``` ### Python ```python import os import requests account_id = "…" # from an earlier response res = requests.delete( f"https://www.dropfans.io/api/external/v1/agency/invites/{account_id}", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [List your agency’s invites](https://www.dropfans.io/developers/reference/list-agency-invites.md) · Next: [Stop managing a creator](https://www.dropfans.io/developers/reference/end-management.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # DELETE /api/external/v1/agency/creators/{id} — Stop managing a creator > Ends the agency's management of a creator or persona — the same as "Leave agreement" in the dashboard. - Source: https://www.dropfans.io/developers/reference/end-management - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/agency/creators/{id}` (the unversioned alias). Ends the agency's management of a creator or persona — the same as "Leave agreement" in the dashboard. Split agreements end, keys the agency minted for the account stop working, `X-Dropfans-Account` stops working for it, and an AI persona is retired. Requires `{ "confirm": true }` in the body so a retry loop or a stray call can never drop a creator. A real creator has to accept a new invite to come back. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. Needs a verified agency's own key, sent without `X-Dropfans-Account` — these endpoints act as the agency itself. ## Path parameters | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The account’s account id (`id` from GET /api/external/v1/agency/creators). | ## Request body (application/json) | Field | Type | Required | Description | |---|---|---|---| | `confirm` | boolean | yes | Must be `true`. | | `reason` | string | | Optional reason, stored with the relationship and shown to the creator. | Example — End management: ```json { "confirm": true, "reason": "Contract ended" } ``` ## Responses ### 200 Ended. | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The account id. | | `status` | `ended` | yes | Always "ended". | ```json { "id": "cmh3k9p2a0002cd5e6f7g8h9j", "status": "ended" } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"Agency endpoints act as the agency itself — send them without X-Dropfans-Account.","code":"account_header_not_allowed"}` | The request carried `X-Dropfans-Account`. Agency endpoints always act as the agency. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"This endpoint needs a verified agency's own API key.","code":"agency_key_required"}` | The key is not a verified agency’s own key — a creator’s key, or a key an agency minted for one of its creators. | | 400 | `{"error":"Send { \"confirm\": true } to end management of this creator.","code":"confirm_required"}` | `confirm` is missing or not `true`. | | 400 | `{"error":"Invalid JSON body"}` | The body is not valid JSON. | | 404 | `{"error":"Not managed by this agency.","code":"account_not_managed"}` | No ACCEPTED relationship with this account. | | 400 | `{"error":"This endpoint acts as the key’s own account or app — call it without /creators/{creatorId}.","code":"creator_scope_not_allowed"}` | Called under /api/external/v1/creators/{creatorId}/ — this endpoint always acts as the key’s own account, agency or app. | | 403 | `{"error":"This app does not have the agency API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"agency"}` | Your app has the Agency roster [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/agency/creators/$ACCOUNT_ID" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "confirm": true, "reason": "Contract ended" }' ``` ### Node ```javascript const accountId = '…'; // from an earlier response const res = await fetch(`https://www.dropfans.io/api/external/v1/agency/creators/${accountId}`, { method: 'DELETE', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ "confirm": true, "reason": "Contract ended" }), }); console.log(await res.json()); ``` ### Python ```python import os import requests account_id = "…" # from an earlier response res = requests.delete( f"https://www.dropfans.io/api/external/v1/agency/creators/{account_id}", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, json={ "confirm": True, "reason": "Contract ended", }, ) print(res.json()) ``` --- Previous: [Revoke an invite](https://www.dropfans.io/developers/reference/revoke-agency-invite.md) · Next: [Revenue across your roster](https://www.dropfans.io/developers/reference/get-agency-stats.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/v1/agency/stats — Revenue across your roster > Revenue for the agency's own account plus every managed account it may see stats for (`canViewStats`), with a per-account breakdown — the same figures as the dashboard's "All creators" view. - Source: https://www.dropfans.io/developers/reference/get-agency-stats - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/agency/stats` (the unversioned alias). Revenue for the agency's own account plus every managed account it may see stats for (`canViewStats`), with a per-account breakdown — the same figures as the dashboard's "All creators" view. Amounts are USD **dollars** (rounded to cents), excluding refunds and chargebacks: `gross` is buyer-paid minus VAT, `net` is the sellers' earnings after the platform fee. Days are UTC; the default range is the last 30 days. For one account's day-by-day chart and transactions, call [GET /api/external/v1/earnings](https://www.dropfans.io/developers/reference/get-earnings.md) with `X-Dropfans-Account`. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. Needs a verified agency's own key, sent without `X-Dropfans-Account` — these endpoints act as the agency itself. ## Query parameters | Field | Type | Required | Description | |---|---|---|---| | `startDate` | string | | First UTC day (YYYY-MM-DD). Defaults to 29 days before today, so the default range is the last 30 days. | | `endDate` | string | | Last UTC day (YYYY-MM-DD), inclusive. Defaults to today. | ## Responses ### 200 Totals and per-account figures. | Field | Type | Required | Description | |---|---|---|---| | `startDate` | string | yes | First UTC day included. | | `endDate` | string | yes | Last UTC day included. | | `currency` | `USD` | yes | Always "USD". | | `total` | RevenueTotals | yes | Sum over every account below. | | `accounts` | object[] | yes | The agency’s own account first, then every ACCEPTED managed account it may see stats for (canViewStats). | ```json { "startDate": "2026-10-01", "endDate": "2026-10-26", "currency": "USD", "total": { "gross": 1240.5, "net": 992.4, "sales": 31, "tips": 12, "subscriptionPayments": 9 }, "accounts": [ { "id": "cmh1agc0q0001ab7k2p9q4r8s", "username": "starlight_agency", "name": "Starlight Agency", "kind": "agency", "gross": 0, "net": 0, "sales": 0, "tips": 0, "subscriptionPayments": 0 }, { "id": "cmh4v2q8k0003xy9z7w6v5u4t", "username": "luna_ai", "name": "Luna", "kind": "creator", "gross": 820, "net": 656, "sales": 22, "tips": 8, "subscriptionPayments": 6 }, { "id": "cmh3k9p2a0002cd5e6f7g8h9j", "username": "valeria", "name": "Valeria", "kind": "creator", "gross": 420.5, "net": 336.4, "sales": 9, "tips": 4, "subscriptionPayments": 3 } ] } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"Agency endpoints act as the agency itself — send them without X-Dropfans-Account.","code":"account_header_not_allowed"}` | The request carried `X-Dropfans-Account`. Agency endpoints always act as the agency. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"This endpoint needs a verified agency's own API key.","code":"agency_key_required"}` | The key is not a verified agency’s own key — a creator’s key, or a key an agency minted for one of its creators. | | 400 | `{"error":"startDate/endDate must be YYYY-MM-DD with start ≤ end"}` | Malformed dates, or start after end. | | 400 | `{"error":"This endpoint acts as the key’s own account or app — call it without /creators/{creatorId}.","code":"creator_scope_not_allowed"}` | Called under /api/external/v1/creators/{creatorId}/ — this endpoint always acts as the key’s own account, agency or app. | | 403 | `{"error":"This app does not have the agency API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"agency"}` | Your app has the Agency roster [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/agency/stats?startDate=2026-10-01&endDate=2026-10-26" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/agency/stats?startDate=2026-10-01&endDate=2026-10-26`, { 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/v1/agency/stats", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, params={"startDate": "2026-10-01", "endDate": "2026-10-26"}, ) print(res.json()) ``` --- Previous: [Stop managing a creator](https://www.dropfans.io/developers/reference/end-management.md) · Next: [Daily earnings per creator](https://www.dropfans.io/developers/reference/list-agency-earnings.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/v1/agency/earnings — Daily earnings per creator > One row per managed creator per day with revenue, across every account you may see stats for (`canViewStats`), in integer cents. Paged with a cursor. - Source: https://www.dropfans.io/developers/reference/list-agency-earnings - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/agency/earnings` (the unversioned alias). One row per managed creator per day with revenue, across every account you may see stats for (`canViewStats`), in integer cents. Paged with a cursor. Rows come ordered by `date`, then `creatorId`; days without revenue have no row. The figures match [GET /api/external/v1/earnings/summary](https://www.dropfans.io/developers/reference/get-earnings-summary.md) for that creator: refunds and chargebacks are left out, and days are whole local days in `timezone`. The agency’s own sales are not included — read those with the summary, without `X-Dropfans-Account`. Keep `startDate`, `endDate` and `timezone` the same while you follow `nextCursor`. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. Needs a verified agency's own key, sent without `X-Dropfans-Account` — these endpoints act as the agency itself. ## Query parameters | Field | Type | Required | Description | |---|---|---|---| | `startDate` | string | | First local day (YYYY-MM-DD). Defaults to 29 days before `endDate`. At most 366 days per window. | | `endDate` | string | | Last local day (YYYY-MM-DD), inclusive. Defaults to today in `timezone`. | | `timezone` | string | | IANA timezone for the days. `tz` is accepted too; an unknown zone is a 400. | | `limit` | integer | | Rows per page. | | `cursor` | string | | `nextCursor` from the previous page. | ## Responses ### 200 A page of rows. All money in cents. | Field | Type | Required | Description | |---|---|---|---| | `startDate` | string | yes | The window’s first day. | | `endDate` | string | yes | The window’s last day (inclusive). | | `timezone` | string | yes | The timezone used. | | `currency` | string | yes | Always USD. | | `data` | object[] | yes | Rows, by date then creatorId. | | `nextCursor` | string \\| null | | Pass as `cursor` for the next page; null on the last page. | ```json { "startDate": "2026-10-01", "endDate": "2026-10-31", "timezone": "Europe/Stockholm", "currency": "USD", "data": [ { "creatorId": "cmh4v2q8k0003xy9z7w6v5u4t", "date": "2026-10-01", "grossCents": 8200, "netCents": 6560, "count": 3 }, { "creatorId": "cmh3k9p2a0002cd5e6f7g8h9j", "date": "2026-10-01", "grossCents": 4050, "netCents": 3240, "count": 2 } ], "nextCursor": "MjAyNi0xMC0wMXxjbWg0djJxOGswMDAzeHk5ejd3NnY1dTR0" } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"Agency endpoints act as the agency itself — send them without X-Dropfans-Account.","code":"account_header_not_allowed"}` | The request carried `X-Dropfans-Account`. Agency endpoints always act as the agency. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"This endpoint needs a verified agency's own API key.","code":"agency_key_required"}` | The key is not a verified agency’s own key — a creator’s key, or a key an agency minted for one of its creators. | | 400 | `{"error":"timezone must be an IANA timezone name, e.g. Europe/Stockholm","code":"invalid_timezone"}` | Unknown `timezone`. | | 400 | `{"error":"startDate and endDate must be YYYY-MM-DD dates","code":"invalid_date"}` | Malformed or impossible date. | | 400 | `{"error":"endDate must be on or after startDate","code":"invalid_date_range"}` | `startDate` is after `endDate`. | | 400 | `{"error":"Date range too large — at most 366 days.","code":"range_too_large"}` | The window is longer than 366 days. | | 400 | `{"error":"limit must be an integer from 1 to 500","code":"invalid_limit"}` | Bad `limit`. | | 400 | `{"error":"Invalid cursor","code":"invalid_cursor"}` | The cursor was not one we returned. | | 400 | `{"error":"This endpoint acts as the key’s own account or app — call it without /creators/{creatorId}.","code":"creator_scope_not_allowed"}` | Called under /api/external/v1/creators/{creatorId}/ — this endpoint always acts as the key’s own account, agency or app. | | 403 | `{"error":"This app does not have the agency API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"agency"}` | Your app has the Agency roster [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/agency/earnings?startDate=2026-10-01&endDate=2026-10-31&timezone=Europe%2FStockholm" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/agency/earnings?startDate=2026-10-01&endDate=2026-10-31&timezone=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/v1/agency/earnings", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, params={"startDate": "2026-10-01", "endDate": "2026-10-31", "timezone": "Europe/Stockholm"}, ) print(res.json()) ``` --- Previous: [Revenue across your roster](https://www.dropfans.io/developers/reference/get-agency-stats.md) · Next: [List the creators you can act for](https://www.dropfans.io/developers/reference/list-creators.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/v1/creators — List the creators you can act for > Every account your key can act for with /api/external/v1/creators/{creatorId}/…: the creators and AI personas your agency manages, and the accounts your app onboarded. - Source: https://www.dropfans.io/developers/reference/list-creators - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/creators` (the unversioned alias). Every account your key can act for with /api/external/v1/creators/{creatorId}/…: the creators and AI personas your agency manages, and the accounts your app onboarded. An agency’s own key sees its accepted creators (`managed`) and its personas (`ai_persona`). Your app owner’s own key sees the accounts your app created with [POST /accounts](https://www.dropfans.io/developers/reference/create-account.md) (`onboarded`). A key that is both sees both; an account on both lists shows once, as the agency relationship. `active: false` means a creator-scoped call would be refused right now (403 `account_not_managed`). For an `onboarded` account that happens when it is restricted, or when it no longer holds a key bound to your app — the creator revoked your access by deleting it. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Query parameters | Field | Type | Required | Description | |---|---|---|---| | `relationship` | `managed` \\| `ai_persona` \\| `onboarded` | | Only this relationship. | | `active` | `true` \\| `false` | | Only accounts that are (`true`) or are not (`false`) usable right now. | | `limit` | integer | | Page size. | | `cursor` | string | | `nextCursor` from the previous page. | ## Responses ### 200 One page, sorted by id. | Field | Type | Required | Description | |---|---|---|---| | `data` | RosterCreator[] | yes | The accounts. | | `nextCursor` | string \\| null | | Pass as `cursor` for the next page; null on the last page. | ```json { "data": [ { "id": "cmh4v2q8k0003xy9z7w6v5u4t", "username": "valeria", "name": "Valeria", "image": null, "relationship": "ai_persona", "active": true, "permissions": { "canEditProfile": true, "canManageDrops": true, "canManageVault": true, "canViewStats": true, "canManageCoPerformers": true }, "creatorPath": "/api/external/v1/creators/cmh4v2q8k0003xy9z7w6v5u4t", "profileUrl": "https://www.dropfans.io/u/valeria", "since": "2026-10-20T09:00:00.000Z" } ], "nextCursor": null } ``` ## Errors | Status | Body | When | |---|---|---| | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"This endpoint needs a verified agency's own key or your app owner's own key.","code":"roster_key_required"}` | Neither a verified agency’s own key nor the key of your app’s owner — e.g. a creator’s key, or a key an agency minted for one of its creators. | | 400 | `{"error":"This endpoint only acts as the key’s own account — send it without X-Dropfans-Account.","code":"account_header_not_allowed"}` | The request carried `X-Dropfans-Account`. | | 400 | `{"error":"limit must be 1–100","code":"invalid_limit"}` | Bad `limit` (also `invalid_relationship`, `invalid_active`). | ## 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/v1/creators" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/creators`, { 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/v1/creators", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [Daily earnings per creator](https://www.dropfans.io/developers/reference/list-agency-earnings.md) · Next: [Get one creator you can act for](https://www.dropfans.io/developers/reference/get-creator.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/v1/creators/{id} — Get one creator you can act for > One account from [GET /creators](/developers/reference/list-creators): its relationship to you, your permissions on it and whether creator-scoped calls work for it right now. - Source: https://www.dropfans.io/developers/reference/get-creator - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/creators/{id}` (the unversioned alias). One account from [GET /creators](https://www.dropfans.io/developers/reference/list-creators.md): its relationship to you, your permissions on it and whether creator-scoped calls work for it right now. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Path parameters | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The account id. | ## Responses ### 200 The account. | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Account id — the `{creatorId}` of /api/external/v1/creators/{creatorId}/…. | | `username` | string \\| null | | Public @handle, without the @. | | `name` | string \\| null | | Display name. | | `image` | string \\| null | | Profile image URL. | | `relationship` | `managed` \\| `ai_persona` \\| `onboarded` | yes | `managed` = a creator who accepted your agency; `ai_persona` = a persona your agency runs; `onboarded` = an account your app created with POST /accounts. | | `active` | boolean | yes | Whether creator-scoped calls are accepted right now. False when the account is suspended, banned or deleted — or, for `onboarded`, when it no longer holds a key bound to your app (the creator revoked your access). | | `permissions` | AgencyPermissions | yes | What you may do on the account. `onboarded` accounts get everything except co-performers. | | `creatorPath` | string | yes | Prefix for creator-scoped calls, e.g. `${creatorPath}/drops`. | | `profileUrl` | string \\| null | | Public profile URL, null until the account has a username. | | `since` | string \\| null | | When the management was accepted, or when your app created the account. | ```json { "id": "cmh4v2q8k0003xy9z7w6v5u4t", "username": "valeria", "name": "Valeria", "image": null, "relationship": "ai_persona", "active": true, "permissions": { "canEditProfile": true, "canManageDrops": true, "canManageVault": true, "canViewStats": true, "canManageCoPerformers": true }, "creatorPath": "/api/external/v1/creators/cmh4v2q8k0003xy9z7w6v5u4t", "profileUrl": "https://www.dropfans.io/u/valeria", "since": "2026-10-20T09:00:00.000Z" } ``` ## Errors | Status | Body | When | |---|---|---| | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"This endpoint needs a verified agency's own key or your app owner's own key.","code":"roster_key_required"}` | Neither a verified agency’s own key nor the key of your app’s owner — e.g. a creator’s key, or a key an agency minted for one of its creators. | | 400 | `{"error":"This endpoint only acts as the key’s own account — send it without X-Dropfans-Account.","code":"account_header_not_allowed"}` | The request carried `X-Dropfans-Account`. | | 404 | `{"error":"Creator not found","code":"creator_not_found"}` | Not on your roster — the same answer for an id that does not exist. | ## 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/v1/creators/$ID" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const id = '…'; // from an earlier response const res = await fetch(`https://www.dropfans.io/api/external/v1/creators/${id}`, { headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, }, }); console.log(await res.json()); ``` ### Python ```python import os import requests id = "…" # from an earlier response res = requests.get( f"https://www.dropfans.io/api/external/v1/creators/{id}", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [List the creators you can act for](https://www.dropfans.io/developers/reference/list-creators.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/v1/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 Also served at `/api/external/vault` (the unversioned alias). 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/v1/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 dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `GET /api/external/v1/creators/{creatorId}/vault` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canManageVault`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## 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. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canManageVault"}` | For a managed creator: the agency lacks `canManageVault` on that account. | | 403 | `{"error":"This app does not have the vault API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"vault"}` | Your app has the Vault [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/vault?page=1&folderId=all" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/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/v1/vault", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, params={"page": 1, "folderId": "all"}, ) print(res.json()) ``` --- Previous: [Get one creator you can act for](https://www.dropfans.io/developers/reference/get-creator.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/v1/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 Also served at `/api/external/vault` (the unversioned alias). 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/v1/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 dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `POST /api/external/v1/creators/{creatorId}/vault` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canManageVault`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## 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. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canManageVault"}` | For a managed creator: the agency lacks `canManageVault` on that account. | | 403 | `{"error":"This app does not have the vault API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"vault"}` | Your app has the Vault [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/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/v1/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/v1/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/v1/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 Also served at `/api/external/vault/{id}` (the unversioned alias). 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 dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `DELETE /api/external/v1/creators/{creatorId}/vault/{id}` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canManageVault`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## 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. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canManageVault"}` | For a managed creator: the agency lacks `canManageVault` on that account. | | 403 | `{"error":"This app does not have the vault API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"vault"}` | Your app has the Vault [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/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/v1/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/v1/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/v1/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 Also served at `/api/external/vault/{id}/folder` (the unversioned alias). 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 dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `PATCH /api/external/v1/creators/{creatorId}/vault/{id}/folder` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canManageVault`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## 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. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canManageVault"}` | For a managed creator: the agency lacks `canManageVault` on that account. | | 403 | `{"error":"This app does not have the vault API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"vault"}` | Your app has the Vault [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/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/v1/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/v1/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/v1/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 Also served at `/api/external/vault/{id}/tags` (the unversioned alias). 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 dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `PATCH /api/external/v1/creators/{creatorId}/vault/{id}/tags` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canManageVault`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## 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. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canManageVault"}` | For a managed creator: the agency lacks `canManageVault` on that account. | | 403 | `{"error":"This app does not have the vault API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"vault"}` | Your app has the Vault [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/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/v1/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/v1/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/v1/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 Also served at `/api/external/vault/folders` (the unversioned alias). 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 dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `GET /api/external/v1/creators/{creatorId}/vault/folders` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canManageVault`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## 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. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canManageVault"}` | For a managed creator: the agency lacks `canManageVault` on that account. | | 403 | `{"error":"This app does not have the vault API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"vault"}` | Your app has the Vault [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/vault/folders" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/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/v1/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/v1/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 Also served at `/api/external/vault/folders` (the unversioned alias). 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 dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `POST /api/external/v1/creators/{creatorId}/vault/folders` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canManageVault`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## 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/v1/vault counts only items visible at that call’s moderation filter, while GET /api/external/v1/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. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canManageVault"}` | For a managed creator: the agency lacks `canManageVault` on that account. | | 403 | `{"error":"This app does not have the vault API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"vault"}` | Your app has the Vault [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/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/v1/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/v1/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/v1/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 Also served at `/api/external/vault/folders/{folderId}` (the unversioned alias). 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 dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `DELETE /api/external/v1/creators/{creatorId}/vault/folders/{folderId}` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canManageVault`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## 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. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canManageVault"}` | For a managed creator: the agency lacks `canManageVault` on that account. | | 403 | `{"error":"This app does not have the vault API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"vault"}` | Your app has the Vault [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/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/v1/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/v1/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/v1/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 Also served at `/api/external/vault/video-upload` (the unversioned alias). Starts a direct-to-CDN video upload and returns presigned TUS credentials. Videos cannot ride through [POST /api/external/v1/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 dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `POST /api/external/v1/creators/{creatorId}/vault/video-upload` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canManageVault`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## 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. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canManageVault"}` | For a managed creator: the agency lacks `canManageVault` on that account. | | 403 | `{"error":"This app does not have the vault API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"vault"}` | Your app has the Vault [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/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/v1/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/v1/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/v1/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 Also served at `/api/external/vault/video-upload/complete` (the unversioned alias). 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 dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `POST /api/external/v1/creators/{creatorId}/vault/video-upload/complete` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canManageVault`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## 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. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canManageVault"}` | For a managed creator: the agency lacks `canManageVault` on that account. | | 403 | `{"error":"This app does not have the vault API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"vault"}` | Your app has the Vault [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/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/v1/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/v1/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/v1/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/v1/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 Also served at `/api/external/vault/video-status` (the unversioned alias). 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 dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `POST /api/external/v1/creators/{creatorId}/vault/video-status` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canManageVault`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## 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. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canManageVault"}` | For a managed creator: the agency lacks `canManageVault` on that account. | | 403 | `{"error":"This app does not have the vault API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"vault"}` | Your app has the Vault [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/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/v1/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/v1/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: [Start a media upload session](https://www.dropfans.io/developers/reference/create-media-upload.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # POST /api/external/v1/media/uploads — Start a media upload session > Opens an upload session for one image, video or audio file. Video gets tus credentials for a direct upload; image and audio get 3 MB part URLs to PUT. - Source: https://www.dropfans.io/developers/reference/create-media-upload - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/media/uploads` (the unversioned alias). Opens an upload session for one image, video or audio file. Video gets tus credentials for a direct upload; image and audio get 3 MB part URLs to PUT. Then call [complete](https://www.dropfans.io/developers/reference/complete-media-upload.md) and poll [the session](https://www.dropfans.io/developers/reference/get-media-upload.md) for the vault item and its moderation. The session is valid 8 hours; abandoned sessions are cleaned up daily. **Video** (up to 500 MB): upload the bytes with any tus client to `tus.endpoint`, sending `tus.headers` as headers — the same direct-to-CDN upload as [start-video-upload](https://www.dropfans.io/developers/reference/start-video-upload.md). **Image** (up to 30 MB) and **audio** (up to 20 MB): PUT each part's bytes to its `url` with your API key — parts go through us because our storage has no presigned writes. Parts can go in any order and in parallel. The stored file has its metadata (EXIF, GPS, XMP, ID3 tags …) removed, as with every vault upload. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `POST /api/external/v1/creators/{creatorId}/media/uploads` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canManageVault`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## Request body (application/json) | Field | Type | Required | Description | |---|---|---|---| | `kind` | `image` \\| `video` \\| `audio` | yes | What you are uploading. | | `filename` | string | yes | File name, stored on the vault item. | | `sizeBytes` | integer | yes | Exact file size in bytes. Image ≤ 30 MB, video ≤ 500 MB, audio ≤ 20 MB. | | `contentType` | string | yes | MIME type; must match `kind` (image/*, video/*, audio/*). | | `folderId` | string \\| null | | Optional vault folder of this account. | | `durationSeconds` | number | | Audio only: the voice note's length (capped at 3600s). | Example — An 8 MB photo: ```json { "kind": "image", "filename": "beach.jpg", "sizeBytes": 7904267, "contentType": "image/jpeg" } ``` Example — A video: ```json { "kind": "video", "filename": "teaser.mp4", "sizeBytes": 52428800, "contentType": "video/mp4" } ``` ## Responses ### 201 Session opened: the session fields plus `parts` (image / audio) or `tus` (video). | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Upload id. | | `kind` | `image` \\| `video` \\| `audio` | yes | What you are uploading. | | `status` | `uploading` \\| `processing` \\| `completed` \\| `failed` \\| `expired` | yes | `uploading` until you complete it; `processing` while a complete runs; `completed` once the vault item exists. | | `fileName` | string | yes | The file name, stored on the vault item. | | `contentType` | string | yes | The stored content type. | | `sizeBytes` | integer | yes | The declared size. | | `folderId` | string \\| null | | The vault folder the item goes in. | | `partSize` | integer \\| null | | Image / audio: bytes per part (the last part is the remainder). Null for video. | | `totalParts` | integer \\| null | | Image / audio: number of parts. Null for video. | | `receivedParts` | integer[] \\| null | | Part numbers received so far. | | `missingParts` | integer[] \\| null | | Part numbers still to upload. | | `vaultItemId` | string \\| null | | The vault item, once completed. | | `moderationStatus` | `PENDING` \\| `APPROVED` \\| `FLAGGED` \\| `REJECTED` \\| null | | The vault item’s moderation status, once completed. | | `bunnyStreamId` | string \\| null | | Video: the Stream video id — poll [video-status](https://www.dropfans.io/developers/reference/video-status.md) with it. | | `error` | object \\| null | | `{ code, message }` when the upload failed or expired. | | `error.code` | string | yes | Machine-readable reason. | | `error.message` | string \\| null | | Human-readable reason. | | `expiresAt` | string | yes | Upload the parts (or the tus bytes) and complete before this. | | `createdAt` | string | yes | When the session was opened. | | `completedAt` | string \\| null | | When it completed. | | `parts` | object[] | | Image / audio: PUT each part to its `url`. | | `tus` | object | | Video: tus upload credentials (the CDN API key itself is never exposed). | | `tus.endpoint` | string | yes | tus endpoint. | | `tus.videoId` | string | yes | The Stream video id. | | `tus.libraryId` | string | yes | The Stream library id. | | `tus.signature` | string | yes | sha256 upload signature. | | `tus.expires` | integer | yes | Signature expiry (unix seconds). | | `tus.headers` | object | yes | Send these as tus headers: AuthorizationSignature, AuthorizationExpire, VideoId, LibraryId. | ```json { "id": "mu_3f9c1a7be0d24c55a1e8b6f2", "kind": "image", "status": "uploading", "fileName": "beach.jpg", "contentType": "image/jpeg", "sizeBytes": 7904267, "folderId": null, "partSize": 3145728, "totalParts": 3, "receivedParts": [], "missingParts": [ 1, 2, 3 ], "vaultItemId": null, "moderationStatus": null, "bunnyStreamId": null, "error": null, "expiresAt": "2026-10-31T18:00:00.000Z", "createdAt": "2026-10-31T10:00:00.000Z", "completedAt": null, "parts": [ { "partNumber": 1, "sizeBytes": 3145728, "method": "PUT", "url": "https://www.dropfans.io/api/external/v1/media/uploads/mu_3f9c1a7be0d24c55a1e8b6f2/parts/1" }, { "partNumber": 2, "sizeBytes": 3145728, "method": "PUT", "url": "https://www.dropfans.io/api/external/v1/media/uploads/mu_3f9c1a7be0d24c55a1e8b6f2/parts/2" }, { "partNumber": 3, "sizeBytes": 1612811, "method": "PUT", "url": "https://www.dropfans.io/api/external/v1/media/uploads/mu_3f9c1a7be0d24c55a1e8b6f2/parts/3" } ] } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"kind must be image, video or audio","code":"invalid_kind"}` | Unknown `kind`. | | 400 | `{"error":"filename is required (at most 255 characters)","code":"invalid_filename"}` | `filename` missing or too long. | | 400 | `{"error":"sizeBytes must be a positive integer","code":"invalid_size"}` | `sizeBytes` missing or not a positive integer. | | 400 | `{"error":"contentType must be a image/* type","code":"invalid_content_type"}` | `contentType` does not match `kind`. | | 400 | `{"error":"folderId is not one of this account’s vault folders","code":"folder_not_found"}` | Unknown folder, or another account’s. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 413 | `{"error":"File too large. Max 30MB for image.","code":"file_too_large","maxBytes":31457280}` | `sizeBytes` is over the limit for `kind`. | | 502 | `{"error":"Could not start the video upload — retry later","code":"video_create_failed"}` | The CDN did not create the video. | | 503 | `{"error":"Video uploads are not available right now","code":"video_uploads_unavailable"}` | Video (or, with media_uploads_unavailable, image / audio) uploads are switched off. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canManageVault"}` | For a managed creator: the agency lacks `canManageVault` on that account. | | 403 | `{"error":"This app does not have the vault API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"vault"}` | Your app has the Vault [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/media/uploads" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "kind": "image", "filename": "beach.jpg", "sizeBytes": 7904267, "contentType": "image/jpeg" }' ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/media/uploads`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ "kind": "image", "filename": "beach.jpg", "sizeBytes": 7904267, "contentType": "image/jpeg" }), }); console.log(await res.json()); ``` ### Python ```python import os import requests res = requests.post( "https://www.dropfans.io/api/external/v1/media/uploads", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, json={ "kind": "image", "filename": "beach.jpg", "sizeBytes": 7904267, "contentType": "image/jpeg", }, ) print(res.json()) ``` --- Previous: [Check video transcoding status (batch)](https://www.dropfans.io/developers/reference/video-status.md) · Next: [Upload one part (image / audio)](https://www.dropfans.io/developers/reference/upload-media-part.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # PUT /api/external/v1/media/uploads/{id}/parts/{partNumber} — Upload one part (image / audio) > Uploads one part of an image or audio session: the raw bytes as the request body (`Content-Type: application/octet-stream`). - Source: https://www.dropfans.io/developers/reference/upload-media-part - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/media/uploads/{id}/parts/{partNumber}` (the unversioned alias). Uploads one part of an image or audio session: the raw bytes as the request body (`Content-Type: application/octet-stream`). Every part except the last is exactly `partSize` (3 MB); the last one is the remainder. Send parts in any order and in parallel. Repeating a part replaces it, so retry a failed part freely. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `PUT /api/external/v1/creators/{creatorId}/media/uploads/{id}/parts/{partNumber}` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canManageVault`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## Path parameters | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The upload `id` from POST /api/external/v1/media/uploads. | | `partNumber` | integer | yes | 1-based part number, up to `totalParts`. | ## Responses ### 200 Part stored. | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Upload id. | | `partNumber` | integer | yes | The part you sent. | | `sizeBytes` | integer | yes | Bytes received. | | `receivedParts` | integer[] | yes | Parts received so far. | | `missingParts` | integer[] | yes | Parts still missing — empty means you can complete. | ```json { "id": "mu_3f9c1a7be0d24c55a1e8b6f2", "partNumber": 1, "sizeBytes": 3145728, "receivedParts": [ 1 ], "missingParts": [ 2, 3 ] } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"Part number must be 1–3","code":"invalid_part_number"}` | Part number out of range. | | 400 | `{"error":"Part 3 must be exactly 1612811 bytes (got 1000)","code":"part_size_mismatch","expectedBytes":1612811}` | The part is not exactly the size it must have. | | 400 | `{"error":"Video sessions upload with tus — send the bytes to the tus endpoint","code":"parts_not_used"}` | A video session. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 404 | `{"error":"Upload not found","code":"upload_not_found"}` | Unknown id, or an upload of another account. | | 409 | `{"error":"This upload is completed and takes no more parts","code":"upload_not_accepting_parts","status":"completed"}` | The session is completed, failed or being completed. | | 410 | `{"error":"This upload session has expired — start a new one","code":"upload_expired"}` | Past `expiresAt`. | | 413 | `{"error":"Parts are at most 3145728 bytes","code":"part_too_large","partSize":3145728}` | The part is bigger than `partSize`. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canManageVault"}` | For a managed creator: the agency lacks `canManageVault` on that account. | | 403 | `{"error":"This app does not have the vault API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"vault"}` | Your app has the Vault [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/media/uploads/$VAULT_ITEM_ID/parts/$VAULT_ITEM_ID" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const vaultItemId = '…'; // from an earlier response const vaultItemId = '…'; // from an earlier response const res = await fetch(`https://www.dropfans.io/api/external/v1/media/uploads/${vaultItemId}/parts/${vaultItemId}`, { method: 'PUT', 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 vault_item_id = "…" # from an earlier response res = requests.put( f"https://www.dropfans.io/api/external/v1/media/uploads/{vault_item_id}/parts/{vault_item_id}", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [Start a media upload session](https://www.dropfans.io/developers/reference/create-media-upload.md) · Next: [Complete a media upload](https://www.dropfans.io/developers/reference/complete-media-upload.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # POST /api/external/v1/media/uploads/{id}/complete — Complete a media upload > Turns the upload into a vault item and starts moderation. Image / audio: the parts are joined and stored with their metadata removed. Video: the finished tus upload is registered. - Source: https://www.dropfans.io/developers/reference/complete-media-upload - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/media/uploads/{id}/complete` (the unversioned alias). Turns the upload into a vault item and starts moderation. Image / audio: the parts are joined and stored with their metadata removed. Video: the finished tus upload is registered. **Idempotent**: completing a completed session returns the same body, so retry freely. The item starts `PENDING`; poll [GET the session](https://www.dropfans.io/developers/reference/get-media-upload.md) for `moderationStatus`, and for a video [video-status](https://www.dropfans.io/developers/reference/video-status.md) for transcoding. For a video, **409 `upload_not_finished` means retry**: the CDN can lag a few seconds after your tus client reports done, so back off (2s, 5s, 10s, 30s) and call again. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `POST /api/external/v1/creators/{creatorId}/media/uploads/{id}/complete` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canManageVault`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## Path parameters | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The upload `id` from POST /api/external/v1/media/uploads. | ## Responses ### 200 Completed (or already completed). | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Upload id. | | `kind` | `image` \\| `video` \\| `audio` | yes | What you are uploading. | | `status` | `uploading` \\| `processing` \\| `completed` \\| `failed` \\| `expired` | yes | `uploading` until you complete it; `processing` while a complete runs; `completed` once the vault item exists. | | `fileName` | string | yes | The file name, stored on the vault item. | | `contentType` | string | yes | The stored content type. | | `sizeBytes` | integer | yes | The declared size. | | `folderId` | string \\| null | | The vault folder the item goes in. | | `partSize` | integer \\| null | | Image / audio: bytes per part (the last part is the remainder). Null for video. | | `totalParts` | integer \\| null | | Image / audio: number of parts. Null for video. | | `receivedParts` | integer[] \\| null | | Part numbers received so far. | | `missingParts` | integer[] \\| null | | Part numbers still to upload. | | `vaultItemId` | string \\| null | | The vault item, once completed. | | `moderationStatus` | `PENDING` \\| `APPROVED` \\| `FLAGGED` \\| `REJECTED` \\| null | | The vault item’s moderation status, once completed. | | `bunnyStreamId` | string \\| null | | Video: the Stream video id — poll [video-status](https://www.dropfans.io/developers/reference/video-status.md) with it. | | `error` | object \\| null | | `{ code, message }` when the upload failed or expired. | | `error.code` | string | yes | Machine-readable reason. | | `error.message` | string \\| null | | Human-readable reason. | | `expiresAt` | string | yes | Upload the parts (or the tus bytes) and complete before this. | | `createdAt` | string | yes | When the session was opened. | | `completedAt` | string \\| null | | When it completed. | ```json { "id": "mu_3f9c1a7be0d24c55a1e8b6f2", "kind": "image", "status": "completed", "fileName": "beach.jpg", "contentType": "image/jpeg", "sizeBytes": 7904267, "folderId": null, "partSize": 3145728, "totalParts": 3, "receivedParts": [ 1, 2, 3 ], "missingParts": [], "vaultItemId": "clxv9z8y70003item", "moderationStatus": "PENDING", "bunnyStreamId": null, "error": null, "expiresAt": "2026-10-31T18:00:00.000Z", "createdAt": "2026-10-31T10:00:00.000Z", "completedAt": "2026-10-31T10:00:41.000Z" } ``` ## Errors | Status | Body | When | |---|---|---| | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 404 | `{"error":"Upload not found","code":"upload_not_found"}` | Unknown id, or an upload of another account. | | 409 | `{"error":"Upload the missing parts first: 3","code":"missing_parts","missingParts":[3]}` | Image / audio: parts are missing. Upload them and complete again. | | 409 | `{"error":"The video has not finished uploading — retry in a few seconds","code":"upload_not_finished"}` | Video: the CDN has not received all the bytes yet — back off and retry. | | 409 | `{"error":"This upload is already being completed — poll GET for its status","code":"complete_in_progress"}` | Another complete call is running for this session. | | 409 | `{"error":"File too large. Max 30MB for image.","code":"upload_failed","reason":"file_too_large"}` | The session already failed — start a new one. | | 410 | `{"error":"This upload session has expired — start a new one","code":"upload_expired"}` | Past `expiresAt`. | | 413 | `{"error":"Video too large. Max 500MB.","code":"file_too_large"}` | The stored file is over the limit; the upload is discarded. | | 415 | `{"error":"Unsupported image format. Please upload a JPEG, PNG, WebP, GIF, HEIC or AVIF.","code":"UNSUPPORTED_FORMAT"}` | Image / audio: we cannot remove the file’s metadata safely (also 422 MALFORMED / UNSANITISABLE), so it is not stored. | | 500 | `{"error":"Could not complete the upload — retry","code":"complete_failed"}` | Unexpected failure — the session goes back to `uploading`; retry. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canManageVault"}` | For a managed creator: the agency lacks `canManageVault` on that account. | | 403 | `{"error":"This app does not have the vault API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"vault"}` | Your app has the Vault [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/media/uploads/$VAULT_ITEM_ID/complete" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const vaultItemId = '…'; // from an earlier response const res = await fetch(`https://www.dropfans.io/api/external/v1/media/uploads/${vaultItemId}/complete`, { method: 'POST', 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.post( f"https://www.dropfans.io/api/external/v1/media/uploads/{vault_item_id}/complete", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [Upload one part (image / audio)](https://www.dropfans.io/developers/reference/upload-media-part.md) · Next: [Get a media upload](https://www.dropfans.io/developers/reference/get-media-upload.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/v1/media/uploads/{id} — Get a media upload > The session's status: which parts arrived, the vault item once completed, and its moderation status. - Source: https://www.dropfans.io/developers/reference/get-media-upload - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/media/uploads/{id}` (the unversioned alias). The session's status: which parts arrived, the vault item once completed, and its moderation status. Poll this after [complete](https://www.dropfans.io/developers/reference/complete-media-upload.md) until `moderationStatus` leaves `PENDING` — every 15–30 seconds is plenty. Sessions are kept for 30 days. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `GET /api/external/v1/creators/{creatorId}/media/uploads/{id}` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canManageVault`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## Path parameters | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The upload `id` from POST /api/external/v1/media/uploads. | ## Responses ### 200 The session. | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Upload id. | | `kind` | `image` \\| `video` \\| `audio` | yes | What you are uploading. | | `status` | `uploading` \\| `processing` \\| `completed` \\| `failed` \\| `expired` | yes | `uploading` until you complete it; `processing` while a complete runs; `completed` once the vault item exists. | | `fileName` | string | yes | The file name, stored on the vault item. | | `contentType` | string | yes | The stored content type. | | `sizeBytes` | integer | yes | The declared size. | | `folderId` | string \\| null | | The vault folder the item goes in. | | `partSize` | integer \\| null | | Image / audio: bytes per part (the last part is the remainder). Null for video. | | `totalParts` | integer \\| null | | Image / audio: number of parts. Null for video. | | `receivedParts` | integer[] \\| null | | Part numbers received so far. | | `missingParts` | integer[] \\| null | | Part numbers still to upload. | | `vaultItemId` | string \\| null | | The vault item, once completed. | | `moderationStatus` | `PENDING` \\| `APPROVED` \\| `FLAGGED` \\| `REJECTED` \\| null | | The vault item’s moderation status, once completed. | | `bunnyStreamId` | string \\| null | | Video: the Stream video id — poll [video-status](https://www.dropfans.io/developers/reference/video-status.md) with it. | | `error` | object \\| null | | `{ code, message }` when the upload failed or expired. | | `error.code` | string | yes | Machine-readable reason. | | `error.message` | string \\| null | | Human-readable reason. | | `expiresAt` | string | yes | Upload the parts (or the tus bytes) and complete before this. | | `createdAt` | string | yes | When the session was opened. | | `completedAt` | string \\| null | | When it completed. | ```json { "id": "mu_3f9c1a7be0d24c55a1e8b6f2", "kind": "image", "status": "completed", "fileName": "beach.jpg", "contentType": "image/jpeg", "sizeBytes": 7904267, "folderId": null, "partSize": 3145728, "totalParts": 3, "receivedParts": [ 1, 2, 3 ], "missingParts": [], "vaultItemId": "clxv9z8y70003item", "moderationStatus": "PENDING", "bunnyStreamId": null, "error": null, "expiresAt": "2026-10-31T18:00:00.000Z", "createdAt": "2026-10-31T10:00:00.000Z", "completedAt": "2026-10-31T10:00:41.000Z" } ``` ## Errors | Status | Body | When | |---|---|---| | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 404 | `{"error":"Upload not found","code":"upload_not_found"}` | Unknown id, or an upload of another account. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canManageVault"}` | For a managed creator: the agency lacks `canManageVault` on that account. | | 403 | `{"error":"This app does not have the vault API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"vault"}` | Your app has the Vault [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/media/uploads/$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/v1/media/uploads/${vaultItemId}`, { 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.get( f"https://www.dropfans.io/api/external/v1/media/uploads/{vault_item_id}", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [Complete a media upload](https://www.dropfans.io/developers/reference/complete-media-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/v1/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 Also served at `/api/external/drops` (the unversioned alias). 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/v1/links](https://www.dropfans.io/developers/reference/get-links.md)' `telegram.buyTemplate`. **One drop, many fans.** Don't create a copy per recipient. Create the drop once and give each fan their own link with a reference: `buyUrl?m={yourRef}` on the web, or `startapp=b_{productId}_{yourRef}` in Telegram. The reference comes back on every paid order — see [Broadcast one drop to many fans](https://www.dropfans.io/developers/guides/broadcast-one-drop.md). Different prices still need separate drops. > [!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 dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `POST /api/external/v1/creators/{creatorId}/drops` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canManageDrops`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## 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 | | Images only: when true, buyers get a Download button serving a watermarked, metadata-stripped JPEG. Videos always stream behind DRM and are never downloadable. Off unless explicitly set. | | `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 | With an app key: your app’s plain buy link id (`/buy/{linkId}`) — created by POST /drops; reads return it when it exists, else `/buy/{productId}` (a drop your app created still counts for it). With a personal key: `/buy/{productId}`. For a per-fan reference, create a link with POST /api/external/v1/links. | | `linkId` | string \\| null | | The link id in buyUrl (app keys only); comes back as linkId on the sale. | | `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. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canManageDrops"}` | For a managed creator: the agency lacks `canManageDrops` on that account. | | 403 | `{"error":"This app does not have the drops API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"drops"}` | Your app has the Drops & checkout links [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/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/v1/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/v1/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/v1/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: [Get a media upload](https://www.dropfans.io/developers/reference/get-media-upload.md) · Next: [List the creator’s drops](https://www.dropfans.io/developers/reference/list-drops.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/v1/drops — List the creator’s drops > Every drop on the account, newest first, with price, who can buy it, moderation status and checkout URL — including drops made in the dashboard. - Source: https://www.dropfans.io/developers/reference/list-drops - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/drops` (the unversioned alias). Every drop on the account, newest first, with price, who can buy it, moderation status and checkout URL — including drops made in the dashboard. Page with `cursor`: pass the `nextCursor` of the previous page until it is null. For per-media status and sales of one drop, use [GET /api/external/v1/drops/{id}](https://www.dropfans.io/developers/reference/get-drop.md). ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `GET /api/external/v1/creators/{creatorId}/drops` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canManageDrops`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## Query parameters | Field | Type | Required | Description | |---|---|---|---| | `limit` | integer | | Page size (out-of-range values are clamped). | | `cursor` | string | | `nextCursor` from the previous page. | ## Responses ### 200 A page of drops. | Field | Type | Required | Description | |---|---|---|---| | `drops` | DropSummary[] | yes | Newest first. | | `nextCursor` | string \\| null | | Pass as `cursor` for the next page; null on the last page. | ```json { "drops": [ { "id": "clxdr0p000001prod", "name": "Beach set — 6 photos", "description": null, "price": 25, "currency": "USD", "accessMode": "OPEN", "allowDownload": false, "status": "APPROVED", "mediaCount": 3, "buyUrl": "https://www.dropfans.io/buy/clxdr0p000001prod", "createdAt": "2026-08-15T09:00:00.000Z" } ], "nextCursor": null } ``` ## Errors | Status | Body | When | |---|---|---| | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canManageDrops"}` | For a managed creator: the agency lacks `canManageDrops` on that account. | | 403 | `{"error":"This app does not have the drops API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"drops"}` | Your app has the Drops & checkout links [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/drops" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/drops`, { 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/v1/drops", 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: [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/v1/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 Also served at `/api/external/drops/{id}` (the unversioned alias). 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 dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `GET /api/external/v1/creators/{creatorId}/drops/{id}` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canManageDrops`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## 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 | With an app key: your app’s plain buy link id (`/buy/{linkId}`) — created by POST /drops; reads return it when it exists, else `/buy/{productId}` (a drop your app created still counts for it). With a personal key: `/buy/{productId}`. For a per-fan reference, create a link with POST /api/external/v1/links. | | `linkId` | string \\| null | | The link id in buyUrl (app keys only); comes back as linkId on the sale. | | `allowDownload` | boolean | yes | Images only: whether buyers get a Download button serving a watermarked, metadata-stripped JPEG. Videos are never downloadable (DRM stream). Defaults to false. | | `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": false, "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). | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canManageDrops"}` | For a managed creator: the agency lacks `canManageDrops` on that account. | | 403 | `{"error":"This app does not have the drops API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"drops"}` | Your app has the Drops & checkout links [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/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/v1/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/v1/drops/{product_id}", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [List the creator’s drops](https://www.dropfans.io/developers/reference/list-drops.md) · Next: [Edit a drop](https://www.dropfans.io/developers/reference/update-drop.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # PATCH /api/external/v1/drops/{id} — Edit a drop > Changes a drop's name, description, price or who can buy it — the same edit the dashboard allows. Send only what changes; `null` clears name or description. - Source: https://www.dropfans.io/developers/reference/update-drop - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/drops/{id}` (the unversioned alias). Changes a drop's name, description, price or who can buy it — the same edit the dashboard allows. Send only what changes; `null` clears name or description. Price is USD **dollars**: `0` (free) or between $5 and $750; a new price applies to future purchases only. Subscriber gating (`SUBS_FREE`, `SUBS_ONLY_PAID`) needs an active [subscription tier](https://www.dropfans.io/developers/reference/update-subscription.md). Name and description run the prohibited-word filter. The media of a drop cannot change — create a new drop instead. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `PATCH /api/external/v1/creators/{creatorId}/drops/{id}` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canManageDrops`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## Path parameters | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The product (drop) id. | ## Request body (application/json) | Field | Type | Required | Description | |---|---|---|---| | `name` | string \\| null | | Drop title, shown at checkout (cut at 200 characters). | | `description` | string \\| null | | Drop description (cut at 5,000 characters). Unlike create-drop, this one is stored. | | `price` | number | | USD dollars. 0 = free; otherwise $5–$750. Rounded to cents. | | `accessMode` | `OPEN` \\| `SUBS_FREE` \\| `SUBS_ONLY_PAID` | | `OPEN` = anyone can buy; `SUBS_FREE` = free for subscribers; `SUBS_ONLY_PAID` = only subscribers can buy. | Example — Rename and reprice: ```json { "name": "Beach set — 8 photos", "price": 30 } ``` ## Responses ### 200 The drop after the edit. | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The product (drop) id. | | `name` | string \\| null | | Drop title. | | `description` | string \\| null | | Drop description. | | `price` | number | yes | USD **dollars**. 0 means free. | | `accessMode` | `OPEN` \\| `SUBS_FREE` \\| `SUBS_ONLY_PAID` | yes | Who can buy. | | `moderationStatus` | `PENDING` \\| `APPROVED` \\| `REJECTED` \\| `FLAGGED` | yes | Same value as `status`. | | `currency` | `USD` | yes | Always "USD". | | `status` | `PENDING` \\| `APPROVED` \\| `REJECTED` \\| `FLAGGED` | yes | The drop’s moderation status. | | `buyUrl` | string | yes | With an app key: your app’s plain buy link id (`/buy/{linkId}`) — created by POST /drops; reads return it when it exists, else `/buy/{productId}` (a drop your app created still counts for it). With a personal key: `/buy/{productId}`. For a per-fan reference, create a link with POST /api/external/v1/links. | | `linkId` | string \\| null | | The link id in buyUrl (app keys only); comes back as linkId on the sale. | ```json { "id": "clxdr0p000001prod", "name": "Beach set — 8 photos", "description": null, "price": 30, "accessMode": "OPEN", "moderationStatus": "APPROVED", "currency": "USD", "status": "APPROVED", "buyUrl": "https://www.dropfans.io/buy/clxdr0p000001prod" } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"price must be a number in USD (0 = free)"}` | `price` not a number 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":"accessMode must be one of OPEN, SUBS_FREE, SUBS_ONLY_PAID"}` | Unknown accessMode. | | 400 | `{"error":"Configure an active subscription tier before gating drops to subscribers.","code":"no_active_tier"}` | Subscriber gating without an active tier. | | 400 | `{"error":"name must be a string"}` | `name` or `description` is not a string or null. | | 400 | `{"error":"Field \"name\" contains a prohibited word: \"…\"","field":"name"}` | Name or description tripped the word filter. | | 400 | `{"error":"Invalid JSON body"}` | The body is not valid JSON. | | 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). | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canManageDrops"}` | For a managed creator: the agency lacks `canManageDrops` on that account. | | 403 | `{"error":"This app does not have the drops API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"drops"}` | Your app has the Drops & checkout links [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/drops/$PRODUCT_ID" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "Beach set — 8 photos", "price": 30 }' ``` ### Node ```javascript const productId = '…'; // from an earlier response const res = await fetch(`https://www.dropfans.io/api/external/v1/drops/${productId}`, { method: 'PATCH', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ "name": "Beach set — 8 photos", "price": 30 }), }); console.log(await res.json()); ``` ### Python ```python import os import requests product_id = "…" # from an earlier response res = requests.patch( f"https://www.dropfans.io/api/external/v1/drops/{product_id}", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, json={ "name": "Beach set — 8 photos", "price": 30, }, ) print(res.json()) ``` --- Previous: [Read back a drop](https://www.dropfans.io/developers/reference/get-drop.md) · Next: [Delete a drop nobody has bought](https://www.dropfans.io/developers/reference/delete-drop.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # DELETE /api/external/v1/drops/{id} — Delete a drop nobody has bought > Deletes a drop that has no orders — not even an unfinished checkout. Once anyone has an order on it, buyers' access depends on it and the call returns `409 has_orders`. - Source: https://www.dropfans.io/developers/reference/delete-drop - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/drops/{id}` (the unversioned alias). Deletes a drop that has no orders — not even an unfinished checkout. Once anyone has an order on it, buyers' access depends on it and the call returns `409 has_orders`. The vault media stays in the vault either way. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `DELETE /api/external/v1/creators/{creatorId}/drops/{id}` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canManageDrops`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## Path parameters | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The product (drop) id. | ## Responses ### 200 Deleted. | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The deleted drop’s id. | | `deleted` | boolean | yes | Always true. | ```json { "id": "clxdr0p000001prod", "deleted": true } ``` ## 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. | | 409 | `{"error":"This drop has orders, so buyers keep access to it and it cannot be deleted.","code":"has_orders"}` | The drop has at least one order (paid or started). | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canManageDrops"}` | For a managed creator: the agency lacks `canManageDrops` on that account. | | 403 | `{"error":"This app does not have the drops API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"drops"}` | Your app has the Drops & checkout links [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/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/v1/drops/${productId}`, { method: 'DELETE', 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.delete( f"https://www.dropfans.io/api/external/v1/drops/{product_id}", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [Edit a drop](https://www.dropfans.io/developers/reference/update-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/v1/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 Also served at `/api/external/drops/{id}/previews` (the unversioned alias). 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 dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `POST /api/external/v1/creators/{creatorId}/drops/{id}/previews` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canManageDrops`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## 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. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canManageDrops"}` | For a managed creator: the agency lacks `canManageDrops` on that account. | | 403 | `{"error":"This app does not have the drops API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"drops"}` | Your app has the Drops & checkout links [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/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/v1/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/v1/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: [Delete a drop nobody has bought](https://www.dropfans.io/developers/reference/delete-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/v1/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 Also served at `/api/external/drops/check-status` (the unversioned alias). 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**. `sales` keeps its original one-entry-per-product shape; `orders` lists **every** paid order per product with its per-fan `externalRef` — use that for shared drops (or the [drop.paid webhook](https://www.dropfans.io/developers/webhooks/overview.md)). > [!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/v1/earnings](https://www.dropfans.io/developers/reference/get-earnings.md) for net truth. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `POST /api/external/v1/creators/{creatorId}/drops/check-status` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canManageDrops`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## 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. | | `orders` | OrdersByProduct | yes | productId → every paid order (newest first), each with its externalRef. Added 2026-10-17. | ```json { "sales": { "clxdr0p000001prod": { "paid": true, "saleAmountCents": 2500, "buyerEmail": "buyer@example.com" } }, "orders": { "clxdr0p000001prod": [ { "orderId": "clxor0001order", "productId": "clxdr0p000001prod", "accountId": "clx2f8a1b0001qw3k", "externalRef": "tg_48213377", "amountCents": 2500, "currency": "USD", "paidAt": "2026-10-17T16:20:00.000Z", "paidWith": "card", "status": "paid", "buyer": { "id": "clxbu0001buyer", "email": "buyer@example.com", "telegramUserId": "48213377", "telegramUsername": "fan_one" } } ] } } ``` ## 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. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canManageDrops"}` | For a managed creator: the agency lacks `canManageDrops` on that account. | | 403 | `{"error":"This app does not have the drops API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"drops"}` | Your app has the Drops & checkout links [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/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/v1/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/v1/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: [List paid orders (per-fan references)](https://www.dropfans.io/developers/reference/list-orders.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/v1/orders — List paid orders (per-fan references) > Paid drop orders of the key's account, newest first — the polling twin of the [drop.paid webhook](/developers/webhooks/overview). - Source: https://www.dropfans.io/developers/reference/list-orders - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/orders` (the unversioned alias). Paid drop orders of the key's account, newest first — the polling twin of the [drop.paid webhook](https://www.dropfans.io/developers/webhooks/overview.md). Filter by `externalRef` to answer "did this fan pay?" on a shared drop, or by `productId` / `since` to sweep. Refunded and charged-back orders stay in the list with their `status` — act on `status: "paid"` only. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `GET /api/external/v1/creators/{creatorId}/orders` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canManageDrops`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## Query parameters | Field | Type | Required | Description | |---|---|---|---| | `productId` | string | | Only orders for this drop. | | `externalRef` | string | | Only orders bought through links carrying this reference. | | `since` | string | | Only orders paid at or after this time. | | `limit` | integer | | Page size. | | `cursor` | string | | `nextCursor` from the previous page. | ## Responses ### 200 A page of orders. | Field | Type | Required | Description | |---|---|---|---| | `orders` | Order[] | yes | Newest first. | | `nextCursor` | string \\| null | | Pass as `cursor` for the next page; null on the last page. | ```json { "orders": [ { "orderId": "clxor0001order", "productId": "clxdr0p000001prod", "accountId": "clx2f8a1b0001qw3k", "externalRef": "tg_48213377", "amountCents": 2750, "currency": "USD", "paidAt": "2026-10-17T16:20:00.000Z", "paidWith": "card", "status": "paid", "buyer": { "id": "clxbu0001buyer", "email": "buyer@example.com", "telegramUserId": "48213377", "telegramUsername": "fan_one" } } ], "nextCursor": null } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"externalRef must be 1–32 of A-Z a-z 0-9 _ -"}` | Malformed reference. | | 400 | `{"error":"since must be an ISO date"}` | Unparseable `since`. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canManageDrops"}` | For a managed creator: the agency lacks `canManageDrops` on that account. | | 403 | `{"error":"This app does not have the drops API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"drops"}` | Your app has the Drops & checkout links [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/orders?productId=clxdr0p000001prod&externalRef=tg_48213377" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/orders?productId=clxdr0p000001prod&externalRef=tg_48213377`, { 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/v1/orders", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, params={"productId": "clxdr0p000001prod", "externalRef": "tg_48213377"}, ) print(res.json()) ``` --- Previous: [Check which drops sold (batch)](https://www.dropfans.io/developers/reference/check-drop-status.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/v1/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 Also served at `/api/external/posts` (the unversioned alias). 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/v1/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 dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `POST /api/external/v1/creators/{creatorId}/posts` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canManageDrops`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## 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/v1/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/v1/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"). | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canManageDrops"}` | For a managed creator: the agency lacks `canManageDrops` on that account. | | 403 | `{"error":"This app does not have the posts API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"posts"}` | Your app has the For You posts [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/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/v1/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/v1/posts", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, json={ "caption": "im 5 min away, wyd?", }, ) print(res.json()) ``` --- Previous: [List paid orders (per-fan references)](https://www.dropfans.io/developers/reference/list-orders.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/v1/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 Also served at `/api/external/posts` (the unversioned alias). 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 dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `GET /api/external/v1/creators/{creatorId}/posts` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canManageDrops`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## 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. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canManageDrops"}` | For a managed creator: the agency lacks `canManageDrops` on that account. | | 403 | `{"error":"This app does not have the posts API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"posts"}` | Your app has the For You posts [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/posts" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/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/v1/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/v1/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 Also served at `/api/external/posts/{id}` (the unversioned alias). 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 dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `GET /api/external/v1/creators/{creatorId}/posts/{id}` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canManageDrops`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## 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. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canManageDrops"}` | For a managed creator: the agency lacks `canManageDrops` on that account. | | 403 | `{"error":"This app does not have the posts API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"posts"}` | Your app has the For You posts [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/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/v1/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/v1/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/v1/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 Also served at `/api/external/posts/{id}` (the unversioned alias). 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 dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `DELETE /api/external/v1/creators/{creatorId}/posts/{id}` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canManageDrops`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## 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). | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canManageDrops"}` | For a managed creator: the agency lacks `canManageDrops` on that account. | | 403 | `{"error":"This app does not have the posts API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"posts"}` | Your app has the For You posts [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/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/v1/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/v1/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/v1/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 Also served at `/api/external/earnings` (the unversioned alias). 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/v1/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. For the complete, paginated ledger with net / fee / VAT per transaction, use [GET /api/external/v1/earnings/transactions](https://www.dropfans.io/developers/reference/list-earnings-transactions.md). ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `GET /api/external/v1/creators/{creatorId}/earnings` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canViewStats`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## 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 the complete ledger use GET /api/external/v1/earnings/transactions. | ```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. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canViewStats"}` | For a managed creator: the agency lacks `canViewStats` on that account. | | 403 | `{"error":"This app does not have the earnings API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"earnings"}` | Your app has the Earnings & balance [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/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/v1/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/v1/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: [Earnings summary: totals, sources and a day / week series](https://www.dropfans.io/developers/reference/get-earnings-summary.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/v1/earnings/summary — Earnings summary: totals, sources and a day / week series > Totals for all time, this month and your window, a breakdown by source (drop, tip, subscription) and a zero-filled day or week series, in integer cents. - Source: https://www.dropfans.io/developers/reference/get-earnings-summary - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/earnings/summary` (the unversioned alias). Totals for all time, this month and your window, a breakdown by source (drop, tip, subscription) and a zero-filled day or week series, in integer cents. The same revenue as [GET /api/external/v1/earnings](https://www.dropfans.io/developers/reference/get-earnings.md): refunds and chargebacks are left out, so for the same window and timezone `totals.period` equals its `stats.totalEarningsCents` (net) and `stats.grossEarningsCents` (gross). Each transaction is rounded to cents once, so the `series` always adds up to `totals.period`. Days are whole local days in `timezone` (DST-aware: a switch day is 23 or 25 hours long). Weeks start on Monday; the first and last week are clipped to your window, and `startsAt` is when each bucket starts inside it. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `GET /api/external/v1/creators/{creatorId}/earnings/summary` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canViewStats`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## Query parameters | Field | Type | Required | Description | |---|---|---|---| | `startDate` | string | | First local day (YYYY-MM-DD). Defaults to 29 days before `endDate`. At most 366 days per window. | | `endDate` | string | | Last local day (YYYY-MM-DD), inclusive. Defaults to today in `timezone`. | | `granularity` | `day` \\| `week` | | Series bucket: a local day, or a Monday-start week. | | `timezone` | string | | IANA timezone for the days, weeks and "this month". `tz` is accepted too. Unlike GET /earnings, an unknown zone is a 400, not UTC. | ## Responses ### 200 Totals, breakdown and series. All money in cents. | Field | Type | Required | Description | |---|---|---|---| | `startDate` | string | yes | The window’s first day. | | `endDate` | string | yes | The window’s last day (inclusive). | | `timezone` | string | yes | The timezone used. | | `granularity` | `day` \\| `week` | yes | The series bucket. | | `currency` | string | yes | Always USD. | | `totals` | object | yes | `allTime`, `thisMonth` (the current month in `timezone`, up to today) and `period` (your window), each `{ grossCents, netCents, count }`. | | `totals.allTime` | object | yes | Since the account’s first sale. | | `totals.allTime.grossCents` | integer | yes | Buyer-paid minus VAT, before the platform fee. | | `totals.allTime.netCents` | integer | yes | The creator’s earnings after the platform fee. | | `totals.allTime.count` | integer | yes | Number of transactions. | | `totals.thisMonth` | object | yes | The current calendar month in `timezone`. | | `totals.thisMonth.grossCents` | integer | yes | Buyer-paid minus VAT, before the platform fee. | | `totals.thisMonth.netCents` | integer | yes | The creator’s earnings after the platform fee. | | `totals.thisMonth.count` | integer | yes | Number of transactions. | | `totals.period` | object | yes | Your window — equals the sum of `series`. | | `totals.period.grossCents` | integer | yes | Buyer-paid minus VAT, before the platform fee. | | `totals.period.netCents` | integer | yes | The creator’s earnings after the platform fee. | | `totals.period.count` | integer | yes | Number of transactions. | | `breakdownBySource` | object | yes | `drop`, `tip` and `subscription` for your window, each `{ grossCents, netCents, count }`. | | `breakdownBySource.drop` | object | yes | Drop sales. | | `breakdownBySource.drop.grossCents` | integer | yes | Buyer-paid minus VAT, before the platform fee. | | `breakdownBySource.drop.netCents` | integer | yes | The creator’s earnings after the platform fee. | | `breakdownBySource.drop.count` | integer | yes | Number of transactions. | | `breakdownBySource.tip` | object | yes | Tips. | | `breakdownBySource.tip.grossCents` | integer | yes | Buyer-paid minus VAT, before the platform fee. | | `breakdownBySource.tip.netCents` | integer | yes | The creator’s earnings after the platform fee. | | `breakdownBySource.tip.count` | integer | yes | Number of transactions. | | `breakdownBySource.subscription` | object | yes | Subscription payments (first charges and renewals). | | `breakdownBySource.subscription.grossCents` | integer | yes | Buyer-paid minus VAT, before the platform fee. | | `breakdownBySource.subscription.netCents` | integer | yes | The creator’s earnings after the platform fee. | | `breakdownBySource.subscription.count` | integer | yes | Number of transactions. | | `series` | object[] | yes | One point per day or week of the window, oldest first, zero-filled. | ```json { "startDate": "2026-10-26", "endDate": "2026-10-27", "timezone": "Europe/Stockholm", "granularity": "day", "currency": "USD", "totals": { "allTime": { "grossCents": 2480000, "netCents": 1984000, "count": 912 }, "thisMonth": { "grossCents": 214180, "netCents": 182050, "count": 41 }, "period": { "grossCents": 22300, "netCents": 18950, "count": 7 } }, "breakdownBySource": { "drop": { "grossCents": 15000, "netCents": 12750, "count": 4 }, "tip": { "grossCents": 5300, "netCents": 4500, "count": 2 }, "subscription": { "grossCents": 2000, "netCents": 1700, "count": 1 } }, "series": [ { "date": "2026-10-26", "startsAt": "2026-10-25T23:00:00.000Z", "grossCents": 12500, "netCents": 10620, "count": 4 }, { "date": "2026-10-27", "startsAt": "2026-10-26T23:00:00.000Z", "grossCents": 9800, "netCents": 8330, "count": 3 } ] } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"timezone must be an IANA timezone name, e.g. Europe/Stockholm","code":"invalid_timezone"}` | Unknown `timezone`. | | 400 | `{"error":"startDate and endDate must be YYYY-MM-DD dates","code":"invalid_date"}` | Malformed or impossible date (e.g. 2026-02-31). | | 400 | `{"error":"endDate must be on or after startDate","code":"invalid_date_range"}` | `startDate` is after `endDate`. | | 400 | `{"error":"Date range too large — at most 366 days.","code":"range_too_large"}` | The window is longer than 366 days. | | 400 | `{"error":"granularity must be day or week","code":"invalid_granularity"}` | Any other `granularity`. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 500 | `{"error":"Failed to fetch the earnings summary"}` | Query failed — retry later. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canViewStats"}` | For a managed creator: the agency lacks `canViewStats` on that account. | | 403 | `{"error":"This app does not have the earnings API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"earnings"}` | Your app has the Earnings & balance [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/earnings/summary?startDate=2026-10-01&endDate=2026-10-31&timezone=Europe%2FStockholm" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/earnings/summary?startDate=2026-10-01&endDate=2026-10-31&timezone=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/v1/earnings/summary", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, params={"startDate": "2026-10-01", "endDate": "2026-10-31", "timezone": "Europe/Stockholm"}, ) print(res.json()) ``` --- Previous: [Earnings stats, chart and recent transactions](https://www.dropfans.io/developers/reference/get-earnings.md) · Next: [Full transaction ledger (paginated, net per transaction)](https://www.dropfans.io/developers/reference/list-earnings-transactions.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/v1/earnings/transactions — Full transaction ledger (paginated, net per transaction) > Every drop sale, tip and subscription payment of the key's account, newest first, with net, gross, platform fee and VAT per transaction. No cap — follow `nextCursor` until it is null. - Source: https://www.dropfans.io/developers/reference/list-earnings-transactions - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/earnings/transactions` (the unversioned alias). Every drop sale, tip and subscription payment of the key's account, newest first, with net, gross, platform fee and VAT per transaction. No cap — follow `nextCursor` until it is null. Same sources and filters as [GET /api/external/v1/earnings](https://www.dropfans.io/developers/reference/get-earnings.md): refunds and chargebacks are left out, so the `netAmountCents` of a window's pages add up to its `stats.totalEarningsCents` (pass the same window as `since` / `until` instants). Pass `includeReversed=true` to also get refunded and charged-back transactions, with their `status`. Keep `since`, `until`, `type` and `includeReversed` the same while you follow a cursor. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `GET /api/external/v1/creators/{creatorId}/earnings/transactions` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canViewStats`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## Query parameters | Field | Type | Required | Description | |---|---|---|---| | `since` | string | | Only transactions paid at or after this time. | | `until` | string | | Only transactions paid at or before this time. | | `type` | string | | Comma-separated subset of drop,tip,subscription. Default: all three. | | `includeReversed` | boolean | | Also return refunded and charged-back transactions. | | `limit` | integer | | Page size. | | `cursor` | string | | `nextCursor` from the previous page. | ## Responses ### 200 A page of transactions. All money in cents. | Field | Type | Required | Description | |---|---|---|---| | `transactions` | LedgerTransaction[] | yes | Newest first. | | `nextCursor` | string \\| null | | Pass as `cursor` for the next page; null on the last page. | ```json { "transactions": [ { "id": "clx0rd3r000001sale", "type": "drop", "productId": "clxdr0p000001prod", "productName": "Beach set — 6 photos", "externalRef": "tg_48213377", "amountCents": 2000, "netAmountCents": 2000, "grossAmountCents": 2500, "platformFeeCents": 500, "taxCents": 250, "totalChargedCents": 2750, "currency": "USD", "buyerEmail": "buyer@example.com", "buyerName": null, "paidAt": "2026-10-18T21:03:00.000Z", "status": "paid" } ], "nextCursor": "MjAyNi0xMC0xOFQyMTowMzowMC4wMDBafGNseDByZDNyMDAwMDAxc2FsZQ" } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"since must be an ISO date"}` | Unparseable `since` (or `until`). | | 400 | `{"error":"type must be a comma-separated subset of drop,tip,subscription"}` | Unknown type. | | 400 | `{"error":"Invalid cursor"}` | The cursor was not one we returned. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canViewStats"}` | For a managed creator: the agency lacks `canViewStats` on that account. | | 403 | `{"error":"This app does not have the earnings API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"earnings"}` | Your app has the Earnings & balance [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/earnings/transactions?since=2026-10-01T00%3A00%3A00Z&until=2026-10-31T23%3A59%3A59.999Z&type=drop%2Ctip" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/earnings/transactions?since=2026-10-01T00%3A00%3A00Z&until=2026-10-31T23%3A59%3A59.999Z&type=drop%2Ctip`, { 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/v1/earnings/transactions", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, params={"since": "2026-10-01T00:00:00Z", "until": "2026-10-31T23:59:59.999Z", "type": "drop,tip"}, ) print(res.json()) ``` --- Previous: [Earnings summary: totals, sources and a day / week series](https://www.dropfans.io/developers/reference/get-earnings-summary.md) · Next: [List paid tips (per-fan references)](https://www.dropfans.io/developers/reference/list-tips.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/v1/tips — List paid tips (per-fan references) > Paid tips of the key's account, newest first — the polling twin of the [tip.paid webhook](/developers/webhooks/overview). - Source: https://www.dropfans.io/developers/reference/list-tips - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/tips` (the unversioned alias). Paid tips of the key's account, newest first — the polling twin of the [tip.paid webhook](https://www.dropfans.io/developers/webhooks/overview.md). Filter by `externalRef` to answer "did this fan tip?" for a tip link you sent, or by `since` to sweep. Each tip carries the fan's `message`. Refunded and charged-back tips stay in the list with their `status` — act on `status: "paid"` only. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `GET /api/external/v1/creators/{creatorId}/tips` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canViewStats`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## Query parameters | Field | Type | Required | Description | |---|---|---|---| | `externalRef` | string | | Only tips made through links carrying this reference. | | `since` | string | | Only tips paid at or after this time. | | `limit` | integer | | Page size. | | `cursor` | string | | `nextCursor` from the previous page. | ## Responses ### 200 A page of tips. | Field | Type | Required | Description | |---|---|---|---| | `tips` | Tip[] | yes | Newest first. | | `nextCursor` | string \\| null | | Pass as `cursor` for the next page; null on the last page. | ```json { "tips": [ { "tipId": "clxtp0001tip", "accountId": "clx2f8a1b0001qw3k", "externalRef": "fan-48213377", "amountCents": 1100, "currency": "USD", "message": "For the new set 💕", "paidAt": "2026-10-23T16:20:00.000Z", "paidWith": "card", "status": "paid", "tipper": { "id": "clxbu0001buyer", "email": "fan@example.com", "telegramUserId": "48213377", "telegramUsername": "fan_one" } } ], "nextCursor": null } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"externalRef must be 1–32 of A-Z a-z 0-9 _ -"}` | Malformed reference. | | 400 | `{"error":"since must be an ISO date"}` | Unparseable `since`. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canViewStats"}` | For a managed creator: the agency lacks `canViewStats` on that account. | | 403 | `{"error":"This app does not have the earnings API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"earnings"}` | Your app has the Earnings & balance [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/tips?externalRef=fan-48213377" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/tips?externalRef=fan-48213377`, { 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/v1/tips", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, params={"externalRef": "fan-48213377"}, ) print(res.json()) ``` --- Previous: [Full transaction ledger (paginated, net per transaction)](https://www.dropfans.io/developers/reference/list-earnings-transactions.md) · Next: [The creator’s profile, tip and subscribe links](https://www.dropfans.io/developers/reference/get-links.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/v1/links — The creator’s profile, tip and subscribe links > The creator's shareable links in one call. Never build Dropfans URLs by hand. - Source: https://www.dropfans.io/developers/reference/get-links - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/links` (the unversioned alias). The creator's shareable links in one call. Never build Dropfans URLs by hand. **Link ids (`links`).** Every link is just `https://www.dropfans.io/buy/{id}` on the web and `https://t.me//app?startapp={id}` in Telegram. There are no parameters on the link. What it opens, the account and your app are stored with the id. `links.profile`, `links.tip` (the fan picks the amount) and `links.subscribe` (opens the subscribe sheet) are created on the first call and returned unchanged after that. For a drop, a fixed tip amount or a per-fan reference, create a link with [POST /api/external/v1/links](https://www.dropfans.io/developers/reference/create-link.md). **Commission.** With a key that belongs to an app, a drop bought, a tip sent or a subscription started through your app's links earns your app owner commission **for life**, including every renewal of that subscription, at your app's commission rate. Every call also makes your app the account's connected CRM. **Templates (`web`, `telegram`), deprecated.** These are unchanged and keep working. Fields ending in `Template` have placeholders to fill in: `{usd}` (whole dollars, web tip), `{cents}` (Telegram tip), `{productId}` and `{externalRef}`. A sale through a template link keeps its `externalRef`. One with an `externalRef` counts for a CRM only if it is the only app that has ever generated links for the account, its own key works the account (directly or through its agency), and the account has no personal key. Use link ids for anything new. The `telegram` block is null when the shared Dropfans bot is not configured, and so is each link's `telegram` URL. Web tip prefill: `?tip=` between $5 and $750; other amounts open the sheet unfilled. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `GET /api/external/v1/creators/{creatorId}/links` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators). The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## Responses ### 200 The creator’s links. | Field | Type | Required | Description | |---|---|---|---| | `username` | string | yes | The creator’s @handle the links are built for. | | `links` | object | yes | Link ids for the account — the same ids on every call. Create buy links and fixed-amount tip links with POST /api/external/v1/links. | | `links.profile` | Link | yes | Opens the profile. | | `links.tip` | Link | yes | Opens the tip sheet; the fan picks the amount. | | `links.subscribe` | Link | yes | Opens the subscribe sheet. | | `web` | object | yes | Deprecated, still served: web links built from templates with literal placeholders. Prefer `links` / POST /api/external/v1/links. | | `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.tipWithRefTemplate` | string | yes | Tip link with a per-fan reference — replace `{usd}` (use `1` to let the fan pick the amount) and `{externalRef}` (1–32 of `A-Z a-z 0-9 _ -`). It comes back on the tip (tip.paid webhook, GET /api/external/v1/tips). | | `web.subscribe` | string | yes | Opens the profile with the subscribe sheet already on screen. | | `web.buyTemplate` | string | yes | Checkout link for any drop — replace `{productId}` with the id from create-drop. | | `web.buyWithRefTemplate` | string | yes | The same with a per-fan reference — replace `{externalRef}` (1–32 of `A-Z a-z 0-9 _ -`). It comes back on the order (drop.paid webhook, GET /api/external/v1/orders). | | `telegram` | object \\| null | | Deprecated, still served: Telegram Mini App deep links built from templates. **Null when the shared Dropfans bot is not configured.** Prefer `links` / POST /api/external/v1/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.tipWithRefTemplate` | string | yes | Mini App tip with a per-fan reference — replace `{cents}` (`0` lets the fan pick the amount) and `{externalRef}`. **Here the reference is 1–20 of `A-Z a-z 0-9 -` (no underscore)**: it sits in front of the username, which may contain underscores, and Telegram caps the start parameter at 64 characters. | | `telegram.subscribe` | string | yes | Opens the Mini App subscribe flow. | | `telegram.buyTemplate` | string | yes | Mini App checkout for any drop — replace `{productId}`. | | `telegram.buyWithRefTemplate` | string | yes | Mini App checkout with a per-fan reference — replace `{productId}` and `{externalRef}` (1–32 of `A-Z a-z 0-9 _ -`). | ```json { "username": "valeria", "links": { "profile": { "id": "k3x9m2qa7d", "type": "profile", "url": "https://www.dropfans.io/buy/k3x9m2qa7d", "telegram": "https://t.me/DropfansBot/app?startapp=k3x9m2qa7d", "productId": null, "amountUsd": null, "externalRef": null, "createdAt": "2026-10-29T12:00:00.000Z" }, "tip": { "id": "p0w8n4rt2c", "type": "tip", "url": "https://www.dropfans.io/buy/p0w8n4rt2c", "telegram": "https://t.me/DropfansBot/app?startapp=p0w8n4rt2c", "productId": null, "amountUsd": null, "externalRef": null, "createdAt": "2026-10-29T12:00:00.000Z" }, "subscribe": { "id": "h5j1v9xs3e", "type": "subscribe", "url": "https://www.dropfans.io/buy/h5j1v9xs3e", "telegram": "https://t.me/DropfansBot/app?startapp=h5j1v9xs3e", "productId": null, "amountUsd": null, "externalRef": null, "createdAt": "2026-10-29T12:00:00.000Z" } }, "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}", "tipWithRefTemplate": "https://www.dropfans.io/u/valeria?tip={usd}&m={externalRef}", "subscribe": "https://www.dropfans.io/u/valeria?subscribe=1", "buyTemplate": "https://www.dropfans.io/buy/{productId}", "buyWithRefTemplate": "https://www.dropfans.io/buy/{productId}?m={externalRef}" }, "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", "tipWithRefTemplate": "https://t.me/DropfansBot/app?startapp=tm_{cents}_{externalRef}_valeria", "subscribe": "https://t.me/DropfansBot/app?startapp=s_valeria", "buyTemplate": "https://t.me/DropfansBot/app?startapp=b_{productId}", "buyWithRefTemplate": "https://t.me/DropfansBot/app?startapp=b_{productId}_{externalRef}" } } ``` ## 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. Have the creator pick a handle in their Dropfans settings, then retry. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"This app does not have the links API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"links"}` | Your app has the Links [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/links" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/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/v1/links", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [List paid tips (per-fan references)](https://www.dropfans.io/developers/reference/list-tips.md) · Next: [Create a link for a drop, a tip, a subscription or the profile](https://www.dropfans.io/developers/reference/create-link.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # POST /api/external/v1/links — Create a link for a drop, a tip, a subscription or the profile > Creates one link id. The link is `https://www.dropfans.io/buy/{id}` (web) and `https://t.me//app?startapp={id}` (Telegram), with no parameters. The drop, the tip amount and your `externalRef` are stored with the id. - Source: https://www.dropfans.io/developers/reference/create-link - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/links` (the unversioned alias). Creates one link id. The link is `https://www.dropfans.io/buy/{id}` (web) and `https://t.me//app?startapp={id}` (Telegram), with no parameters. The drop, the tip amount and your `externalRef` are stored with the id. - `buy`: a drop (`productId` from create-drop) opens its checkout. A subscriber-only drop opens the subscribe sheet instead. - `tip`: opens the tip sheet, prefilled with `amountUsd` when you set one ($5–$750), or the fan picks. - `subscribe`: opens the subscribe sheet. `profile` opens the profile. `externalRef` (1–32 of `A-Z a-z 0-9 _ -`) comes back on the sale: the `drop.paid` / `tip.paid` webhooks and GET /orders and /tips, together with the link's `linkId`. One link per fan per drop is the usual pattern. The same request always returns the same link, so it's safe to call again. With a key that belongs to an app, sales through the link earn your app owner commission for life, and the call makes your app the account's connected CRM. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `POST /api/external/v1/creators/{creatorId}/links` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators). The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## Request body (application/json) | Field | Type | Required | Description | |---|---|---|---| | `type` | `buy` \\| `tip` \\| `subscribe` \\| `profile` | yes | What the link opens. | | `productId` | string | | The drop, for `buy` (required there, refused otherwise). Must belong to the account. | | `amountUsd` | number | | Fixed tip amount for `tip` ($5–$750). Omit to let the fan pick. | | `externalRef` | string | | Your per-fan reference, returned on the sale. | Example — One drop, one fan: ```json { "type": "buy", "productId": "cmv5tmpcm0015h4oucxkgc4ma", "externalRef": "tg_48213377" } ``` Example — A $20 tip link: ```json { "type": "tip", "amountUsd": 20, "externalRef": "tg_48213377" } ``` ## Responses ### 200 The link (new, or the existing one for this exact request). | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The link id (10 of `a-z 0-9`). Comes back as `linkId` on sales through the link. | | `type` | `buy` \\| `tip` \\| `subscribe` \\| `profile` | yes | What the link opens. | | `url` | string | yes | The web link — `/buy/{id}`, no parameters. | | `telegram` | string \\| null | | The Telegram Mini App link (`startapp={id}`). Null when the shared Dropfans bot is not configured. | | `productId` | string \\| null | | The drop of a `buy` link. | | `amountUsd` | number \\| null | | Fixed amount of a `tip` link; null = the fan picks. | | `externalRef` | string \\| null | | Your per-fan reference, returned on the sale. | | `createdAt` | string | yes | When the link was created. | ```json { "id": "q7m2c8zk4w", "type": "buy", "url": "https://www.dropfans.io/buy/q7m2c8zk4w", "telegram": "https://t.me/DropfansBot/app?startapp=q7m2c8zk4w", "productId": "cmv5tmpcm0015h4oucxkgc4ma", "amountUsd": null, "externalRef": "tg_48213377", "createdAt": "2026-10-29T12:00:00.000Z" } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"type must be one of profile, tip, subscribe, buy","code":"invalid_type"}` | Unknown `type`. Also `invalid_external_ref`, `invalid_amount` (out of range, or not a tip) and `invalid_product` (productId on a non-buy link). | | 404 | `{"error":"Drop not found","code":"product_not_found"}` | `productId` is missing, unknown or another account’s. | | 409 | `{"error":"Set a username first","code":"username_required"}` | The creator has no username yet. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"This app does not have the links API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"links"}` | Your app has the Links [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/links" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "buy", "productId": "cmv5tmpcm0015h4oucxkgc4ma", "externalRef": "tg_48213377" }' ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/links`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ "type": "buy", "productId": "cmv5tmpcm0015h4oucxkgc4ma", "externalRef": "tg_48213377" }), }); console.log(await res.json()); ``` ### Python ```python import os import requests res = requests.post( "https://www.dropfans.io/api/external/v1/links", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, json={ "type": "buy", "productId": "cmv5tmpcm0015h4oucxkgc4ma", "externalRef": "tg_48213377", }, ) print(res.json()) ``` --- Previous: [The creator’s profile, tip and subscribe links](https://www.dropfans.io/developers/reference/get-links.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/v1/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 Also served at `/api/external/notifications` (the unversioned alias). 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 dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `GET /api/external/v1/creators/{creatorId}/notifications` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canEditProfile`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## 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. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canEditProfile"}` | For a managed creator: the agency lacks `canEditProfile` on that account. | | 403 | `{"error":"This app does not have the notifications API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"notifications"}` | Your app has the Telegram notifications [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/notifications" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/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/v1/notifications", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [Create a link for a drop, a tip, a subscription or the profile](https://www.dropfans.io/developers/reference/create-link.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/v1/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 Also served at `/api/external/notifications` (the unversioned alias). 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 dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `PUT /api/external/v1/creators/{creatorId}/notifications` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canEditProfile`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## 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. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canEditProfile"}` | For a managed creator: the agency lacks `canEditProfile` on that account. | | 403 | `{"error":"This app does not have the notifications API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"notifications"}` | Your app has the Telegram notifications [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/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/v1/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/v1/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/v1/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 Also served at `/api/external/register-telegram-chat` (the unversioned alias). 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 dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `POST /api/external/v1/creators/{creatorId}/register-telegram-chat` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canEditProfile`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## 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. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canEditProfile"}` | For a managed creator: the agency lacks `canEditProfile` on that account. | | 403 | `{"error":"This app does not have the notifications API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"notifications"}` | Your app has the Telegram notifications [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/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/v1/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/v1/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: [Create a creator account (sign-up from your page)](https://www.dropfans.io/developers/reference/create-account.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # POST /api/external/v1/accounts — Create a creator account (sign-up from your page) > Creates a creator (or agency) account from your own sign-up page and returns the ID-check link to redirect the creator to. - Source: https://www.dropfans.io/developers/reference/create-account - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/accounts` (the unversioned alias). Creates a creator (or agency) account from your own sign-up page and returns the ID-check link to redirect the creator to. The account is created with everything our own onboarding sets — handle, display name, bio, residency, avatar, banner and subscription price — and starts as `pending_kyc`. Send the creator to `kycUrl` (a full-page redirect or a new tab, never an iframe). When they finish, they come back to your `kycReturnUrl` with `?kyc=approved|declined|in_review`. That parameter is only a hint for your UI: the decision arrives as the [account.kyc_updated webhook](https://www.dropfans.io/developers/webhooks/overview.md), and selling is unlocked only once the account is `active`. The response also holds an `apiKey` for the new account, bound to your app. Use it straight away to upload media and create drops for the creator. Store it like a password; it is shown once. Your app owner’s own key can also act for the account under [/api/external/v1/creators/{accountId}/…](https://www.dropfans.io/developers/concepts/agencies.md) for as long as the account keeps that key. **Commission.** Sales made through the links you get from GET /api/external/v1/links for the account (a drop bought, a tip, a subscription and all its renewals) earn your app owner a commission for life — 5% unless agreed otherwise, paid out of our platform fee, never taken from the creator. **Terms.** Show the creator our Terms of Service, Privacy Policy and 2257 statement and record their acceptance. Send `legalAccepted: true` with the IP address and user agent of **the creator’s browser** at the moment they accepted (not your server’s). We generate the signed acceptance record from them. > [!NOTE] Owner-held key + the Onboarding category > Call this with a key your app owner holds: their own, or one their agency minted for its personas or managed creators. Creator sign-up is the Onboarding [API category](https://www.dropfans.io/developers/concepts/api-categories.md), off for new apps; ask us to switch it on (403 `api_category_disabled` until then). ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Request body (application/json) | Field | Type | Required | Description | |---|---|---|---| | `email` | string | yes | The creator’s email. It becomes their sign-in. 409 if it is already registered. | | `password` | string | | Optional. Leave it out and we email the creator a link to set one (valid 7 days). Either way they get a welcome email. | | `username` | string | yes | Public @handle (letters, digits, underscores). 409 when taken. | | `displayName` | string | | Display name on the profile. | | `bio` | string | | Profile bio. | | `location` | string | | Optional profile location line. | | `accountType` | `creator` \\| `agency` | | `agency` is for AI-creator operators that run several personas. | | `residencyCountry` | string | yes | Country of residence, ISO 3166-1 alpha-2. Used for payouts and tax. | | `avatarUrl` | string | | https URL of the profile picture (JPEG, PNG or WebP, ≤4 MB). Fetched once, stripped of metadata and moderated like a normal upload. A bad image never fails the sign-up; check `images.avatar`. | | `bannerUrl` | string | | https URL of the profile banner. Same rules as avatarUrl. | | `subscription` | object | | Optional subscription offer for the profile. | | `subscription.priceUsd` | number | yes | Price in USD dollars, $5–$750. | | `subscription.interval` | `monthly` \\| `weekly` | | Billing period. | | `subscription.trialDays` | integer | | Free trial length. | | `legalAccepted` | boolean | yes | Must be `true`: the creator accepted the Terms of Service, Privacy Policy and 2257 statement on your page. | | `acceptedFromIp` | string | yes | The creator’s browser IP at acceptance. | | `acceptedUserAgent` | string | yes | The creator’s browser user agent at acceptance. | | `kycReturnUrl` | string | | https URL the creator returns to after the ID check. We append `?kyc=`. | Example — A creator with profile and subscription: ```json { "email": "valeria@example.com", "username": "valeria", "displayName": "Valeria", "bio": "New sets every Friday.", "residencyCountry": "SE", "avatarUrl": "https://cdn.example.com/valeria/avatar.jpg", "bannerUrl": "https://cdn.example.com/valeria/banner.jpg", "subscription": { "priceUsd": 9.99, "interval": "monthly", "trialDays": 0 }, "legalAccepted": true, "acceptedFromIp": "203.0.113.7", "acceptedUserAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) …", "kycReturnUrl": "https://crm.example.com/onboarding/done" } ``` ## Responses ### 201 Created. Redirect the creator to `kycUrl`; keep `accountId` and `apiKey`. | Field | Type | Required | Description | |---|---|---|---| | `accountId` | string | yes | The new account’s id. | | `username` | string | yes | Public @handle (without @). | | `displayName` | string \\| null | | Display name. | | `email` | string \\| null | | Sign-in email. | | `accountType` | `creator` \\| `agency` | yes | What the account was created as. | | `status` | `pending_kyc` \\| `active` | yes | `pending_kyc` until the ID check is approved, then `active` (can sell and get paid). | | `sellerStatus` | string | yes | Internal seller state, for support (PENDING_KYC, KYC_IN_REVIEW, KYC_REJECTED, VERIFIED_CREATOR, VERIFIED_AGENCY). | | `kycStatus` | `not_started` \\| `session_created` \\| `in_review` \\| `processing` \\| `approved` \\| `rejected` | yes | ID-check state. `session_created` = link opened, not finished; `processing` = approved, compliance documents still being generated. | | `profileUrl` | string | yes | Public profile URL. | | `createdAt` | string | yes | When the account was created. | | `apiKey` | string | yes | A key for the new account, bound to your app. Shown once. | | `kycUrl` | string \\| null | | The ID-check link. Null if it could not be opened right now; then call create-kyc-session. | | `kycExpiresAt` | string \\| null | | The link is reused until then; ask for a new one after. | | `referralCredited` | boolean | yes | True when the account was attributed to your app owner. Commission does not depend on it: it is paid on sales through your app’s links. | | `images` | object | yes | What happened to avatarUrl / bannerUrl: "saved", "failed" or null (not sent). | | `images.avatar` | `saved` \\| `failed` \\| null | | Avatar result. | | `images.banner` | `saved` \\| `failed` \\| null | | Banner result. | ```json { "accountId": "clxac0001acct", "username": "valeria", "displayName": "Valeria", "email": "valeria@example.com", "accountType": "creator", "status": "pending_kyc", "sellerStatus": "PENDING_KYC", "kycStatus": "session_created", "profileUrl": "https://www.dropfans.io/u/valeria", "createdAt": "2026-10-17T16:00:00.000Z", "apiKey": "dpfn_3f9c…", "kycUrl": "https://verify.didit.me/session/…", "kycExpiresAt": "2026-10-18T16:00:00.000Z", "referralCredited": true, "images": { "avatar": "saved", "banner": "saved" } } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"residencyCountry must be an ISO 3166-1 alpha-2 code (e.g. \"SE\").","code":"invalid_input","field":"residencyCountry"}` | A field failed validation. `field` names it. | | 409 | `{"error":"There is already a DropFans account with this email. Sign in instead.","code":"email_taken"}` | The email is registered. | | 409 | `{"error":"That profile link is taken. Pick another.","code":"username_taken"}` | The handle is taken. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"This endpoint needs a key bound to an approved app.","code":"app_key_required"}` | A Personal key was used. | | 403 | `{"error":"Use the app owner's key for this endpoint.","code":"owner_key_required"}` | The key was minted by a creator for themselves. App-level calls need a key held by your app owner: their own, or one their agency minted for its personas or managed creators. | | 429 | `{"error":"At most 20 sign-ups per hour.","code":"rate_limited"}` | Too many sign-ups from your app this hour. `Retry-After` says when to retry. | | 400 | `{"error":"This endpoint acts as the key’s own account or app — call it without /creators/{creatorId}.","code":"creator_scope_not_allowed"}` | Called under /api/external/v1/creators/{creatorId}/ — this endpoint always acts as the key’s own account, agency or app. | | 403 | `{"error":"This app does not have the onboarding API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"onboarding"}` | Your app has the Creator onboarding [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off (the default for new apps). | ## 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/v1/accounts" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "email": "valeria@example.com", "username": "valeria", "displayName": "Valeria", "bio": "New sets every Friday.", "residencyCountry": "SE", "avatarUrl": "https://cdn.example.com/valeria/avatar.jpg", "bannerUrl": "https://cdn.example.com/valeria/banner.jpg", "subscription": { "priceUsd": 9.99, "interval": "monthly", "trialDays": 0 }, "legalAccepted": true, "acceptedFromIp": "203.0.113.7", "acceptedUserAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) …", "kycReturnUrl": "https://crm.example.com/onboarding/done" }' ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/accounts`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ "email": "valeria@example.com", "username": "valeria", "displayName": "Valeria", "bio": "New sets every Friday.", "residencyCountry": "SE", "avatarUrl": "https://cdn.example.com/valeria/avatar.jpg", "bannerUrl": "https://cdn.example.com/valeria/banner.jpg", "subscription": { "priceUsd": 9.99, "interval": "monthly", "trialDays": 0 }, "legalAccepted": true, "acceptedFromIp": "203.0.113.7", "acceptedUserAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) …", "kycReturnUrl": "https://crm.example.com/onboarding/done" }), }); console.log(await res.json()); ``` ### Python ```python import os import requests res = requests.post( "https://www.dropfans.io/api/external/v1/accounts", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, json={ "email": "valeria@example.com", "username": "valeria", "displayName": "Valeria", "bio": "New sets every Friday.", "residencyCountry": "SE", "avatarUrl": "https://cdn.example.com/valeria/avatar.jpg", "bannerUrl": "https://cdn.example.com/valeria/banner.jpg", "subscription": { "priceUsd": 9.99, "interval": "monthly", "trialDays": 0, }, "legalAccepted": True, "acceptedFromIp": "203.0.113.7", "acceptedUserAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 18_0 like Mac OS X) …", "kycReturnUrl": "https://crm.example.com/onboarding/done", }, ) print(res.json()) ``` --- Previous: [Register the creator’s personal notification chat](https://www.dropfans.io/developers/reference/register-telegram-chat.md) · Next: [List accounts you created](https://www.dropfans.io/developers/reference/list-accounts.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/v1/accounts — List accounts you created > Accounts your app created through create-account, newest first, with their current status. - Source: https://www.dropfans.io/developers/reference/list-accounts - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/accounts` (the unversioned alias). Accounts your app created through create-account, newest first, with their current status. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Query parameters | Field | Type | Required | Description | |---|---|---|---| | `limit` | integer | | Page size. | | `cursor` | string | | `nextCursor` from the previous page. | ## Responses ### 200 A page of accounts. | Field | Type | Required | Description | |---|---|---|---| | `accounts` | CrmAccount[] | yes | Newest first. | | `nextCursor` | string \\| null | | Null on the last page. | ```json { "accounts": [ { "accountId": "clxac0001acct", "username": "valeria", "displayName": "Valeria", "email": "valeria@example.com", "accountType": "creator", "status": "pending_kyc", "sellerStatus": "PENDING_KYC", "kycStatus": "session_created", "profileUrl": "https://www.dropfans.io/u/valeria", "createdAt": "2026-10-17T16:00:00.000Z" } ], "nextCursor": null } ``` ## Errors | Status | Body | When | |---|---|---| | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"This endpoint needs a key bound to an approved app.","code":"app_key_required"}` | A Personal key was used. | | 403 | `{"error":"Use the app owner's key for this endpoint.","code":"owner_key_required"}` | The key was minted by a creator for themselves. App-level calls need a key held by your app owner: their own, or one their agency minted for its personas or managed creators. | | 400 | `{"error":"This endpoint acts as the key’s own account or app — call it without /creators/{creatorId}.","code":"creator_scope_not_allowed"}` | Called under /api/external/v1/creators/{creatorId}/ — this endpoint always acts as the key’s own account, agency or app. | | 403 | `{"error":"This app does not have the onboarding API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"onboarding"}` | Your app has the Creator onboarding [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off (the default for new apps). | ## 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/v1/accounts" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/accounts`, { 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/v1/accounts", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [Create a creator account (sign-up from your page)](https://www.dropfans.io/developers/reference/create-account.md) · Next: [Get an account you created](https://www.dropfans.io/developers/reference/get-account.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/v1/accounts/{id} — Get an account you created > Status of one account your app created — `pending_kyc` until the ID check is approved, then `active`. - Source: https://www.dropfans.io/developers/reference/get-account - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/accounts/{id}` (the unversioned alias). Status of one account your app created — `pending_kyc` until the ID check is approved, then `active`. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Path parameters | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The accountId from create-account. | ## Responses ### 200 The account. | Field | Type | Required | Description | |---|---|---|---| | `accountId` | string | yes | The new account’s id. | | `username` | string | yes | Public @handle (without @). | | `displayName` | string \\| null | | Display name. | | `email` | string \\| null | | Sign-in email. | | `accountType` | `creator` \\| `agency` | yes | What the account was created as. | | `status` | `pending_kyc` \\| `active` | yes | `pending_kyc` until the ID check is approved, then `active` (can sell and get paid). | | `sellerStatus` | string | yes | Internal seller state, for support (PENDING_KYC, KYC_IN_REVIEW, KYC_REJECTED, VERIFIED_CREATOR, VERIFIED_AGENCY). | | `kycStatus` | `not_started` \\| `session_created` \\| `in_review` \\| `processing` \\| `approved` \\| `rejected` | yes | ID-check state. `session_created` = link opened, not finished; `processing` = approved, compliance documents still being generated. | | `profileUrl` | string | yes | Public profile URL. | | `createdAt` | string | yes | When the account was created. | ```json { "accountId": "clxac0001acct", "username": "valeria", "displayName": "Valeria", "email": "valeria@example.com", "accountType": "creator", "status": "pending_kyc", "sellerStatus": "PENDING_KYC", "kycStatus": "session_created", "profileUrl": "https://www.dropfans.io/u/valeria", "createdAt": "2026-10-17T16:00:00.000Z" } ``` ## Errors | Status | Body | When | |---|---|---| | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"This endpoint needs a key bound to an approved app.","code":"app_key_required"}` | A Personal key was used. | | 403 | `{"error":"Use the app owner's key for this endpoint.","code":"owner_key_required"}` | The key was minted by a creator for themselves. App-level calls need a key held by your app owner: their own, or one their agency minted for its personas or managed creators. | | 404 | `{"error":"Account not found"}` | No such account, or another app created it. | | 400 | `{"error":"This endpoint acts as the key’s own account or app — call it without /creators/{creatorId}.","code":"creator_scope_not_allowed"}` | Called under /api/external/v1/creators/{creatorId}/ — this endpoint always acts as the key’s own account, agency or app. | | 403 | `{"error":"This app does not have the onboarding API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"onboarding"}` | Your app has the Creator onboarding [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off (the default for new apps). | ## 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/v1/accounts/$ID" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const id = '…'; // from an earlier response const res = await fetch(`https://www.dropfans.io/api/external/v1/accounts/${id}`, { headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, }, }); console.log(await res.json()); ``` ### Python ```python import os import requests id = "…" # from an earlier response res = requests.get( f"https://www.dropfans.io/api/external/v1/accounts/{id}", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [List accounts you created](https://www.dropfans.io/developers/reference/list-accounts.md) · Next: [Get a fresh ID-check link](https://www.dropfans.io/developers/reference/create-kyc-session.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # POST /api/external/v1/accounts/{id}/kyc-session — Get a fresh ID-check link > The ID-check link for an account you created — when the first one expired, the creator closed it, or the check was declined and they may try again. - Source: https://www.dropfans.io/developers/reference/create-kyc-session - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/accounts/{id}/kyc-session` (the unversioned alias). The ID-check link for an account you created — when the first one expired, the creator closed it, or the check was declined and they may try again. A link younger than 24 hours is reused unless you pass a new `kycReturnUrl`. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Path parameters | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The accountId. | ## Request body (application/json) Optional. | Field | Type | Required | Description | |---|---|---|---| | `kycReturnUrl` | string | | https URL to return to; we append `?kyc=`. | | `acceptedFromIp` | string | | The creator’s browser IP, when you have it. | | `acceptedUserAgent` | string | | The creator’s browser user agent. | Example — New link with a return URL: ```json { "kycReturnUrl": "https://crm.example.com/onboarding/done" } ``` ## Responses ### 200 The link. | Field | Type | Required | Description | |---|---|---|---| | `kycUrl` | string | yes | Redirect the creator here. | | `kycExpiresAt` | string | yes | Reused until then. | | `reused` | boolean | yes | True when an open session was returned. | ```json { "kycUrl": "https://verify.didit.me/session/…", "kycExpiresAt": "2026-10-18T16:00:00.000Z", "reused": false } ``` ## Errors | Status | Body | When | |---|---|---| | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"This endpoint needs a key bound to an approved app.","code":"app_key_required"}` | A Personal key was used. | | 403 | `{"error":"Use the app owner's key for this endpoint.","code":"owner_key_required"}` | The key was minted by a creator for themselves. App-level calls need a key held by your app owner: their own, or one their agency minted for its personas or managed creators. | | 404 | `{"error":"Account not found"}` | No such account, or another app created it. | | 409 | `{"error":"The ID check is already done.","code":"already_verified"}` | Approved (or being finalised) — nothing to do. | | 503 | `{"error":"The ID check is unavailable right now. Try again later.","code":"kyc_unavailable"}` | The verification provider is down. | | 400 | `{"error":"This endpoint acts as the key’s own account or app — call it without /creators/{creatorId}.","code":"creator_scope_not_allowed"}` | Called under /api/external/v1/creators/{creatorId}/ — this endpoint always acts as the key’s own account, agency or app. | | 403 | `{"error":"This app does not have the onboarding API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"onboarding"}` | Your app has the Creator onboarding [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off (the default for new apps). | ## 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/v1/accounts/$ID/kyc-session" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "kycReturnUrl": "https://crm.example.com/onboarding/done" }' ``` ### Node ```javascript const id = '…'; // from an earlier response const res = await fetch(`https://www.dropfans.io/api/external/v1/accounts/${id}/kyc-session`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ "kycReturnUrl": "https://crm.example.com/onboarding/done" }), }); console.log(await res.json()); ``` ### Python ```python import os import requests id = "…" # from an earlier response res = requests.post( f"https://www.dropfans.io/api/external/v1/accounts/{id}/kyc-session", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, json={ "kycReturnUrl": "https://crm.example.com/onboarding/done", }, ) print(res.json()) ``` --- Previous: [Get an account you created](https://www.dropfans.io/developers/reference/get-account.md) · Next: [Get your webhook endpoint](https://www.dropfans.io/developers/reference/get-webhook.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/v1/webhooks — Get your webhook endpoint > Your app’s webhook endpoint, its events and the last 20 deliveries (with status codes and errors) — the first place to look when an event seems missing. - Source: https://www.dropfans.io/developers/reference/get-webhook - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/webhooks` (the unversioned alias). Your app’s webhook endpoint, its events and the last 20 deliveries (with status codes and errors) — the first place to look when an event seems missing. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Responses ### 200 The configuration. | Field | Type | Required | Description | |---|---|---|---| | `url` | string \\| null | | Your https endpoint, or null when none is set. | | `events` | string[] | yes | Events delivered to the endpoint. | | `hasSecret` | boolean | yes | Whether a signing secret exists. | | `availableEvents` | string[] | yes | Every event you can subscribe to. | | `recentDeliveries` | object[] | yes | The last 20 deliveries, newest first. | ```json { "url": "https://crm.example.com/hooks/dropfans", "events": [ "drop.paid", "drop.refunded", "account.kyc_updated" ], "hasSecret": true, "availableEvents": [ "drop.paid", "drop.refunded", "tip.paid", "tip.refunded", "subscription.paid", "subscription.canceled", "subscription.ended", "account.kyc_updated", "payout.requested", "payout.processing", "payout.paid", "payout.failed" ], "recentDeliveries": [ { "id": "evt_2b7c…", "event": "drop.paid", "status": "DELIVERED", "attempts": 1, "lastStatusCode": 200, "lastError": null, "createdAt": "2026-10-17T16:20:01.000Z", "deliveredAt": "2026-10-17T16:20:01.000Z", "nextAttemptAt": null } ] } ``` ## Errors | Status | Body | When | |---|---|---| | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"This endpoint needs a key bound to an approved app.","code":"app_key_required"}` | A Personal key was used. | | 403 | `{"error":"Use the app owner's key for this endpoint.","code":"owner_key_required"}` | The key was minted by a creator for themselves. App-level calls need a key held by your app owner: their own, or one their agency minted for its personas or managed creators. | | 400 | `{"error":"This endpoint acts as the key’s own account or app — call it without /creators/{creatorId}.","code":"creator_scope_not_allowed"}` | Called under /api/external/v1/creators/{creatorId}/ — this endpoint always acts as the key’s own account, agency or app. | | 403 | `{"error":"This app does not have the webhooks API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"webhooks"}` | Your app has the App webhooks [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/webhooks" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/webhooks`, { 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/v1/webhooks", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [Get a fresh ID-check link](https://www.dropfans.io/developers/reference/create-kyc-session.md) · Next: [Set your webhook endpoint](https://www.dropfans.io/developers/reference/set-webhook.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # PUT /api/external/v1/webhooks — Set your webhook endpoint > Sets the https endpoint that receives your app’s events. The first call also creates the signing secret and returns it **once** as `secret`. Store it; later calls don’t return it (use rotate-webhook-secret). - Source: https://www.dropfans.io/developers/reference/set-webhook - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/webhooks` (the unversioned alias). Sets the https endpoint that receives your app’s events. The first call also creates the signing secret and returns it **once** as `secret`. Store it; later calls don’t return it (use rotate-webhook-secret). Leave `events` out to get every event, including ones added later. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Request body (application/json) | Field | Type | Required | Description | |---|---|---|---| | `url` | string | yes | Public https URL (no IPs, no localhost, no credentials). | | `events` | string[] | | Subset of events to receive. Omit for all. | Example — All events: ```json { "url": "https://crm.example.com/hooks/dropfans" } ``` ## Responses ### 200 Saved. | Field | Type | Required | Description | |---|---|---|---| | `url` | string | yes | The saved endpoint. | | `events` | string[] | yes | Events delivered. | | `secret` | string | | Signing secret (`whsec_…`) — only on the first save. | ```json { "url": "https://crm.example.com/hooks/dropfans", "events": [ "drop.paid", "drop.refunded", "account.kyc_updated" ], "secret": "whsec_8d1e…" } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"url must use https"}` | Not a public https URL, or unknown events. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"This endpoint needs a key bound to an approved app.","code":"app_key_required"}` | A Personal key was used. | | 403 | `{"error":"Use the app owner's key for this endpoint.","code":"owner_key_required"}` | The key was minted by a creator for themselves. App-level calls need a key held by your app owner: their own, or one their agency minted for its personas or managed creators. | | 400 | `{"error":"This endpoint acts as the key’s own account or app — call it without /creators/{creatorId}.","code":"creator_scope_not_allowed"}` | Called under /api/external/v1/creators/{creatorId}/ — this endpoint always acts as the key’s own account, agency or app. | | 403 | `{"error":"This app does not have the webhooks API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"webhooks"}` | Your app has the App webhooks [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/webhooks" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://crm.example.com/hooks/dropfans" }' ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/webhooks`, { method: 'PUT', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ "url": "https://crm.example.com/hooks/dropfans" }), }); console.log(await res.json()); ``` ### Python ```python import os import requests res = requests.put( "https://www.dropfans.io/api/external/v1/webhooks", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, json={ "url": "https://crm.example.com/hooks/dropfans", }, ) print(res.json()) ``` --- Previous: [Get your webhook endpoint](https://www.dropfans.io/developers/reference/get-webhook.md) · Next: [Remove your webhook endpoint](https://www.dropfans.io/developers/reference/delete-webhook.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # DELETE /api/external/v1/webhooks — Remove your webhook endpoint > Stops deliveries. Events still waiting for a retry are dropped. Poll [GET /api/external/v1/orders](/developers/reference/list-orders) meanwhile. - Source: https://www.dropfans.io/developers/reference/delete-webhook - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/webhooks` (the unversioned alias). Stops deliveries. Events still waiting for a retry are dropped. Poll [GET /api/external/v1/orders](https://www.dropfans.io/developers/reference/list-orders.md) meanwhile. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Responses ### 200 Removed. | Field | Type | Required | Description | |---|---|---|---| | `deleted` | boolean | yes | Always true. | ```json { "deleted": true } ``` ## Errors | Status | Body | When | |---|---|---| | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"This endpoint needs a key bound to an approved app.","code":"app_key_required"}` | A Personal key was used. | | 403 | `{"error":"Use the app owner's key for this endpoint.","code":"owner_key_required"}` | The key was minted by a creator for themselves. App-level calls need a key held by your app owner: their own, or one their agency minted for its personas or managed creators. | | 400 | `{"error":"This endpoint acts as the key’s own account or app — call it without /creators/{creatorId}.","code":"creator_scope_not_allowed"}` | Called under /api/external/v1/creators/{creatorId}/ — this endpoint always acts as the key’s own account, agency or app. | | 403 | `{"error":"This app does not have the webhooks API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"webhooks"}` | Your app has the App webhooks [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/webhooks" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/webhooks`, { method: 'DELETE', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, }, }); console.log(await res.json()); ``` ### Python ```python import os import requests res = requests.delete( "https://www.dropfans.io/api/external/v1/webhooks", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [Set your webhook endpoint](https://www.dropfans.io/developers/reference/set-webhook.md) · Next: [Rotate the webhook signing secret](https://www.dropfans.io/developers/reference/rotate-webhook-secret.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # POST /api/external/v1/webhooks/rotate-secret — Rotate the webhook signing secret > Issues a new signing secret and returns it once. Every delivery from now on, including retries, is signed with it. Accept both secrets for a few minutes while you deploy. - Source: https://www.dropfans.io/developers/reference/rotate-webhook-secret - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/webhooks/rotate-secret` (the unversioned alias). Issues a new signing secret and returns it once. Every delivery from now on, including retries, is signed with it. Accept both secrets for a few minutes while you deploy. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Responses ### 200 The new secret. | Field | Type | Required | Description | |---|---|---|---| | `secret` | string | yes | The new `whsec_…` secret. | ```json { "secret": "whsec_51aa…" } ``` ## Errors | Status | Body | When | |---|---|---| | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"This endpoint needs a key bound to an approved app.","code":"app_key_required"}` | A Personal key was used. | | 403 | `{"error":"Use the app owner's key for this endpoint.","code":"owner_key_required"}` | The key was minted by a creator for themselves. App-level calls need a key held by your app owner: their own, or one their agency minted for its personas or managed creators. | | 400 | `{"error":"This endpoint acts as the key’s own account or app — call it without /creators/{creatorId}.","code":"creator_scope_not_allowed"}` | Called under /api/external/v1/creators/{creatorId}/ — this endpoint always acts as the key’s own account, agency or app. | | 403 | `{"error":"This app does not have the webhooks API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"webhooks"}` | Your app has the App webhooks [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/webhooks/rotate-secret" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/webhooks/rotate-secret`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, }, }); console.log(await res.json()); ``` ### Python ```python import os import requests res = requests.post( "https://www.dropfans.io/api/external/v1/webhooks/rotate-secret", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [Remove your webhook endpoint](https://www.dropfans.io/developers/reference/delete-webhook.md) · Next: [Send a test event](https://www.dropfans.io/developers/reference/test-webhook.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # POST /api/external/v1/webhooks/test — Send a test event > Sends a signed `ping` event to your endpoint and reports what it answered — use it to check your signature verification. - Source: https://www.dropfans.io/developers/reference/test-webhook - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/webhooks/test` (the unversioned alias). Sends a signed `ping` event to your endpoint and reports what it answered — use it to check your signature verification. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Responses ### 200 The outcome. | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Event id. | | `delivered` | boolean | yes | True when your endpoint answered 2xx. | | `statusCode` | integer \\| null | | Your endpoint’s status code. | | `error` | string \\| null | | Why it failed. | ```json { "id": "evt_9f01…", "delivered": true, "statusCode": 200, "error": null } ``` ## Errors | Status | Body | When | |---|---|---| | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"This endpoint needs a key bound to an approved app.","code":"app_key_required"}` | A Personal key was used. | | 403 | `{"error":"Use the app owner's key for this endpoint.","code":"owner_key_required"}` | The key was minted by a creator for themselves. App-level calls need a key held by your app owner: their own, or one their agency minted for its personas or managed creators. | | 409 | `{"error":"Set a webhook URL first (PUT /api/external/webhooks)."}` | No endpoint configured. | | 400 | `{"error":"This endpoint acts as the key’s own account or app — call it without /creators/{creatorId}.","code":"creator_scope_not_allowed"}` | Called under /api/external/v1/creators/{creatorId}/ — this endpoint always acts as the key’s own account, agency or app. | | 403 | `{"error":"This app does not have the webhooks API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"webhooks"}` | Your app has the App webhooks [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/webhooks/test" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/webhooks/test`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, }, }); console.log(await res.json()); ``` ### Python ```python import os import requests res = requests.post( "https://www.dropfans.io/api/external/v1/webhooks/test", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [Rotate the webhook signing secret](https://www.dropfans.io/developers/reference/rotate-webhook-secret.md) · Next: [Payout balance, eligibility and status](https://www.dropfans.io/developers/reference/get-payout-summary.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/v1/payouts/summary — Payout balance, eligibility and status > Everything a "Pay out" button needs in one call: balances, the minimum, the payout method and whether a payout request would be accepted right now. - Source: https://www.dropfans.io/developers/reference/get-payout-summary - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/payouts/summary` (the unversioned alias). Everything a "Pay out" button needs in one call: balances, the minimum, the payout method and whether a payout request would be accepted right now. It returns the balance per bucket and per side, the minimum, the default payout method (masked), the open request, whether a payout is being sent, the upcoming releases, and whether a payout request would be accepted right now. All money is integer **cents**. The balance is everything the account owns — its creator side (own sales), agency side (income from managed creators and AI personas), referral commission and partner shares — the same totals as the creator's Payouts page. `ineligibleReasons` lists **every** gate that fails, in the order [POST /payouts/requests](https://www.dropfans.io/developers/reference/create-payout-request.md) checks them, with the same codes it answers with: `kyc_required`, `payouts_disabled`, `consumer_account`, `no_method`, `masspay_not_activated`, `below_minimum`, `request_open`, `in_flight`. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `GET /api/external/v1/creators/{creatorId}/payouts/summary` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canViewStats`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## Responses ### 200 The summary. Money in cents. | Field | Type | Required | Description | |---|---|---|---| | `accountId` | string | yes | The account this describes. | | `currency` | `USD` | yes | Always "USD". | | `pendingCents` | integer | yes | Integer **cents**. Money the account owns (all sides) still inside the chargeback hold window; not payable yet. | | `availableCents` | integer | yes | Integer cents. Money the account owns (all sides) that cleared the hold and is not in a payout yet. Can be negative (a refund of money already paid out is netted against future earnings). | | `processingCents` | integer | yes | Integer cents. Money the account owns (all sides) inside a payout that is being sent. | | `paidOutCents` | integer | yes | Integer cents. Lifetime money the account owns (all sides) already paid out. | | `sides` | object | yes | The same four buckets per side, integer cents. | | `sides.creator` | PayoutBuckets | yes | Own sales. | | `sides.agency` | PayoutBuckets | yes | Income from managed creators / AI personas. | | `sides.affiliate` | PayoutBuckets | yes | Referral commission. | | `sides.share` | PayoutBuckets | yes | Partner shares of other accounts’ sales. | | `requestableCents` | integer | yes | Integer cents a payout request would ask for right now: the available balance of every side that has money and no open request. | | `minimumPayoutCents` | integer | yes | Integer cents. The available balance must reach this before a payout can be requested. 0 = no minimum. | | `payoutsEnabled` | boolean | yes | False when Dropfans has paused payouts for the account. | | `kycVerified` | boolean | yes | True once the account passed the ID check. | | `defaultMethod` | PayoutMethodSummary \\| null | | Where payouts go, masked. Null when no method is set up (the creator adds one in the dashboard). | | `openRequest` | Payout \\| null | | The newest open payout request (requested, approved or processing), or null. | | `inFlight` | boolean | yes | True while a payout is pending or processing. | | `upcoming` | object[] | yes | Creator-side earnings and partner shares still on hold, by the UTC day they become available. Soonest first. | | `eligible` | boolean | yes | True when POST /api/external/v1/payouts/requests would be accepted now. | | `ineligibleReasons` | string[] | yes | Every reason it would be refused, in the order the request checks them. Empty when eligible. | ```json { "accountId": "clx2f8a1b0001qw3k", "currency": "USD", "pendingCents": 12050, "availableCents": 34210, "processingCents": 0, "paidOutCents": 187500, "sides": { "creator": { "pendingCents": 12050, "availableCents": 34210, "processingCents": 0, "paidOutCents": 175500 }, "agency": { "pendingCents": 0, "availableCents": 0, "processingCents": 0, "paidOutCents": 12000 }, "affiliate": { "pendingCents": 0, "availableCents": 0, "processingCents": 0, "paidOutCents": 0 }, "share": { "pendingCents": 0, "availableCents": 0, "processingCents": 0, "paidOutCents": 0 } }, "requestableCents": 34210, "minimumPayoutCents": 2000, "payoutsEnabled": true, "kycVerified": true, "defaultMethod": { "id": "clxpm0001method", "type": "MASSPAY", "isDefault": true, "nickname": null, "label": "Payout wallet · SWE", "holderType": "INDIVIDUAL", "createdAt": "2026-09-02T10:00:00.000Z", "paxum": null, "masspay": { "country": "SWE", "currency": null, "payerName": null, "activated": true }, "bank": null }, "openRequest": null, "inFlight": false, "upcoming": [ { "availableOn": "2026-11-04", "amountCents": 4500 } ], "eligible": true, "ineligibleReasons": [] } ``` ## Errors | Status | Body | When | |---|---|---| | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canViewStats"}` | For a managed creator: the agency lacks `canViewStats` on that account. | | 403 | `{"error":"This app does not have the payouts API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"payouts"}` | Your app has the Payouts [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off (the default for new apps). | ## Notes `requestableCents` is what a request would ask for: the available balance of every side that has money and no open request. The minimum is checked against the combined `availableCents`. A negative `availableCents` (a refund of money already paid out) is netted against future earnings. ## 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/v1/payouts/summary" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/payouts/summary`, { 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/v1/payouts/summary", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [Send a test event](https://www.dropfans.io/developers/reference/test-webhook.md) · Next: [Payout history (paginated)](https://www.dropfans.io/developers/reference/list-payouts.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/v1/payouts — Payout history (paginated) > The account's payout history, newest first — the same rows as the creator's Payouts page, in integer cents. - Source: https://www.dropfans.io/developers/reference/list-payouts - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/payouts` (the unversioned alias). The account's payout history, newest first — the same rows as the creator's Payouts page, in integer cents. It lists payouts sent by Dropfans (`kind: "payout"`), payouts recorded by an admin (`"manual"`) and payout requests not fulfilled yet (`"request"`). Once a request is paid it is replaced by the payout that fulfilled it (`requestId` links them). Money in integer **cents**. Like the dashboard, payout attempts that never moved money (`failed` / `cancelled`) are left out unless you ask for them with `status`, and a wallet load that was sent back is hidden together with its negative twin unless `includeReversed=true` (the pair nets to zero). Keep the filters the same while you follow `nextCursor`. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `GET /api/external/v1/creators/{creatorId}/payouts` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canViewStats`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## Query parameters | Field | Type | Required | Description | |---|---|---|---| | `since` | string | | Only rows created (requests: made) at or after this time. | | `until` | string | | Only rows created at or before this time. | | `status` | string | | Comma-separated subset of requested, approved, pending, processing, paid, failed, cancelled, rejected. Default: everything except failed / cancelled payout attempts. | | `kind` | string | | Comma-separated subset of payout, manual, request. Default: all three. | | `includeReversed` | boolean | | Also return reversed wallet loads and their negative reversal rows. | | `limit` | integer | | Page size. | | `cursor` | string | | `nextCursor` from the previous page. | ## Responses ### 200 A page of rows. Money in cents. | Field | Type | Required | Description | |---|---|---|---| | `payouts` | Payout[] | yes | Newest first. | | `nextCursor` | string \\| null | | Pass as `cursor` for the next page; null on the last page. | ```json { "payouts": [ { "id": "clxpr0001request", "kind": "request", "status": "requested", "amountCents": 34210, "currency": "USD", "side": "creator", "sides": { "creatorCents": 34210, "agencyCents": 0, "affiliateCents": 0, "shareCents": 0 }, "method": { "type": "MASSPAY", "label": "Payout wallet · SWE" }, "note": "October", "failureMessage": null, "invoiceNumber": null, "requestId": null, "payoutId": null, "createdAt": "2026-10-30T09:12:00.000Z", "initiatedAt": null, "paidAt": null, "failedAt": null, "reversedAt": null, "reversalOfId": null }, { "id": "clxpo0001payout", "kind": "payout", "status": "paid", "amountCents": 52000, "currency": "USD", "side": "combined", "sides": { "creatorCents": 40000, "agencyCents": 12000, "affiliateCents": 0, "shareCents": 0 }, "method": { "type": "PAXUM_P2P", "label": "Paxum · j***@example.com" }, "note": null, "failureMessage": null, "invoiceNumber": 1042, "requestId": "clxpr0000request", "payoutId": null, "createdAt": "2026-10-15T08:00:00.000Z", "initiatedAt": "2026-10-15T08:00:05.000Z", "paidAt": "2026-10-15T08:03:41.000Z", "failedAt": null, "reversedAt": null, "reversalOfId": null } ], "nextCursor": null } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"since must be an ISO date","code":"invalid_since"}` | Unparseable `since` (`invalid_until` for `until`). | | 400 | `{"error":"status must be a comma-separated subset of requested,approved,pending,processing,paid,failed,cancelled,rejected","code":"invalid_status"}` | Unknown status (`invalid_kind` for an unknown kind). | | 400 | `{"error":"limit must be an integer from 1 to 100","code":"invalid_limit"}` | `limit` out of range. | | 400 | `{"error":"Invalid cursor","code":"invalid_cursor"}` | The cursor was not one we returned. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canViewStats"}` | For a managed creator: the agency lacks `canViewStats` on that account. | | 403 | `{"error":"This app does not have the payouts API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"payouts"}` | Your app has the Payouts [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off (the default for new apps). | ## 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/v1/payouts?since=2026-01-01T00%3A00%3A00Z&status=paid" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/payouts?since=2026-01-01T00%3A00%3A00Z&status=paid`, { 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/v1/payouts", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, params={"since": "2026-01-01T00:00:00Z", "status": "paid"}, ) print(res.json()) ``` --- Previous: [Payout balance, eligibility and status](https://www.dropfans.io/developers/reference/get-payout-summary.md) · Next: [One payout or payout request](https://www.dropfans.io/developers/reference/get-payout.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/v1/payouts/{id} — One payout or payout request > One row of the history by id — a payout, a manual payout or a payout request — in any status, including failed attempts, fulfilled requests and reversal rows. - Source: https://www.dropfans.io/developers/reference/get-payout - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/payouts/{id}` (the unversioned alias). One row of the history by id — a payout, a manual payout or a payout request — in any status, including failed attempts, fulfilled requests and reversal rows. The same shape as the [payout.* webhooks](https://www.dropfans.io/developers/webhooks/overview.md) (which add `accountId`). ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `GET /api/external/v1/creators/{creatorId}/payouts/{id}` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canViewStats`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## Path parameters | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Row id from the list, a webhook or POST /payouts/requests. | ## Responses ### 200 The row. Money in cents. | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Id of the row. Unique across the three kinds — use it with GET /api/external/v1/payouts/{id}. | | `kind` | `payout` \\| `manual` \\| `request` | yes | `payout`: sent by Dropfans (Paxum, MassPay wallet load, or settled by an admin). `manual`: paid outside the platform and recorded by an admin. `request`: a payout request the account made that has not been fulfilled into a payout yet. | | `status` | `requested` \\| `approved` \\| `pending` \\| `processing` \\| `paid` \\| `failed` \\| `cancelled` \\| `rejected` | yes | Requests: `requested` → `approved` → (`processing` for Paxum, from the moment they are made) → `paid`, or `rejected` / `cancelled`. Payouts: `pending` (in a batch, not sent yet) → `processing` (sent) → `paid` or `failed`; `cancelled` when an attempt was dropped. Manual payouts are always `paid`. | | `amountCents` | integer | yes | Integer **cents**. What is paid out (negative only on a reversal row). | | `currency` | `USD` | yes | Always "USD". | | `side` | string | yes | Which balance it draws from: `creator` (own sales), `agency` (income from managed creators / AI personas), `affiliate` (referral commission), `share` (partner shares of other accounts’ sales), or `combined` (several sides in one transfer — see `sides`). | | `sides` | object | yes | The amount split by side, integer cents. Adds up to `amountCents`. | | `sides.creatorCents` | integer | yes | Creator side, cents. | | `sides.agencyCents` | integer | yes | Agency side, cents. | | `sides.affiliateCents` | integer | yes | Referral commission, cents. | | `sides.shareCents` | integer | yes | Partner shares, cents. | | `method` | object \\| null | | The destination frozen when the payout or request was made, masked. Null for manual payouts. | | `method.type` | `PAXUM_P2P` \\| `MASSPAY` \\| `BANK_SWIFT` | yes | Provider method type. | | `method.label` | string | yes | Display label — the creator’s nickname, or e.g. "Paxum · j***@example.com", "Payout wallet · SWE", "Bank transfer · ••••1234". Never a full e-mail, IBAN or token. | | `note` | string \\| null | | Requests: the note sent with the request. Manual payouts: the note the admin recorded for the payee. Otherwise null. | | `failureMessage` | string \\| null | | Why a `failed` payout failed, as reported by the provider. Null unless `status` is `failed`. | | `invoiceNumber` | integer \\| null | | Payout invoice number once the invoice exists. The invoice itself is in the dashboard. | | `requestId` | string \\| null | | kind `payout` / `manual`: the request this payout fulfilled, if any. | | `payoutId` | string \\| null | | kind `request`: the payout or manual payout that fulfilled it, if any. | | `createdAt` | string | yes | When the payout was created or the request made. The list is ordered by this. | | `initiatedAt` | string \\| null | | When the payout was sent to the provider. | | `paidAt` | string \\| null | | When it was paid. Null unless `status` is `paid` (requests carry it on the fulfilling payout instead). | | `failedAt` | string \\| null | | When it failed. Null unless `status` is `failed`. | | `reversedAt` | string \\| null | | Set on a paid wallet load that was sent back (spendback): the money is available again. Its negative twin has `reversalOfId`. Both are hidden from GET /payouts unless includeReversed=true. | | `reversalOfId` | string \\| null | | Set on the negative row that reverses the payout with this id. | ```json { "id": "clxpo0001payout", "kind": "payout", "status": "paid", "amountCents": 52000, "currency": "USD", "side": "combined", "sides": { "creatorCents": 40000, "agencyCents": 12000, "affiliateCents": 0, "shareCents": 0 }, "method": { "type": "PAXUM_P2P", "label": "Paxum · j***@example.com" }, "note": null, "failureMessage": null, "invoiceNumber": 1042, "requestId": "clxpr0000request", "payoutId": null, "createdAt": "2026-10-15T08:00:00.000Z", "initiatedAt": "2026-10-15T08:00:05.000Z", "paidAt": "2026-10-15T08:03:41.000Z", "failedAt": null, "reversedAt": null, "reversalOfId": null } ``` ## Errors | Status | Body | When | |---|---|---| | 404 | `{"error":"Payout not found","code":"not_found"}` | Unknown id, or it belongs to another account. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canViewStats"}` | For a managed creator: the agency lacks `canViewStats` on that account. | | 403 | `{"error":"This app does not have the payouts API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"payouts"}` | Your app has the Payouts [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off (the default for new apps). | ## 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/v1/payouts/$ID" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const id = '…'; // from an earlier response const res = await fetch(`https://www.dropfans.io/api/external/v1/payouts/${id}`, { headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, }, }); console.log(await res.json()); ``` ### Python ```python import os import requests id = "…" # from an earlier response res = requests.get( f"https://www.dropfans.io/api/external/v1/payouts/{id}", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [Payout history (paginated)](https://www.dropfans.io/developers/reference/list-payouts.md) · Next: [Payout methods (masked)](https://www.dropfans.io/developers/reference/list-payout-methods.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/v1/payouts/methods — Payout methods (masked) > The account's payout methods, default first, with every destination masked. - Source: https://www.dropfans.io/developers/reference/list-payout-methods - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/payouts/methods` (the unversioned alias). The account's payout methods, default first, with every destination masked. Add a Paxum method with [POST /payouts/methods](https://www.dropfans.io/developers/reference/create-payout-method.md), the MassPay wallet with [POST /payouts/masspay/profile](https://www.dropfans.io/developers/reference/setup-masspay-wallet.md); the creator can also manage them in the dashboard. Nothing that moves money is returned: the Paxum e-mail is masked (`j***@example.com`), a bank account shows its last 4 characters only, and MassPay tokens are never included. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. **For a creator you manage or onboarded:** `GET /api/external/v1/creators/{creatorId}/payouts/methods` — with a verified agency's own key for a creator or AI persona it manages, or your app owner's own key for an account your app onboarded (ids from GET /api/external/v1/creators); for a managed creator the agency needs `canViewStats`. The response echoes the account acted on in `X-Dropfans-Account`. Sending `X-Dropfans-Account: ` on the plain path still works but is deprecated. ## Responses ### 200 The methods. | Field | Type | Required | Description | |---|---|---|---| | `methods` | PayoutMethodSummary[] | yes | Default first, then newest first. | ```json { "methods": [ { "id": "clxpm0001method", "type": "PAXUM_P2P", "isDefault": true, "nickname": null, "label": "Paxum · j***@example.com", "holderType": "INDIVIDUAL", "createdAt": "2026-09-02T10:00:00.000Z", "paxum": { "email": "j***@example.com", "lookupStatus": "active", "verificationStatus": "COMPLETED" }, "masspay": null, "bank": null } ] } ``` ## Errors | Status | Body | When | |---|---|---| | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 400 | `{"error":"X-Dropfans-Account must be an account id (from GET /api/external/agency/creators), not a username.","code":"invalid_account_header"}` | `X-Dropfans-Account` is not an account id. | | 400 | `{"error":"X-Dropfans-Account names a different account than the path — send one or the other.","code":"account_mismatch"}` | Called under /v1/creators/{creatorId}/ with an `X-Dropfans-Account` naming another account. | | 403 | `{"error":"This key cannot act for that account.","code":"account_not_managed"}` | The creator in the path (or `X-Dropfans-Account`) is one this key cannot act for: no ACCEPTED management by your agency and not an account your app onboarded (or it no longer holds a key bound to your app), a key minted for one creator, or someone is suspended, banned or deleted. An unknown id gets the same answer. | | 403 | `{"error":"The agency does not have permission for this on that account.","code":"permission_denied","permission":"canViewStats"}` | For a managed creator: the agency lacks `canViewStats` on that account. | | 403 | `{"error":"This app does not have the payouts API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"payouts"}` | Your app has the Payouts [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off (the default for new apps). | ## 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/v1/payouts/methods" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/payouts/methods`, { 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/v1/payouts/methods", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [One payout or payout request](https://www.dropfans.io/developers/reference/get-payout.md) · Next: [Add a Paxum payout method](https://www.dropfans.io/developers/reference/create-payout-method.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # POST /api/external/v1/payouts/methods — Add a Paxum payout method > Add a Paxum account as a payout method — what "Add Paxum" does in the dashboard, with a stricter e-mail check. - Source: https://www.dropfans.io/developers/reference/create-payout-method - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/payouts/methods` (the unversioned alias). Add a Paxum account as a payout method — what "Add Paxum" does in the dashboard, with a stricter e-mail check. The e-mail is looked up at Paxum and must be an **active** Paxum account: unknown or inactive accounts are refused with 422 `paxum_account_not_active` (the dashboard shows that on the card; an integration has nobody to show it to). If Paxum cannot be reached the answer is 502 `paxum_unavailable` — retry. **Safe to retry, no `Idempotency-Key` needed:** adding an e-mail the account already has (case-insensitive) returns that existing method with 200 and `Idempotent-Replayed: true` instead of a duplicate; `nickname` / `isDefault` are then not applied (use PATCH). `isDefault: true` makes it the method payouts go to. A MassPay wallet is set up with [POST /payouts/masspay/profile](https://www.dropfans.io/developers/reference/setup-masspay-wallet.md) instead. The key's own account only (`X-Dropfans-Account` and agency-minted keys are refused) and only after the ID check (403 `kyc_required`). > [!NOTE] Needs the Payouts category and an app key > Payouts are an [API category](https://www.dropfans.io/developers/concepts/api-categories.md) that is off for every app until we switch it on — ask support. Personal keys can read payouts but get 403 `app_key_required` here. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Request body (application/json) | Field | Type | Required | Description | |---|---|---|---| | `type` | `PAXUM_P2P` | yes | The method type. Only `PAXUM_P2P` is created here. | | `paxumEmail` | string | yes | The e-mail of the creator’s Paxum account. Never returned unmasked. | | `nickname` | string \\| null | | The creator’s own label, shown instead of the masked e-mail. | | `isDefault` | boolean | | Make it the default payout method. | Example — Paxum as the default method: ```json { "type": "PAXUM_P2P", "paxumEmail": "jane@example.com", "nickname": "Main Paxum", "isDefault": true } ``` ## Responses ### 201 The method was added (200 with `Idempotent-Replayed: true` when the account already had this e-mail). | Field | Type | Required | Description | |---|---|---|---| | `method` | PayoutMethodSummary | yes | The method, masked. | ```json { "method": { "id": "clxpm0002method", "type": "PAXUM_P2P", "isDefault": true, "nickname": "Main Paxum", "label": "Main Paxum", "holderType": "INDIVIDUAL", "createdAt": "2026-10-30T10:00:00.000Z", "paxum": { "email": "j***@example.com", "lookupStatus": "active", "verificationStatus": null }, "masspay": null, "bank": null } } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"paxumEmail is not a valid email","code":"invalid_paxum_email"}` | `paxumEmail` missing or not an e-mail. | | 400 | `{"error":"type must be \"PAXUM_P2P\".","code":"unsupported_type"}` | `type` missing or another type (MASSPAY: use POST /payouts/masspay/profile). | | 400 | `{"error":"Unknown field: iban. Allowed: type, paxumEmail, nickname, isDefault.","code":"unknown_field"}` | A field that is not in the table (`invalid_nickname` / `invalid_is_default` for a wrong type or length). | | 400 | `{"error":"Invalid JSON body"}` | The body is not valid JSON (`invalid_body` when it is not an object). | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"Verify your identity (KYC) before setting up payouts.","code":"kyc_required"}` | The account has not passed the ID check. | | 403 | `{"error":"Payout methods need the account’s own key — a key an agency minted for a managed account cannot read or change where payouts go.","code":"agency_key_not_allowed"}` | The key was minted by an agency for an account it manages. | | 403 | `{"error":"This endpoint needs a key bound to an app with the payouts API category enabled — personal keys cannot use it.","code":"app_key_required","category":"payouts"}` | A personal key (not bound to an app). Personal keys may read payouts, but not change them. | | 409 | `{"error":"Another payout operation is running for this account. Try again in a moment.","code":"payout_busy"}` | Another payout operation holds the account’s payout lock. Retry. | | 422 | `{"error":"No Paxum account uses this e-mail. Check it, or create the Paxum account first.","code":"paxum_account_not_active","lookupStatus":"not_found"}` | The e-mail is not an active Paxum account (`lookupStatus`: `not_found` or `inactive`). | | 502 | `{"error":"Could not reach Paxum to check the e-mail. Try again in a moment.","code":"paxum_unavailable"}` | The Paxum lookup failed. Retry. | | 400 | `{"error":"This endpoint only acts as the key’s own account — send it without X-Dropfans-Account.","code":"account_header_not_allowed"}` | `X-Dropfans-Account` names another account. An agency can never change where a creator it manages is paid. | | 400 | `{"error":"This endpoint acts as the key’s own account or app — call it without /creators/{creatorId}.","code":"creator_scope_not_allowed"}` | Called under /api/external/v1/creators/{creatorId}/ — this endpoint always acts as the key’s own account, agency or app. | | 403 | `{"error":"This app does not have the payouts API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"payouts"}` | Your app has the Payouts [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off (the default for new apps). | ## 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/v1/payouts/methods" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "type": "PAXUM_P2P", "paxumEmail": "jane@example.com", "nickname": "Main Paxum", "isDefault": true }' ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/payouts/methods`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ "type": "PAXUM_P2P", "paxumEmail": "jane@example.com", "nickname": "Main Paxum", "isDefault": true }), }); console.log(await res.json()); ``` ### Python ```python import os import requests res = requests.post( "https://www.dropfans.io/api/external/v1/payouts/methods", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, json={ "type": "PAXUM_P2P", "paxumEmail": "jane@example.com", "nickname": "Main Paxum", "isDefault": True, }, ) print(res.json()) ``` --- Previous: [Payout methods (masked)](https://www.dropfans.io/developers/reference/list-payout-methods.md) · Next: [Change a payout method](https://www.dropfans.io/developers/reference/update-payout-method.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # PATCH /api/external/v1/payouts/methods/{id} — Change a payout method > Rename a payout method, make it the default (or stop using it as the default), or point a Paxum method at another Paxum e-mail. - Source: https://www.dropfans.io/developers/reference/update-payout-method - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/payouts/methods/{id}` (the unversioned alias). Rename a payout method, make it the default (or stop using it as the default), or point a Paxum method at another Paxum e-mail. Send only what changes. Any other field is refused with 400 `unknown_field`. A new `paxumEmail` is looked up again like on create (422 `paxum_account_not_active` unless active) and clears the method's earlier Paxum identity check, which was for the old account. `isDefault: true` moves the default here; `isDefault: false` leaves the account without a default method. The key's own account only, after the ID check. > [!NOTE] Needs the Payouts category and an app key > Payouts are an [API category](https://www.dropfans.io/developers/concepts/api-categories.md) that is off for every app until we switch it on — ask support. Personal keys can read payouts but get 403 `app_key_required` here. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Path parameters | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The method id (from GET /payouts/methods). | ## Request body (application/json) | Field | Type | Required | Description | |---|---|---|---| | `nickname` | string \\| null | | New label; null or "" clears it. | | `isDefault` | boolean | | true: make it the default. false: no longer the default. | | `paxumEmail` | string | | `PAXUM_P2P` only: the new Paxum e-mail. | Example — Make it the default: ```json { "isDefault": true } ``` ## Responses ### 200 The updated method. | Field | Type | Required | Description | |---|---|---|---| | `method` | PayoutMethodSummary | yes | The method, masked. | ```json { "method": { "id": "clxpm0002method", "type": "PAXUM_P2P", "isDefault": true, "nickname": "Main Paxum", "label": "Main Paxum", "holderType": "INDIVIDUAL", "createdAt": "2026-10-30T10:00:00.000Z", "paxum": { "email": "j***@example.com", "lookupStatus": "active", "verificationStatus": null }, "masspay": null, "bank": null } } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"Unknown field: bankIban. Allowed: nickname, isDefault, paxumEmail.","code":"unknown_field"}` | A field that cannot be changed (`invalid_nickname`, `invalid_is_default`, `invalid_paxum_email` for bad values). | | 400 | `{"error":"paxumEmail can only be changed on a PAXUM_P2P method.","code":"field_not_applicable"}` | `paxumEmail` on a MassPay wallet. | | 400 | `{"error":"Invalid JSON body"}` | The body is not valid JSON (`invalid_body` when it is not an object). | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"Verify your identity (KYC) before setting up payouts.","code":"kyc_required"}` | The account has not passed the ID check. | | 403 | `{"error":"Payout methods need the account’s own key — a key an agency minted for a managed account cannot read or change where payouts go.","code":"agency_key_not_allowed"}` | The key was minted by an agency for an account it manages. | | 403 | `{"error":"This endpoint needs a key bound to an app with the payouts API category enabled — personal keys cannot use it.","code":"app_key_required","category":"payouts"}` | A personal key (not bound to an app). Personal keys may read payouts, but not change them. | | 404 | `{"error":"Not found","code":"not_found"}` | Unknown id, or it belongs to another account. | | 422 | `{"error":"This Paxum account is not active. Activate it at Paxum first.","code":"paxum_account_not_active","lookupStatus":"inactive"}` | The new e-mail is not an active Paxum account. | | 502 | `{"error":"Could not reach Paxum to check the e-mail. Try again in a moment.","code":"paxum_unavailable"}` | The Paxum lookup failed. Retry. | | 400 | `{"error":"This endpoint only acts as the key’s own account — send it without X-Dropfans-Account.","code":"account_header_not_allowed"}` | `X-Dropfans-Account` names another account. An agency can never change where a creator it manages is paid. | | 400 | `{"error":"This endpoint acts as the key’s own account or app — call it without /creators/{creatorId}.","code":"creator_scope_not_allowed"}` | Called under /api/external/v1/creators/{creatorId}/ — this endpoint always acts as the key’s own account, agency or app. | | 403 | `{"error":"This app does not have the payouts API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"payouts"}` | Your app has the Payouts [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off (the default for new apps). | ## 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/v1/payouts/methods/$ID" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "isDefault": true }' ``` ### Node ```javascript const id = '…'; // from an earlier response const res = await fetch(`https://www.dropfans.io/api/external/v1/payouts/methods/${id}`, { method: 'PATCH', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ "isDefault": true }), }); console.log(await res.json()); ``` ### Python ```python import os import requests id = "…" # from an earlier response res = requests.patch( f"https://www.dropfans.io/api/external/v1/payouts/methods/{id}", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, json={ "isDefault": True, }, ) print(res.json()) ``` --- Previous: [Add a Paxum payout method](https://www.dropfans.io/developers/reference/create-payout-method.md) · Next: [Remove a payout method](https://www.dropfans.io/developers/reference/delete-payout-method.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # DELETE /api/external/v1/payouts/methods/{id} — Remove a payout method > Remove a payout method — refused with 409 `method_in_use` while a payout to it is still pending or processing. - Source: https://www.dropfans.io/developers/reference/delete-payout-method - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/payouts/methods/{id}` (the unversioned alias). Remove a payout method — refused with 409 `method_in_use` while a payout to it is still pending or processing. Payouts already made keep their own masked copy of the destination, so the history is unaffected. Removing the MassPay wallet does not close the payee at MassPay: setting the wallet up again re-attaches it. The key's own account only, after the ID check. > [!NOTE] Needs the Payouts category and an app key > Payouts are an [API category](https://www.dropfans.io/developers/concepts/api-categories.md) that is off for every app until we switch it on — ask support. Personal keys can read payouts but get 403 `app_key_required` here. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Path parameters | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The method id. | ## Responses ### 200 Removed. | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The removed method. | | `deleted` | boolean | yes | Always true. | ```json { "id": "clxpm0002method", "deleted": true } ``` ## Errors | Status | Body | When | |---|---|---| | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"Verify your identity (KYC) before setting up payouts.","code":"kyc_required"}` | The account has not passed the ID check. | | 403 | `{"error":"Payout methods need the account’s own key — a key an agency minted for a managed account cannot read or change where payouts go.","code":"agency_key_not_allowed"}` | The key was minted by an agency for an account it manages. | | 403 | `{"error":"This endpoint needs a key bound to an app with the payouts API category enabled — personal keys cannot use it.","code":"app_key_required","category":"payouts"}` | A personal key (not bound to an app). Personal keys may read payouts, but not change them. | | 404 | `{"error":"Not found","code":"not_found"}` | Unknown id, or it belongs to another account (also on a repeated delete). | | 409 | `{"error":"Cannot delete: this method is attached to active payouts. Wait for them to settle.","code":"method_in_use"}` | A payout to this method is pending or processing. | | 400 | `{"error":"This endpoint only acts as the key’s own account — send it without X-Dropfans-Account.","code":"account_header_not_allowed"}` | `X-Dropfans-Account` names another account. An agency can never change where a creator it manages is paid. | | 400 | `{"error":"This endpoint acts as the key’s own account or app — call it without /creators/{creatorId}.","code":"creator_scope_not_allowed"}` | Called under /api/external/v1/creators/{creatorId}/ — this endpoint always acts as the key’s own account, agency or app. | | 403 | `{"error":"This app does not have the payouts API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"payouts"}` | Your app has the Payouts [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off (the default for new apps). | ## 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/v1/payouts/methods/$ID" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const id = '…'; // from an earlier response const res = await fetch(`https://www.dropfans.io/api/external/v1/payouts/methods/${id}`, { method: 'DELETE', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, }, }); console.log(await res.json()); ``` ### Python ```python import os import requests id = "…" # from an earlier response res = requests.delete( f"https://www.dropfans.io/api/external/v1/payouts/methods/{id}", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [Change a payout method](https://www.dropfans.io/developers/reference/update-payout-method.md) · Next: [MassPay payout profile](https://www.dropfans.io/developers/reference/get-masspay-profile.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/v1/payouts/masspay/profile — MassPay payout profile > The MassPay payout profile behind the payout wallet, read live from MassPay, with the required fields it still lacks in `missingFields`. - Source: https://www.dropfans.io/developers/reference/get-masspay-profile - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/payouts/masspay/profile` (the unversioned alias). The MassPay payout profile behind the payout wallet, read live from MassPay, with the required fields it still lacks in `missingFields`. Before the payee exists (`provisioned: false`) the profile is a prefill from the account (name, e-mail, country) for your form. A payee created automatically at the ID check often has no address yet: `missingFields` then lists what [POST /payouts/masspay/profile](https://www.dropfans.io/developers/reference/setup-masspay-wallet.md) has to send. Personal data — the key's own account only. > [!NOTE] Needs the Payouts category and an app key > Payouts are an [API category](https://www.dropfans.io/developers/concepts/api-categories.md) that is off for every app until we switch it on — ask support. Personal keys can read payouts but get 403 `app_key_required` here. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Responses ### 200 The profile. | Field | Type | Required | Description | |---|---|---|---| | `provisioned` | boolean | yes | True once the payee exists at MassPay. | | `status` | string \\| null | | MassPay’s status for the payee (e.g. ACTIVE). Null before it exists. | | `missingFields` | string[] | yes | Required fields blank at MassPay. Empty when complete (and before the payee exists). | | `profile` | MasspayProfile | yes | The values. Blank fields are empty strings. | ```json { "provisioned": true, "status": "ACTIVE", "missingFields": [ "address1", "city", "stateProvince", "postalCode" ], "profile": { "accountType": "INDIVIDUAL", "businessName": "", "firstName": "Jane", "middleName": "", "lastName": "Doe", "email": "jane@example.com", "mobileNumber": "46701234567", "language": "en", "dateOfBirth": "1994-05-17", "country": "SWE", "address1": "", "address2": "", "city": "", "stateProvince": "", "postalCode": "" } } ``` ## Errors | Status | Body | When | |---|---|---| | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"Payout methods need the account’s own key — a key an agency minted for a managed account cannot read or change where payouts go.","code":"agency_key_not_allowed"}` | The key was minted by an agency for an account it manages. | | 403 | `{"error":"This endpoint needs a key bound to an app with the payouts API category enabled — personal keys cannot use it.","code":"app_key_required","category":"payouts"}` | A personal key (not bound to an app). Personal keys may read payouts, but not change them. | | 502 | `{"error":"Could not read your MassPay profile. Try again in a moment.","code":"masspay_unavailable"}` | MassPay did not answer. Retry. | | 503 | `{"error":"MassPay is not available right now.","code":"not_configured"}` | MassPay payouts are switched off on this environment. | | 400 | `{"error":"This endpoint only acts as the key’s own account — send it without X-Dropfans-Account.","code":"account_header_not_allowed"}` | `X-Dropfans-Account` names another account. An agency can never change where a creator it manages is paid. | | 400 | `{"error":"This endpoint acts as the key’s own account or app — call it without /creators/{creatorId}.","code":"creator_scope_not_allowed"}` | Called under /api/external/v1/creators/{creatorId}/ — this endpoint always acts as the key’s own account, agency or app. | | 403 | `{"error":"This app does not have the payouts API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"payouts"}` | Your app has the Payouts [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off (the default for new apps). | ## 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/v1/payouts/masspay/profile" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/payouts/masspay/profile`, { 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/v1/payouts/masspay/profile", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [Remove a payout method](https://www.dropfans.io/developers/reference/delete-payout-method.md) · Next: [Set up the MassPay wallet](https://www.dropfans.io/developers/reference/setup-masspay-wallet.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # POST /api/external/v1/payouts/masspay/profile — Set up the MassPay wallet > Set up the account's MassPay payout wallet from the same profile the dashboard's setup form collects, and make it the default payout method. - Source: https://www.dropfans.io/developers/reference/setup-masspay-wallet - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/payouts/masspay/profile` (the unversioned alias). Set up the account's MassPay payout wallet from the same profile the dashboard's setup form collects, and make it the default payout method. **Required:** `firstName`, `lastName`, `email`, `country`, `address1`, `city`, `stateProvince`, `postalCode`, plus `businessName` when `accountType` is `BUSINESS`. **Optional:** `middleName`, `mobileNumber`, `language`, `dateOfBirth`, `address2`. Enter everything as on the payee's government-issued ID. Every field is checked before anything is sent to MassPay; a refused profile answers 400 `invalid_profile` naming the first bad `field`, with all of them in `errors`. - **No payee yet** → the payee is created at MassPay with the full profile (201). - **Payee exists but lacks required details** (see `missingFields` on [GET /payouts/masspay/profile](https://www.dropfans.io/developers/reference/get-masspay-profile.md)) → the fields you send fill them in. `country`, `email` and `businessName` stay as MassPay has them; send at least the missing fields. - **Payee complete** → the body is not needed; the wallet is (re)attached. **Activation:** before the first payout to the wallet, the payee must open `activationUrl` once in a browser (a MassPay requirement). The link expires — get a fresh one from [GET /payouts/masspay/activation](https://www.dropfans.io/developers/reference/get-masspay-activation.md) when you send the payee there. Payout requests to a wallet that is not activated are refused (`masspay_not_activated`). Safe to retry: there is one MassPay payee and one wallet per account. The key's own account only, after the ID check. > [!NOTE] Needs the Payouts category and an app key > Payouts are an [API category](https://www.dropfans.io/developers/concepts/api-categories.md) that is off for every app until we switch it on — ask support. Personal keys can read payouts but get 403 `app_key_required` here. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Request body (application/json) The payout profile. Optional when the payee already exists and is complete. | Field | Type | Required | Description | |---|---|---|---| | `accountType` | `INDIVIDUAL` \\| `BUSINESS` | | Who the payee is. `BUSINESS` makes `businessName` required; the person fields are then the authorized representative’s. | | `businessName` | string | | **Required when `accountType` is BUSINESS.** The company’s registered legal name. Its presence is what makes the payee a business account at MassPay; the person fields are then the authorized representative’s. Cannot be changed once set. | | `firstName` | string | yes | As on the government-issued ID. | | `middleName` | string | | Optional. As on the ID. | | `lastName` | string | yes | As on the ID. | | `email` | string | yes | Must not be used by another MassPay account. | | `mobileNumber` | string | | Optional. With country code; 7–15 digits once spaces and symbols are removed. | | `language` | string | | Optional. ISO 639-1 code for MassPay’s e-mails and portal (default "en"). | | `dateOfBirth` | string | | Optional. YYYY-MM-DD, a real date; the payee must be at least 18. | | `country` | string | yes | ISO 3166-1 alpha-3 country of residence. | | `address1` | string | yes | Street address. | | `address2` | string | | Optional. Apartment, suite, c/o. | | `city` | string | yes | City. | | `stateProvince` | string | yes | State, province, county or region. | | `postalCode` | string | yes | Postal / ZIP code. | Example — An individual: ```json { "accountType": "INDIVIDUAL", "firstName": "Jane", "lastName": "Doe", "email": "jane@example.com", "country": "SWE", "address1": "Storgatan 1", "city": "Stockholm", "stateProvince": "Stockholm County", "postalCode": "111 22", "dateOfBirth": "1994-05-17", "language": "sv" } ``` Example — Completing an existing payee: ```json { "address1": "Storgatan 1", "city": "Stockholm", "stateProvince": "Stockholm County", "postalCode": "111 22" } ``` ## Responses ### 201 The wallet was attached (201 when the wallet method was created, 200 when an existing one was re-attached). | Field | Type | Required | Description | |---|---|---|---| | `method` | PayoutMethodSummary | yes | The wallet method, now the default. | | `activationUrl` | string \\| null | | Where the payee activates the wallet (once, in a browser). Null when already activated. Expires. | | `created` | boolean | yes | True when the MassPay payee was created by this call. | | `completed` | boolean | yes | True when missing profile fields were filled in at MassPay by this call. | ```json { "method": { "id": "clxpm0001method", "type": "MASSPAY", "isDefault": true, "nickname": null, "label": "Payout wallet · SWE", "holderType": "INDIVIDUAL", "createdAt": "2026-10-30T10:00:00.000Z", "paxum": null, "masspay": { "country": "SWE", "currency": null, "payerName": null, "activated": false }, "bank": null }, "activationUrl": "https://payee.masspay.io/activate/…", "created": true, "completed": false } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"City is required.","code":"invalid_profile","field":"city","errors":[{"field":"city","code":"required","message":"City is required."}]}` | A field is missing or invalid. Codes in `errors`: required, too_long, invalid_type, invalid_account_type, invalid_email, invalid_country, invalid_date_of_birth, under_18, invalid_mobile_number, invalid_language. | | 400 | `{"error":"Invalid JSON body"}` | The body is not valid JSON (`invalid_body` when it is not an object). | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"Verify your identity (KYC) before setting up payouts.","code":"kyc_required"}` | The account has not passed the ID check. | | 403 | `{"error":"Payout methods need the account’s own key — a key an agency minted for a managed account cannot read or change where payouts go.","code":"agency_key_not_allowed"}` | The key was minted by an agency for an account it manages. | | 403 | `{"error":"This endpoint needs a key bound to an app with the payouts API category enabled — personal keys cannot use it.","code":"app_key_required","category":"payouts"}` | A personal key (not bound to an app). Personal keys may read payouts, but not change them. | | 409 | `{"error":"Could not determine your payout country. Contact support.","code":"no_country"}` | An existing payee has no usable country. Contact support. | | 422 | `{"error":"This email is already used by another MassPay account. Enter a different email.","code":"masspay_email_in_use"}` | MassPay refused the profile (`masspay_rejected` with MassPay’s reason for other refusals). | | 502 | `{"error":"Could not read your MassPay profile. Try again in a moment.","code":"masspay_unavailable"}` | MassPay did not answer. Retry. | | 503 | `{"error":"MassPay is not available right now.","code":"not_configured"}` | MassPay payouts are switched off on this environment. | | 400 | `{"error":"This endpoint only acts as the key’s own account — send it without X-Dropfans-Account.","code":"account_header_not_allowed"}` | `X-Dropfans-Account` names another account. An agency can never change where a creator it manages is paid. | | 400 | `{"error":"This endpoint acts as the key’s own account or app — call it without /creators/{creatorId}.","code":"creator_scope_not_allowed"}` | Called under /api/external/v1/creators/{creatorId}/ — this endpoint always acts as the key’s own account, agency or app. | | 403 | `{"error":"This app does not have the payouts API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"payouts"}` | Your app has the Payouts [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off (the default for new apps). | ## 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/v1/payouts/masspay/profile" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "accountType": "INDIVIDUAL", "firstName": "Jane", "lastName": "Doe", "email": "jane@example.com", "country": "SWE", "address1": "Storgatan 1", "city": "Stockholm", "stateProvince": "Stockholm County", "postalCode": "111 22", "dateOfBirth": "1994-05-17", "language": "sv" }' ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/payouts/masspay/profile`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ "accountType": "INDIVIDUAL", "firstName": "Jane", "lastName": "Doe", "email": "jane@example.com", "country": "SWE", "address1": "Storgatan 1", "city": "Stockholm", "stateProvince": "Stockholm County", "postalCode": "111 22", "dateOfBirth": "1994-05-17", "language": "sv" }), }); console.log(await res.json()); ``` ### Python ```python import os import requests res = requests.post( "https://www.dropfans.io/api/external/v1/payouts/masspay/profile", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, json={ "accountType": "INDIVIDUAL", "firstName": "Jane", "lastName": "Doe", "email": "jane@example.com", "country": "SWE", "address1": "Storgatan 1", "city": "Stockholm", "stateProvince": "Stockholm County", "postalCode": "111 22", "dateOfBirth": "1994-05-17", "language": "sv", }, ) print(res.json()) ``` --- Previous: [MassPay payout profile](https://www.dropfans.io/developers/reference/get-masspay-profile.md) · Next: [MassPay wallet activation](https://www.dropfans.io/developers/reference/get-masspay-activation.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/v1/payouts/masspay/activation — MassPay wallet activation > Whether the payee has activated the MassPay wallet and, if not, a fresh `activationUrl` to send them to. - Source: https://www.dropfans.io/developers/reference/get-masspay-activation - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/payouts/masspay/activation` (the unversioned alias). Whether the payee has activated the MassPay wallet and, if not, a fresh `activationUrl` to send them to. The payee opens it once in a browser; MassPay refuses payouts to the wallet until then. The link expires, so fetch it right before you show it, and never store it. Once activated, `activated` stays true. The link signs the payee in: the key's own account only. > [!NOTE] Needs the Payouts category and an app key > Payouts are an [API category](https://www.dropfans.io/developers/concepts/api-categories.md) that is off for every app until we switch it on — ask support. Personal keys can read payouts but get 403 `app_key_required` here. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Responses ### 200 The activation state. | Field | Type | Required | Description | |---|---|---|---| | `provisioned` | boolean | yes | True when the account has a MassPay payee. | | `activated` | boolean | yes | True once the payee finished activation. | | `activationUrl` | string \\| null | | The activation link while not activated; null otherwise. | ```json { "provisioned": true, "activated": false, "activationUrl": "https://payee.masspay.io/activate/…" } ``` ## Errors | Status | Body | When | |---|---|---| | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"Payout methods need the account’s own key — a key an agency minted for a managed account cannot read or change where payouts go.","code":"agency_key_not_allowed"}` | The key was minted by an agency for an account it manages. | | 403 | `{"error":"This endpoint needs a key bound to an app with the payouts API category enabled — personal keys cannot use it.","code":"app_key_required","category":"payouts"}` | A personal key (not bound to an app). Personal keys may read payouts, but not change them. | | 502 | `{"error":"MassPay request failed","code":"masspay_unavailable","provisioned":true,"activated":false,"activationUrl":null}` | MassPay did not answer (503 when unreachable). Retry. | | 400 | `{"error":"This endpoint only acts as the key’s own account — send it without X-Dropfans-Account.","code":"account_header_not_allowed"}` | `X-Dropfans-Account` names another account. An agency can never change where a creator it manages is paid. | | 400 | `{"error":"This endpoint acts as the key’s own account or app — call it without /creators/{creatorId}.","code":"creator_scope_not_allowed"}` | Called under /api/external/v1/creators/{creatorId}/ — this endpoint always acts as the key’s own account, agency or app. | | 403 | `{"error":"This app does not have the payouts API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"payouts"}` | Your app has the Payouts [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off (the default for new apps). | ## 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/v1/payouts/masspay/activation" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/payouts/masspay/activation`, { 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/v1/payouts/masspay/activation", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [Set up the MassPay wallet](https://www.dropfans.io/developers/reference/setup-masspay-wallet.md) · Next: [Request a payout](https://www.dropfans.io/developers/reference/create-payout-request.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # POST /api/external/v1/payouts/requests — Request a payout > Request a payout of the account's **full** available balance — exactly what "Pay out funds" does in the dashboard. - Source: https://www.dropfans.io/developers/reference/create-payout-request - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/payouts/requests` (the unversioned alias). Request a payout of the account's **full** available balance — exactly what "Pay out funds" does in the dashboard. One request is created per side (creator, agency, affiliate, share) that has money and no open request; each is reviewed and paid on its own. There is no amount: partial payouts are not possible. **`Idempotency-Key` header required** (1–255 printable characters, e.g. a UUID per payout you intend). Sending the same key with the same body again returns the original requests (201, with `Idempotent-Replayed: true`) instead of a second payout; the same key with a different body is refused with 409 `idempotency_conflict`. Keys are scoped to the account **and your app** (or, for a personal key, that key), so another app can never replay or block yours. A second call with the same key while the first is still running gets 409 `payout_busy`; retry it and it replays the first one's answer. Retry network errors with the same key. Checked in this order, each with its own `code`: ID check (`kyc_required`) → payouts enabled (`payouts_disabled`) → not a buyer account (`consumer_account`) → a default payout method (`no_method`) → the payout wallet activated if the method is a MassPay wallet (`masspay_not_activated`) → the minimum (`below_minimum`) → no open request (`request_open`) → no payout being sent (`in_flight`). [GET /payouts/summary](https://www.dropfans.io/developers/reference/get-payout-summary.md) reports the same gates up front. The key's own account only: `X-Dropfans-Account` is refused, and so is a key an agency minted for an account it manages (403 `agency_key_not_allowed`) — an agency can never request a payout for a creator it manages. > [!NOTE] Needs the Payouts category and an app key > Payouts are an [API category](https://www.dropfans.io/developers/concepts/api-categories.md) that is off for every app until we switch it on — ask support. Personal keys can read payouts but get 403 `app_key_required` here. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Request body (application/json) Optional — an empty body is fine. | Field | Type | Required | Description | |---|---|---|---| | `note` | string \\| null | | A note for the Dropfans team, shown with the request. | Example — With a note: ```json { "note": "October" } ``` ## Responses ### 201 The requests created (or, on a replay, the original ones). | Field | Type | Required | Description | |---|---|---|---| | `requests` | Payout[] | yes | One payout request per side that had money and no open request (kind `request`). | ```json { "requests": [ { "id": "clxpr0001request", "kind": "request", "status": "requested", "amountCents": 34210, "currency": "USD", "side": "creator", "sides": { "creatorCents": 34210, "agencyCents": 0, "affiliateCents": 0, "shareCents": 0 }, "method": { "type": "MASSPAY", "label": "Payout wallet · SWE" }, "note": "October", "failureMessage": null, "invoiceNumber": null, "requestId": null, "payoutId": null, "createdAt": "2026-10-30T09:12:00.000Z", "initiatedAt": null, "paidAt": null, "failedAt": null, "reversedAt": null, "reversalOfId": null } ] } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"Send an Idempotency-Key header (a unique value per payout request, e.g. a UUID).","code":"idempotency_key_required"}` | No `Idempotency-Key` header (`invalid_idempotency_key` when it is malformed). | | 400 | `{"error":"note must be at most 500 characters","code":"invalid_note"}` | `note` is not a string or too long. | | 400 | `{"error":"Invalid JSON body"}` | The body is not valid JSON. | | 400 | `{"error":"Add a payout method before requesting a payout.","code":"no_method"}` | No default payout method. | | 400 | `{"error":"Activate your payout wallet before requesting a payout.","code":"masspay_not_activated"}` | The default method is a MassPay wallet the payee has not activated yet. | | 400 | `{"error":"Minimum payout is $20.00.","code":"below_minimum"}` | The available balance is below the account’s minimum (or not positive). | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"Verify your identity (KYC) before requesting a payout.","code":"kyc_required"}` | The account has not passed the ID check. | | 403 | `{"error":"Payouts are not enabled on your account yet.","code":"payouts_disabled"}` | Dropfans has paused payouts for the account. | | 403 | `{"error":"Payouts are not available for this account","code":"consumer_account"}` | A buyer account. | | 403 | `{"error":"Payout requests need the account’s own key — a key an agency minted for a managed account cannot request or cancel payouts.","code":"agency_key_not_allowed"}` | The key was minted by an agency for an account it manages. | | 403 | `{"error":"This endpoint needs a key bound to an app with the payouts API category enabled — personal keys cannot use it.","code":"app_key_required","category":"payouts"}` | A personal key (not bound to an app). Personal keys may read payouts, but not change them. | | 409 | `{"error":"You already have a payout request awaiting review.","code":"request_open"}` | Every side with money already has an open request. | | 409 | `{"error":"A payout is already in progress. Try again once it has settled.","code":"in_flight"}` | A payout is pending or processing. | | 409 | `{"error":"Another payout operation is running for this account. Try again in a moment.","code":"payout_busy"}` | Another payout operation holds the account’s payout lock (e.g. a concurrent request). Retry with the same key. | | 409 | `{"error":"This Idempotency-Key was already used with a different request body.","code":"idempotency_conflict"}` | The key was used before with another body. | | 400 | `{"error":"This endpoint only acts as the key’s own account — send it without X-Dropfans-Account.","code":"account_header_not_allowed"}` | `X-Dropfans-Account` names another account. An agency can never request or cancel a payout for a creator it manages. | | 400 | `{"error":"This endpoint acts as the key’s own account or app — call it without /creators/{creatorId}.","code":"creator_scope_not_allowed"}` | Called under /api/external/v1/creators/{creatorId}/ — this endpoint always acts as the key’s own account, agency or app. | | 403 | `{"error":"This app does not have the payouts API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"payouts"}` | Your app has the Payouts [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off (the default for new apps). | ## 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/v1/payouts/requests" \ -H "Authorization: Bearer $DROPFANS_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{"note":"October"}' ``` ### Node ```javascript import { randomUUID } from 'node:crypto'; // Keep the key with your record of this payout and reuse it on retries. const idempotencyKey = randomUUID(); const res = await fetch('https://www.dropfans.io/api/external/v1/payouts/requests', { method: 'POST', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, 'Idempotency-Key': idempotencyKey, 'Content-Type': 'application/json', }, body: JSON.stringify({ note: 'October' }), }); const data = await res.json(); if (!res.ok) throw new Error(`${res.status} ${data.code}: ${data.error}`); console.log(data.requests); ``` ### Python ```python import os, uuid, requests # Keep the key with your record of this payout and reuse it on retries. idempotency_key = str(uuid.uuid4()) res = requests.post( "https://www.dropfans.io/api/external/v1/payouts/requests", headers={ "Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}", "Idempotency-Key": idempotency_key, }, json={"note": "October"}, ) res.raise_for_status() print(res.json()["requests"]) ``` --- Previous: [MassPay wallet activation](https://www.dropfans.io/developers/reference/get-masspay-activation.md) · Next: [Cancel a payout request](https://www.dropfans.io/developers/reference/cancel-payout-request.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # POST /api/external/v1/payouts/requests/{id}/cancel — Cancel a payout request > Cancel one of the account's payout requests while it is still `requested`. - Source: https://www.dropfans.io/developers/reference/cancel-payout-request - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/payouts/requests/{id}/cancel` (the unversioned alias). Cancel one of the account's payout requests while it is still `requested`. Its side can be requested again straight away. Once a request is `approved` or `processing` (Paxum requests are `processing` from the moment they are made — the mass-payout file may already be paid) it is on its way and can no longer be cancelled; nor once it is paid, rejected or cancelled. The answer is then 409 `request_not_cancellable` — so repeating a cancel is harmless. The key's own account only (`X-Dropfans-Account` and agency-minted keys are refused). > [!NOTE] Needs the Payouts category and an app key > Payouts are an [API category](https://www.dropfans.io/developers/concepts/api-categories.md) that is off for every app until we switch it on — ask support. Personal keys can read payouts but get 403 `app_key_required` here. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Path parameters | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | The request id (`kind: "request"`). | ## Responses ### 200 The cancelled request. | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Id of the row. Unique across the three kinds — use it with GET /api/external/v1/payouts/{id}. | | `kind` | `payout` \\| `manual` \\| `request` | yes | `payout`: sent by Dropfans (Paxum, MassPay wallet load, or settled by an admin). `manual`: paid outside the platform and recorded by an admin. `request`: a payout request the account made that has not been fulfilled into a payout yet. | | `status` | `requested` \\| `approved` \\| `pending` \\| `processing` \\| `paid` \\| `failed` \\| `cancelled` \\| `rejected` | yes | Requests: `requested` → `approved` → (`processing` for Paxum, from the moment they are made) → `paid`, or `rejected` / `cancelled`. Payouts: `pending` (in a batch, not sent yet) → `processing` (sent) → `paid` or `failed`; `cancelled` when an attempt was dropped. Manual payouts are always `paid`. | | `amountCents` | integer | yes | Integer **cents**. What is paid out (negative only on a reversal row). | | `currency` | `USD` | yes | Always "USD". | | `side` | string | yes | Which balance it draws from: `creator` (own sales), `agency` (income from managed creators / AI personas), `affiliate` (referral commission), `share` (partner shares of other accounts’ sales), or `combined` (several sides in one transfer — see `sides`). | | `sides` | object | yes | The amount split by side, integer cents. Adds up to `amountCents`. | | `sides.creatorCents` | integer | yes | Creator side, cents. | | `sides.agencyCents` | integer | yes | Agency side, cents. | | `sides.affiliateCents` | integer | yes | Referral commission, cents. | | `sides.shareCents` | integer | yes | Partner shares, cents. | | `method` | object \\| null | | The destination frozen when the payout or request was made, masked. Null for manual payouts. | | `method.type` | `PAXUM_P2P` \\| `MASSPAY` \\| `BANK_SWIFT` | yes | Provider method type. | | `method.label` | string | yes | Display label — the creator’s nickname, or e.g. "Paxum · j***@example.com", "Payout wallet · SWE", "Bank transfer · ••••1234". Never a full e-mail, IBAN or token. | | `note` | string \\| null | | Requests: the note sent with the request. Manual payouts: the note the admin recorded for the payee. Otherwise null. | | `failureMessage` | string \\| null | | Why a `failed` payout failed, as reported by the provider. Null unless `status` is `failed`. | | `invoiceNumber` | integer \\| null | | Payout invoice number once the invoice exists. The invoice itself is in the dashboard. | | `requestId` | string \\| null | | kind `payout` / `manual`: the request this payout fulfilled, if any. | | `payoutId` | string \\| null | | kind `request`: the payout or manual payout that fulfilled it, if any. | | `createdAt` | string | yes | When the payout was created or the request made. The list is ordered by this. | | `initiatedAt` | string \\| null | | When the payout was sent to the provider. | | `paidAt` | string \\| null | | When it was paid. Null unless `status` is `paid` (requests carry it on the fulfilling payout instead). | | `failedAt` | string \\| null | | When it failed. Null unless `status` is `failed`. | | `reversedAt` | string \\| null | | Set on a paid wallet load that was sent back (spendback): the money is available again. Its negative twin has `reversalOfId`. Both are hidden from GET /payouts unless includeReversed=true. | | `reversalOfId` | string \\| null | | Set on the negative row that reverses the payout with this id. | ```json { "id": "clxpr0001request", "kind": "request", "status": "cancelled", "amountCents": 34210, "currency": "USD", "side": "creator", "sides": { "creatorCents": 34210, "agencyCents": 0, "affiliateCents": 0, "shareCents": 0 }, "method": { "type": "MASSPAY", "label": "Payout wallet · SWE" }, "note": "October", "failureMessage": null, "invoiceNumber": null, "requestId": null, "payoutId": null, "createdAt": "2026-10-30T09:12:00.000Z", "initiatedAt": null, "paidAt": null, "failedAt": null, "reversedAt": null, "reversalOfId": null } ``` ## Errors | Status | Body | When | |---|---|---| | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"Payout requests need the account’s own key — a key an agency minted for a managed account cannot request or cancel payouts.","code":"agency_key_not_allowed"}` | The key was minted by an agency for an account it manages. | | 403 | `{"error":"This endpoint needs a key bound to an app with the payouts API category enabled — personal keys cannot use it.","code":"app_key_required","category":"payouts"}` | A personal key (not bound to an app). Personal keys may read payouts, but not change them. | | 404 | `{"error":"Not found","code":"not_found"}` | Unknown id, or it belongs to another account. | | 409 | `{"error":"This request is already being paid out and can no longer be cancelled.","code":"request_not_cancellable"}` | Approved or processing (on its way), or already paid, rejected or cancelled. | | 400 | `{"error":"This endpoint only acts as the key’s own account — send it without X-Dropfans-Account.","code":"account_header_not_allowed"}` | `X-Dropfans-Account` names another account. An agency can never request or cancel a payout for a creator it manages. | | 400 | `{"error":"This endpoint acts as the key’s own account or app — call it without /creators/{creatorId}.","code":"creator_scope_not_allowed"}` | Called under /api/external/v1/creators/{creatorId}/ — this endpoint always acts as the key’s own account, agency or app. | | 403 | `{"error":"This app does not have the payouts API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"payouts"}` | Your app has the Payouts [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off (the default for new apps). | ## 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/v1/payouts/requests/$ID/cancel" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const id = '…'; // from an earlier response const res = await fetch(`https://www.dropfans.io/api/external/v1/payouts/requests/${id}/cancel`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`, }, }); console.log(await res.json()); ``` ### Python ```python import os import requests id = "…" # from an earlier response res = requests.post( f"https://www.dropfans.io/api/external/v1/payouts/requests/{id}/cancel", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [Request a payout](https://www.dropfans.io/developers/reference/create-payout-request.md) · Next: [Get your app](https://www.dropfans.io/developers/reference/get-crm-app.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/v1/crm/app — Get your app > Your app as we know it: name, logo, status, commission terms and which API categories are switched on. Always the app the key is bound to. - Source: https://www.dropfans.io/developers/reference/get-crm-app - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/crm/app` (the unversioned alias). Your app as we know it: name, logo, status, commission terms and which API categories are switched on. Always the app the key is bound to. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Responses ### 200 Your app. | Field | Type | Required | Description | |---|---|---|---| | `id` | string | yes | Your app’s id. | | `name` | string | yes | App name, as creators see it. | | `logoUrl` | string \\| null | | App logo, or null. | | `status` | `approved` | yes | Always `approved` here: a suspended app’s keys get 403 `app_suspended` on every endpoint. | | `commission` | object | yes | Your commission terms. | | `commission.rate` | number | yes | Share of each sale (excl. tax) booked for your app owner, as a fraction: 0.05 = 5%. | | `commission.types` | string[] | yes | Sale types that earn commission. | | `apiCategories` | ApiCategoryState[] | yes | Your app’s API category switches: which parts of the API your keys may call (see /developers/concepts/api-categories). Added 2026-11-01. | ```json { "id": "clxap0001app", "name": "Acme CRM", "logoUrl": "https://cdn.example.com/acme/logo.png", "status": "approved", "commission": { "rate": 0.05, "types": [ "drop", "tip", "subscription" ] }, "apiCategories": [ { "id": "account", "label": "Account", "enabled": true, "locked": true }, { "id": "payouts", "label": "Payouts", "enabled": false, "locked": false } ] } ``` ## Errors | Status | Body | When | |---|---|---| | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"This endpoint needs a key bound to an approved app.","code":"app_key_required"}` | A Personal key was used. | | 403 | `{"error":"Use the app owner's key for this endpoint.","code":"owner_key_required"}` | The key was minted by a creator for themselves. App-level calls need a key held by your app owner: their own, or one their agency minted for its personas or managed creators. | | 400 | `{"error":"This endpoint only acts as the key’s own account — send it without X-Dropfans-Account.","code":"account_header_not_allowed"}` | `X-Dropfans-Account` was sent. These endpoints always answer for the key’s app. | | 400 | `{"error":"This endpoint acts as the key’s own account or app — call it without /creators/{creatorId}.","code":"creator_scope_not_allowed"}` | Called under /api/external/v1/creators/{creatorId}/ — this endpoint always acts as the key’s own account, agency or app. | | 403 | `{"error":"This app does not have the crm API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"crm"}` | Your app has the CRM dashboard [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/crm/app" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/crm/app`, { 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/v1/crm/app", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [Cancel a payout request](https://www.dropfans.io/developers/reference/cancel-payout-request.md) · Next: [Dashboard stats](https://www.dropfans.io/developers/reference/get-crm-stats.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/v1/crm/stats — Dashboard stats > Your integration dashboard’s Dashboard tab as data: revenue, booked commission, sales and new accounts, as totals plus zero-filled hour, day, week or month buckets. - Source: https://www.dropfans.io/developers/reference/get-crm-stats - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/crm/stats` (the unversioned alias). Your integration dashboard’s Dashboard tab as data: revenue, booked commission, sales and new accounts, as totals plus zero-filled hour, day, week or month buckets. It returns revenue through your app’s links, the commission booked for your app owner, sales and newly connected accounts. The numbers are the dashboard’s own: same sales, same day boundaries (Europe/Stockholm unless you pass `tz`), same automatic bucket size. Amounts are integer **US cents, excluding tax, before our platform fee**; refunded and charged-back sales are left out. `earningsCents` is the commission **booked** on those sales — it is paid out with your app owner’s earnings, so booked is not the same as already paid. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Query parameters | Field | Type | Required | Description | |---|---|---|---| | `startDate` | string | yes | First day, YYYY-MM-DD (inclusive, in `tz`). | | `endDate` | string | yes | Last day, YYYY-MM-DD (inclusive, in `tz`). At most 366 days after startDate. | | `tz` | string | | IANA time zone the calendar days (and buckets) are cut in. Defaults to Europe/Stockholm, like your dashboard. | | `granularity` | `hour` \\| `day` \\| `week` \\| `month` | | Bucket size. Leave it out for the dashboard’s automatic pick (hour for one day, day up to 30 days, week up to 90, month beyond). `hour` is allowed for up to 3 days. | ## Responses ### 200 Totals and the bucket series. | Field | Type | Required | Description | |---|---|---|---| | `startDate` | string | yes | As requested. | | `endDate` | string | yes | As requested. | | `tz` | string | yes | The zone days and buckets are cut in. | | `granularity` | `hour` \\| `day` \\| `week` \\| `month` | yes | Bucket size used (the one you asked for, or the automatic pick). | | `currency` | `USD` | yes | Currency of every *Cents field. | | `totals` | object | yes | The whole range. Counts equal the sum of `series`; cent amounts are rounded once over the range, so they can differ from the summed buckets by a cent. | | `totals.revenueCents` | integer | yes | US cents, excl. tax, before our platform fee; reversals left out. | | `totals.earningsCents` | integer | yes | Commission booked for your app owner, US cents. | | `totals.transactions` | integer | yes | Sales in the range. | | `totals.newConnectedAccounts` | integer | yes | Accounts first connected in the range. | | `totals.connectedAccountsNow` | integer | yes | Accounts connected to your app right now (your app generated their links most recently). Not range-bound. | | `series` | CrmStatsBucket[] | yes | Every bucket of the range, oldest first, zero-filled. | ```json { "startDate": "2026-10-27", "endDate": "2026-10-29", "tz": "Europe/Stockholm", "granularity": "day", "currency": "USD", "totals": { "revenueCents": 6000, "earningsCents": 300, "transactions": 4, "newConnectedAccounts": 1, "connectedAccountsNow": 18 }, "series": [ { "bucket": "2026-10-27", "start": "2026-10-26T23:00:00.000Z", "revenueCents": 0, "earningsCents": 0, "transactions": 0, "newConnectedAccounts": 0 }, { "bucket": "2026-10-28", "start": "2026-10-27T23:00:00.000Z", "revenueCents": 4500, "earningsCents": 225, "transactions": 3, "newConnectedAccounts": 1 }, { "bucket": "2026-10-29", "start": "2026-10-28T23:00:00.000Z", "revenueCents": 1500, "earningsCents": 75, "transactions": 1, "newConnectedAccounts": 0 } ] } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"startDate must be a real calendar day, YYYY-MM-DD.","code":"invalid_input","field":"startDate"}` | `startDate` / `endDate` missing (stats) or not a YYYY-MM-DD day. | | 400 | `{"error":"endDate must be on or after startDate.","code":"invalid_input","field":"endDate"}` | `endDate` is before `startDate`. | | 400 | `{"error":"The range can be at most 366 days.","code":"invalid_input","field":"endDate"}` | More than 366 days. | | 400 | `{"error":"tz must be an IANA time zone, e.g. Europe/Stockholm or America/New_York.","code":"invalid_input","field":"tz"}` | Unknown `tz`. | | 400 | `{"error":"granularity=hour is only allowed for ranges of up to 3 days.","code":"invalid_input","field":"granularity"}` | `granularity=hour` over more than 3 days, or an unknown granularity. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"This endpoint needs a key bound to an approved app.","code":"app_key_required"}` | A Personal key was used. | | 403 | `{"error":"Use the app owner's key for this endpoint.","code":"owner_key_required"}` | The key was minted by a creator for themselves. App-level calls need a key held by your app owner: their own, or one their agency minted for its personas or managed creators. | | 400 | `{"error":"This endpoint only acts as the key’s own account — send it without X-Dropfans-Account.","code":"account_header_not_allowed"}` | `X-Dropfans-Account` was sent. These endpoints always answer for the key’s app. | | 400 | `{"error":"This endpoint acts as the key’s own account or app — call it without /creators/{creatorId}.","code":"creator_scope_not_allowed"}` | Called under /api/external/v1/creators/{creatorId}/ — this endpoint always acts as the key’s own account, agency or app. | | 403 | `{"error":"This app does not have the crm API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"crm"}` | Your app has the CRM dashboard [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## Notes Buckets are local to `tz`: on a day the clocks go forward there are 23 hourly buckets; on a day they go back the repeated hour is a single bucket (24). Each bucket’s `start` is its exact UTC instant. ## 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/v1/crm/stats?startDate=2026-10-01&endDate=2026-10-30&tz=America%2FNew_York" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/crm/stats?startDate=2026-10-01&endDate=2026-10-30&tz=America%2FNew_York`, { 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/v1/crm/stats", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, params={"startDate": "2026-10-01", "endDate": "2026-10-30", "tz": "America/New_York"}, ) print(res.json()) ``` --- Previous: [Get your app](https://www.dropfans.io/developers/reference/get-crm-app.md) · Next: [List sales through your app](https://www.dropfans.io/developers/reference/list-crm-transactions.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/v1/crm/transactions — List sales through your app > Your integration dashboard’s Transactions tab: every sale through your app’s links across all your accounts, newest first, with the commission booked on each. - Source: https://www.dropfans.io/developers/reference/list-crm-transactions - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/crm/transactions` (the unversioned alias). Your integration dashboard’s Transactions tab: every sale through your app’s links across all your accounts, newest first, with the commission booked on each. Drop sales, tips and subscription payments are all included. Refunded and charged-back sales are included by default with their `status` (like the dashboard); pass `includeReversed=false` to leave them out. Window by exact instants (`since` / `until`) or by calendar days (`startDate` / `endDate` in `tz`), not both. Page with `cursor`. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Query parameters | Field | Type | Required | Description | |---|---|---|---| | `since` | string | | Only sales paid at or after this instant (ISO 8601). | | `until` | string | | Only sales paid at or before this instant (ISO 8601). | | `startDate` | string | | Instead of since/until: first day, YYYY-MM-DD, in `tz`. Needs endDate. | | `endDate` | string | | Last day, YYYY-MM-DD, in `tz` (at most 366 days). | | `tz` | string | | IANA time zone the calendar days (and buckets) are cut in. Defaults to Europe/Stockholm, like your dashboard. | | `type` | string | | Comma-separated subset of `drop,tip,subscription`. Default: all. | | `includeReversed` | boolean | | `false` leaves refunded and charged-back sales out. | | `limit` | integer | | Page size (clamped to the range). | | `cursor` | string | | `nextCursor` from the previous page, unchanged. Keep the other parameters the same. | ## Responses ### 200 A page of sales. | Field | Type | Required | Description | |---|---|---|---| | `currency` | `USD` | yes | Currency of every *Cents field. | | `transactions` | CrmTransaction[] | yes | Newest first. | | `nextCursor` | string \\| null | | Pass as `cursor` for the next page; null on the last page. | ```json { "currency": "USD", "transactions": [ { "id": "clxor0001ord", "type": "drop", "paidAt": "2026-10-29T18:42:11.000Z", "transactionId": "cb_8812734", "linkId": "k3v9x2m1qa", "amountCents": 1500, "crmEarnedCents": 75, "status": "paid", "creator": { "id": "clxac0001acct", "username": "valeria", "name": "Valeria" } } ], "nextCursor": "MjAyNi0xMC0yOVQxODo0MjoxMS4wMDBafGNseG9yMDAwMW9yZA" } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"Use since/until or startDate/endDate, not both.","code":"invalid_input","field":"since"}` | Both windows were sent. | | 400 | `{"error":"since must be an ISO 8601 date or date-time.","code":"invalid_input","field":"since"}` | Unparseable `since` / `until`, or `until` before `since`. | | 400 | `{"error":"startDate must be a real calendar day, YYYY-MM-DD.","code":"invalid_input","field":"startDate"}` | `startDate` / `endDate` missing (stats) or not a YYYY-MM-DD day. | | 400 | `{"error":"endDate must be on or after startDate.","code":"invalid_input","field":"endDate"}` | `endDate` is before `startDate`. | | 400 | `{"error":"The range can be at most 366 days.","code":"invalid_input","field":"endDate"}` | More than 366 days. | | 400 | `{"error":"tz must be an IANA time zone, e.g. Europe/Stockholm or America/New_York.","code":"invalid_input","field":"tz"}` | Unknown `tz`. | | 400 | `{"error":"type must be a comma-separated subset of drop,tip,subscription.","code":"invalid_input","field":"type"}` | Unknown `type`. | | 400 | `{"error":"includeReversed must be true or false.","code":"invalid_input","field":"includeReversed"}` | Anything else. | | 400 | `{"error":"Invalid cursor. Pass nextCursor from the previous page unchanged.","code":"invalid_input","field":"cursor"}` | Malformed `cursor`. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"This endpoint needs a key bound to an approved app.","code":"app_key_required"}` | A Personal key was used. | | 403 | `{"error":"Use the app owner's key for this endpoint.","code":"owner_key_required"}` | The key was minted by a creator for themselves. App-level calls need a key held by your app owner: their own, or one their agency minted for its personas or managed creators. | | 400 | `{"error":"This endpoint only acts as the key’s own account — send it without X-Dropfans-Account.","code":"account_header_not_allowed"}` | `X-Dropfans-Account` was sent. These endpoints always answer for the key’s app. | | 400 | `{"error":"This endpoint acts as the key’s own account or app — call it without /creators/{creatorId}.","code":"creator_scope_not_allowed"}` | Called under /api/external/v1/creators/{creatorId}/ — this endpoint always acts as the key’s own account, agency or app. | | 403 | `{"error":"This app does not have the crm API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"crm"}` | Your app has the CRM dashboard [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/crm/transactions?tz=America%2FNew_York&type=drop%2Ctip" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/crm/transactions?tz=America%2FNew_York&type=drop%2Ctip`, { 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/v1/crm/transactions", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, params={"tz": "America/New_York", "type": "drop,tip"}, ) print(res.json()) ``` --- Previous: [Dashboard stats](https://www.dropfans.io/developers/reference/get-crm-stats.md) · Next: [List your connected accounts](https://www.dropfans.io/developers/reference/list-crm-accounts.md) · All pages: [llms.txt](https://www.dropfans.io/developers/llms.txt) --- # GET /api/external/v1/crm/accounts — List your connected accounts > Your integration dashboard’s Accounts tab: every account your app has generated links for, with its all-time sales through your links and the commission booked on them. - Source: https://www.dropfans.io/developers/reference/list-crm-accounts - Section: API reference - OpenAPI: https://www.dropfans.io/developers/openapi.json Also served at `/api/external/crm/accounts` (the unversioned alias). Your integration dashboard’s Accounts tab: every account your app has generated links for, with its all-time sales through your links and the commission booked on them. Accounts that sold through your links without you generating them are listed too. An account is `connected` while your app is the one that generated its links most recently. When another CRM generated them after you, `movedAway.at` says when; which CRM is never disclosed. Agencies are the sum of their `children` (their creators and their own sales as a creator); pass `flat=true` to get only those rows, each with `agencyId`, so that a column sums without double counting. Amounts are integer US cents, excluding tax, before our platform fee; refunded and charged-back sales are left out. ## Authentication `Authorization: Bearer dpfn_...` — an API key generated in the dashboard (Vault → API Connect). By default the key acts for the one account it belongs to. Missing or invalid keys return 401 `{"error":"Unauthorized","code":"unauthorized"}`. ## Query parameters | Field | Type | Required | Description | |---|---|---|---| | `flat` | boolean | | `true`: no nesting — plain accounts plus every agency’s rows, each with `agencyId`; agency sum rows left out. | ## Responses ### 200 Every account, connected first, then by sales through your links. | Field | Type | Required | Description | |---|---|---|---| | `currency` | `USD` | yes | Currency of every *Cents field. | | `accounts` | CrmConnectedAccount[] | yes | Connected first, then by viaLinksCents. | | `totals` | CrmAccountsTotals | yes | Sums over every account. | ```json { "currency": "USD", "accounts": [ { "id": "clxac0001acct", "username": "valeria", "name": "Valeria", "kind": "account", "connected": true, "connectedAt": "2026-10-17T16:05:00.000Z", "movedAway": null, "links": 4, "allTimeGrossCents": 250000, "viaLinksCents": 64000, "crmEarnedCents": 3200 }, { "id": "clxac0002acct", "username": "nora", "name": "Nora", "kind": "account", "connected": false, "connectedAt": "2026-09-02T10:00:00.000Z", "movedAway": { "at": "2026-10-20T08:30:00.000Z" }, "links": 2, "allTimeGrossCents": 90000, "viaLinksCents": 12000, "crmEarnedCents": 600 } ], "totals": { "links": 6, "allTimeGrossCents": 340000, "viaLinksCents": 76000, "crmEarnedCents": 3800 } } ``` ## Errors | Status | Body | When | |---|---|---| | 400 | `{"error":"flat must be true or false.","code":"invalid_input","field":"flat"}` | Anything else. | | 401 | `{"error":"Unauthorized","code":"unauthorized"}` | Missing or invalid API key. | | 403 | `{"error":"This endpoint needs a key bound to an approved app.","code":"app_key_required"}` | A Personal key was used. | | 403 | `{"error":"Use the app owner's key for this endpoint.","code":"owner_key_required"}` | The key was minted by a creator for themselves. App-level calls need a key held by your app owner: their own, or one their agency minted for its personas or managed creators. | | 400 | `{"error":"This endpoint only acts as the key’s own account — send it without X-Dropfans-Account.","code":"account_header_not_allowed"}` | `X-Dropfans-Account` was sent. These endpoints always answer for the key’s app. | | 400 | `{"error":"This endpoint acts as the key’s own account or app — call it without /creators/{creatorId}.","code":"creator_scope_not_allowed"}` | Called under /api/external/v1/creators/{creatorId}/ — this endpoint always acts as the key’s own account, agency or app. | | 403 | `{"error":"This app does not have the crm API category enabled. Contact support to enable it.","code":"api_category_disabled","category":"crm"}` | Your app has the CRM dashboard [API category](https://www.dropfans.io/developers/concepts/api-categories.md) switched off. | ## 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/v1/crm/accounts" \ -H "Authorization: Bearer $DROPFANS_API_KEY" ``` ### Node ```javascript const res = await fetch(`https://www.dropfans.io/api/external/v1/crm/accounts`, { 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/v1/crm/accounts", headers={"Authorization": f"Bearer {os.environ['DROPFANS_API_KEY']}"}, ) print(res.json()) ``` --- Previous: [List sales through your app](https://www.dropfans.io/developers/reference/list-crm-transactions.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-11-01 — API v1 for OAuth apps at api.dropfans.io - New: OAuth 2.0 / OpenID Connect at auth.dropfans.io (authorization code with PKCE S256, client_secret_basic, rotating refresh tokens with reuse detection, revoke, userinfo, discovery). Ask us for a client id and secret for your approved app. See /developers/api-v1/oauth-apps. - New: API v1 at https://api.dropfans.io with OAuth access tokens: account and audience, insights, agencies and their creators (/creators/{creatorUserUuid}/…), vault media and uploads, posts, media links, tracking links and webhooks. /v1 pages with a cursor ({data, nextCursor}); /v0 and unprefixed paths page with page and size ({data, pagination}). See /developers/api-v1/overview and the generated /developers/api-v1/resources. - New: per-account webhook subscriptions for OAuth apps (POST /v1/webhooks/subscriptions), signed t=…,v0=HMAC-SHA256(secret, "t.body"), with POST /v1/webhooks/test. See /developers/api-v1/webhooks. - Chat operations and chat webhook topics are part of API v1 and become available once chat is enabled for your app; until then they answer 404. - The API-key endpoints under /api/external are unchanged. ## 2026-11-01 — Act for a creator with /v1/creators/{creatorId}/…; creator roster; invite list and revoke - Every creator endpoint is now also served at /api/external/v1/creators/{creatorId}/ (for example GET /api/external/v1/creators/{creatorId}/drops). An agency’s own key can use it for every creator and AI persona it manages (same permissions as before); your app owner’s own key can use it for every account your app onboarded with POST /accounts, while that account still holds the key bound to your app. - The X-Dropfans-Account header keeps working but is deprecated: responses to calls that send it carry Deprecation: true and a Link: rel="successor-version" header with the path form. A path and a header naming different accounts is 400 account_mismatch; a creator path on an endpoint that acts as your own account, agency or app (agency, accounts, webhooks, crm, payout writes, splits) is 400 creator_scope_not_allowed. Endpoints that act as your app (accounts, webhooks) now refuse X-Dropfans-Account too (400 account_header_not_allowed). - New GET /api/external/v1/creators and GET /api/external/v1/creators/{id}: the accounts your key can act for, with relationship (managed, ai_persona, onboarded), permissions, active and creatorPath, paged with {data, nextCursor}. - New GET /api/external/v1/agency/invites (the agency’s invites with status pending, accepted, expired or revoked) and DELETE /api/external/v1/agency/invites/{id} (revoke a pending invite). - GET /me adds key.via: self, agency or app. Links generated by an app owner for an account it onboarded are no longer recorded as an agency’s. - openapi.json lists each creator endpoint a second time as a …ForCreator operation under /api/external/v1/creators/{creatorId}/…, and marks the X-Dropfans-Account parameter deprecated. ## 2026-11-01 — Versioned paths: /api/external/v1 - Every endpoint is now also served under /api/external/v1/ (for example GET /api/external/v1/me), and the docs, the OpenAPI spec and the Markdown editions show the /v1 paths. The unversioned /api/external/ paths stay as permanent aliases: same handlers, same responses, nothing to change. ## 2026-11-01 — API categories: a switch per app; Payouts off by default - Every endpoint now belongs to one API category (account, onboarding, agency, vault, drops, posts, links, earnings, payouts, splits, notifications, webhooks, crm), and every app has a switch per category. A call to a category that is off returns 403 api_category_disabled with a category field. See /developers/concepts/api-categories. - Payouts are off for every app, including Dropfans-operated ones, until we switch them on for that app; Onboarding (POST /api/external/accounts) is off for new apps, as creator sign-up was before. Everything else stays on, so other integrations see no change. GET /balance is in the earnings category and keeps working. - Payout requests, payout methods and the MassPay wallet setup are no longer limited to Dropfans-operated integrations: any app with Payouts on can use them, with the same rules as before (the key’s own account only, no agency-minted keys, after the ID check). Personal keys can read payouts but get 403 app_key_required on these writes. The first_party_only answer on them is gone. - Creator sign-up no longer answers 403 capability_missing: an app without Onboarding gets 403 api_category_disabled instead. - GET /api/external/crm/app adds apiCategories, and GET /api/external/me adds key.apiCategories (null for a personal key): each category with enabled and locked. ## 2026-10-31 — Subscription webhooks - New webhook events subscription.paid (every charge — first charge and renewals — plus a $0 trial start with trial: true), subscription.canceled (renewal turned off, access continues until accessUntil) and subscription.ended (access over). subscription.paid carries a per-charge transactionId to dedupe on. See /developers/webhooks/overview. - Apps whose webhook has no event filter (the default: everything) receive them automatically — ignore types you do not handle. PUT /api/external/webhooks accepts them in events. ## 2026-10-31 — Earnings summary, daily agency earnings, media upload sessions - New GET /api/external/earnings/summary: totals for all time, this month and your window, a breakdown by source (drop, tip, subscription) and a zero-filled day or Monday-start week series, in integer cents and any IANA timezone (an unknown zone is a 400). The series always adds up to the window total, which equals GET /api/external/earnings for the same window and timezone. An agency with canViewStats can read a managed account with X-Dropfans-Account. - New GET /api/external/agency/earnings (the agency’s own key): one row per managed creator per day with revenue — creatorId, date, grossCents, netCents, count — across every account the agency may see stats for, paged with a cursor ({data, nextCursor}). - New media upload sessions: POST /api/external/media/uploads {kind, filename, sizeBytes, contentType, folderId?} returns tus credentials for a video, or 3 MB part URLs for an image or audio file (PUT /api/external/media/uploads/{id}/parts/{n}). POST /api/external/media/uploads/{id}/complete creates the vault item and starts moderation (idempotent; 409 missing_parts lists what is missing) and GET /api/external/media/uploads/{id} shows the parts, the vault item and its moderation status. Up to 30 MB per image and 20 MB per audio file — more than the 4 MB a single POST /api/external/vault takes. The existing /vault and /vault/video-upload endpoints are unchanged. - Fix: GET /api/external/earnings no longer counts the first hour of the next day (or drops the last hour) when endDate is a daylight-saving switch day in tz. ## 2026-10-30 — Payouts API; GET /balance adds total and sides - New Payouts endpoints (any key; an agency with canViewStats can read a managed account with X-Dropfans-Account): GET /api/external/payouts/summary (balance per bucket and side in cents, minimum, masked default method, open request, in-flight payout, upcoming releases, eligible + ineligibleReasons), GET /api/external/payouts (the payout history — payouts, manual payouts and open requests — paginated with a cursor, filters since, until, status, kind, includeReversed), GET /api/external/payouts/{id} and GET /api/external/payouts/methods (masked: Paxum e-mail as j***@example.com, bank last 4 only, no tokens). All money in integer cents. - New POST /api/external/payouts/requests (Idempotency-Key header required) requests a payout of the full available balance, one request per side, with the same gates as the dashboard plus the ID check, wallet activation and in-flight checks, each with its own code. Idempotency keys are scoped to the account and your app (a personal key: that key); a concurrent call with the same key gets 409 payout_busy and a retry replays. POST /api/external/payouts/requests/{id}/cancel cancels a request while it is still requested (approved and processing requests are on their way: 409 request_not_cancellable). Both act for the key’s own account only (X-Dropfans-Account is refused, and agency-minted keys get 403 agency_key_not_allowed — agencies cannot request payouts for managed creators) and are limited to Dropfans-operated integrations at launch (403 first_party_only). - GET /api/external/balance adds total (the full payable balance — creator side + agency side + referral commission + partner shares, the same numbers as the Payouts page and a payout request) and sides (total broken down per side). The existing top-level fields are unchanged (creator side, USD dollars), so current integrations keep working. - New payout-method writes, with the same rules as payout requests (the key’s own account only, no agency-minted keys, after the ID check, Dropfans-operated integrations only at launch): POST /api/external/payouts/methods adds a Paxum method (the e-mail must be an active Paxum account, else 422 paxum_account_not_active; repeating an e-mail returns the existing method with Idempotent-Replayed: true), PATCH /api/external/payouts/methods/{id} changes nickname, isDefault or paxumEmail (other fields: 400 unknown_field), DELETE /api/external/payouts/methods/{id} removes one (409 method_in_use while a payout to it is in flight). - New MassPay wallet setup: GET /api/external/payouts/masspay/profile (the live profile + missingFields), POST /api/external/payouts/masspay/profile (creates the payee from the same profile as the dashboard form — or fills in what an existing payee lacks — and attaches the wallet as the default method; returns activationUrl) and GET /api/external/payouts/masspay/activation (activated, plus a fresh activationUrl). Address line 1, city, state / province and postal code are now required, as MassPay needs them to pay out; middle name, mobile number, language, date of birth and address line 2 stay optional. - New webhook events: payout.requested, payout.processing, payout.paid and payout.failed, with the GET /payouts/{id} row plus accountId as data. Apps whose webhook has no event filter (the default) start receiving them now — ignore types you do not handle, or set events with PUT /api/external/webhooks. ## 2026-10-30 — Your integration dashboard, as an API - New CRM endpoints for the app owner’s keys, with the same numbers as Dashboard → Developer: GET /api/external/crm/app, GET /api/external/crm/stats (totals + zero-filled hour / day / week / month buckets, any IANA tz), GET /api/external/crm/transactions (every sale through your app, paginated) and GET /api/external/crm/accounts (connected accounts, all-time). Amounts are integer US cents, excl. tax, before our platform fee. - An account that moved to another CRM shows movedAway (when), never which CRM — in the API and in your dashboard. ## 2026-10-29 — Link ids; commission on sales through your links, for life - New: link ids. POST /api/external/links creates a link for a drop, a tip (fixed amount or the fan picks), a subscription or the profile, with your externalRef. The link is just https://www.dropfans.io/buy/{id} (startapp={id} in Telegram); everything else is stored with the id. GET /api/external/links adds links.profile / links.tip / links.subscribe. - A drop bought, a tip sent or a subscription started through your app’s links earns your app owner commission for life (every renewal of that subscription too), by default 5%. Generating links makes your app the account’s connected CRM. - Orders, tips, the earnings ledger and the drop.paid / tip.paid webhooks gain linkId. - Called with an app key, POST /drops returns buyUrl as your app’s link id (/buy/{linkId}, plus linkId); the read endpoints return that link when it exists and never create one. Personal keys still get /buy/{productId}. - A drop your app created with its key counts as yours however it is bought: its plain /buy/{productId} and b_{productId} links are links your app generated. - No commission when your app’s owner is the managing agency of the selling creator — the agency share already pays for that sale. - The web / telegram templates are deprecated but unchanged and keep working. A template sale with an externalRef counts for a CRM only when it is the only app that has ever generated links for the account, its own key works the account (directly or through its agency), and the account has no personal key. Links created with a personal key never earn commission. ## 2026-10-28 — Referral commission is CRM-only; subscription period cards; paginated earnings ledger - New GET /api/external/earnings/transactions: every drop sale, tip and subscription payment, paginated with a cursor (limit up to 200, no total cap), with netAmountCents, grossAmountCents, platformFeeCents, taxCents and totalChargedCents per transaction. Filters: since, until, type, includeReversed. GET /api/external/earnings is unchanged (its transactions stay the newest 50). - Orders (GET /api/external/orders, check-status, drop.paid / drop.refunded) and tips (GET /api/external/tips, tip.paid / tip.refunded) now also carry netAmountCents, platformFeeCents and taxCents. Additive — existing fields are unchanged. - Referral commission is now only for CRM apps: accounts created through POST /api/external/accounts earn the app owner 5% of their sales for their first 12 months, and drops created with a CRM app’s keys keep earning 5% on every sale. The creator referral link program has ended. Nothing changes in the API (referralCredited is unchanged). - GET /api/external/subscription now returns plans (the subscription period cards, left to right), and PUT accepts plans: [{ period: WEEK|MONTH|MONTH_3|MONTH_6|LIFETIME, priceUsd, promoPriceUsd?, trialDays? }]. The single priceUsd + interval form still works and saves one card. ## 2026-10-27 — Lucky Wheel retired - The Lucky Wheel feature has been removed. GET /api/external/links no longer returns telegram.spin, and POST /api/external/wheel/grant now returns 404. Old t.me w_ links open the creator’s profile. ## 2026-10-26 — Agencies: one key for every creator - Account targeting: a verified agency’s own key can act for any creator or AI persona it manages by sending X-Dropfans-Account: on any creator endpoint. Checked on every request (accepted management, nobody suspended, banned or deleted, plus the endpoint’s permission). New errors: 400 invalid_account_header, 403 account_not_managed, 403 permission_denied. Without the header nothing changes. - Every response now carries an X-Dropfans-Account header with the id of the account the request acted on. GET /me adds key.viaAgency, and key.managedBy names the agency when the header is used. Rate limits stay per key. - New Agencies endpoints (the agency’s own key, without the header): GET /api/external/agency/creators, POST /api/external/agency/personas, PATCH and DELETE /api/external/agency/personas/{id}, POST /api/external/agency/personas/{id}/reactivate, POST /api/external/agency/invites, DELETE /api/external/agency/creators/{id} and GET /api/external/agency/stats. - New creator endpoints (own key or with the header): GET/PATCH /api/external/profile, GET /api/external/drops (list), PATCH and DELETE /api/external/drops/{id} and GET/PUT /api/external/subscription. - Webhooks: an app holding an agency’s own key also receives events for every creator that agency manages; those payloads carry data.agencyId next to data.accountId. An app never gets the same event twice. ## 2026-10-23 — Referral commission on drops a CRM app creates - Drops created with a CRM app’s keys (apps with CRM sign-up enabled) earn the app owner the 5% referral commission on every sale of that drop, with no time limit. On those sales it replaces the creator’s own referrer. ## 2026-10-23 — Per-fan references on tips - Tip links can carry your per-fan reference: ?tip={usd}&m={externalRef} on the web, startapp=tm_{cents}_{externalRef}_{username} in Telegram ({cents} 0 = the fan picks; there the reference is 1–20 of A-Z a-z 0-9 - with no underscore). GET /links returns tipWithRefTemplate for both. - New webhook events: tip.paid and tip.refunded, with externalRef and the fan’s message. - New endpoint: GET /api/external/tips — paid tips with externalRef, message, tipper and status; filter by externalRef or since. ## 2026-10-17 — Per-fan references, webhooks and creator sign-up - Per-fan checkout references: share ONE drop with many fans. Add ?m={externalRef} to the buy link (Telegram: startapp=b_{productId}_{externalRef}); the reference comes back on every paid order. GET /links returns buyWithRefTemplate for both. - New endpoint: GET /api/external/orders — paid drop orders with externalRef, buyer and status; filter by productId, externalRef or since. - POST /drops/check-status now also returns orders — every paid order per product (the existing sales map is unchanged). - Webhooks are live: drop.paid, drop.refunded and account.kyc_updated, HMAC-signed (DropFans-Signature) with retries. Configure with PUT /api/external/webhooks using a key the app owner holds. - Creator sign-up for approved CRM apps: POST /api/external/accounts creates a creator account with profile, residency, images and subscription price, returns the ID-check link and a key for the account, and credits the referral commission to the app owner. ## 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: [List your connected accounts](https://www.dropfans.io/developers/reference/list-crm-accounts.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/v1/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/v1/vault) and only use items whose moderationStatus is APPROVED. 2. Create a drop with POST /api/external/v1/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/v1/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/v1/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. ```