{"openapi":"3.1.0","info":{"title":"Dropfans API","version":"1.1.0","summary":"Sell drops, manage a creator’s vault, post to the For You feed and read earnings — one dpfn_ API key per creator.","description":"The Dropfans API lets software act on behalf of a creator: list and upload vault media,\npackage it into sellable drops, publish to the For You feed, read earnings and balance,\nand fetch canonical share links.\n\n**Auth**: every request carries `Authorization: Bearer dpfn_...`. A key is bound to exactly\none creator account — there is no context switching. Creators mint keys in their dashboard\n(Vault → API Connect) and choose which app the key is for; to appear in that picker,\napply at https://www.dropfans.io/developers/apply.\n\n**Rate limits**: fixed per-minute and per-day windows per tier — Personal keys\n60/min and 5,000/day, approved apps 300/min and 50,000/day,\nDropfans-operated integrations exempt. Read the live values from the response headers\n(`X-RateLimit-Tier`, `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`,\nplus the `-Day` variants) rather than hardcoding; a 429 carries `Retry-After` and the body\n`{\"error\":\"Rate limit exceeded\",\"code\":\"rate_limited\"}`.\n\n**Moderation parity**: everything created through the API goes through exactly the same\nmoderation as the dashboard — captions run the prohibited-word filter and media stays\nPENDING until it clears the NSFW pipeline. The API cannot bypass review.\n\nHuman docs: https://www.dropfans.io/developers · LLM edition: https://www.dropfans.io/developers/llms.txt","contact":{"email":"support@dropfans.io"},"termsOfService":"https://www.dropfans.io/developers/api-terms"},"servers":[{"url":"https://www.dropfans.io"}],"security":[{"bearerAuth":[]}],"tags":[{"name":"account","description":"Account — Who the key belongs to, timezone and payout balance."},{"name":"vault","description":"Vault — List, upload, organise and tag the creator’s media."},{"name":"drops","description":"Drops — Create sellable drops, attach previews, check sales."},{"name":"posts","description":"Posts — Publish to the For You feed and track moderation."},{"name":"earnings","description":"Earnings — The creator’s dashboard numbers, mirrored."},{"name":"links","description":"Links — Canonical profile, tip, subscribe and buy links."},{"name":"notifications","description":"Telegram (advanced) — Where the creator’s sale notifications go. Rewrites their settings — use with care."}],"externalDocs":{"description":"Full documentation","url":"https://www.dropfans.io/developers"},"paths":{"/api/external/me":{"get":{"operationId":"getMe","tags":["account"],"summary":"Which creator this API key belongs to","description":"Returns the creator account behind the key, plus metadata about the key itself.\n\nCall 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.","responses":{"200":{"description":"The key’s creator account.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Me"},"example":{"id":"clx2f8a1b0001qw3k","username":"valeria","name":"Valeria","image":"https://cdn.dropfans.io/valeria/profile.jpg","accountType":"CREATOR","key":{"name":"KVIQA","app":{"slug":"kviqa","name":"KVIQA"},"tier":"app"}}}}},"401":{"description":"Missing or invalid API key.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorWithCode"},"example":{"error":"Unauthorized","code":"unauthorized"}}}},"403":{"$ref":"#/components/responses/AppSuspended"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl \"https://www.dropfans.io/api/external/me\" \\\n  -H \"Authorization: Bearer $DROPFANS_API_KEY\""},{"lang":"JavaScript","label":"Node","source":"const res = await fetch(`https://www.dropfans.io/api/external/me`, {\n  headers: {\n    Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`,\n  },\n});\nconsole.log(await res.json());"},{"lang":"Python","source":"import os\nimport requests\n\nres = requests.get(\n    \"https://www.dropfans.io/api/external/me\",\n    headers={\"Authorization\": f\"Bearer {os.environ['DROPFANS_API_KEY']}\"},\n)\nprint(res.json())"}],"x-dropfans-docs":"https://www.dropfans.io/developers/reference/get-me"}},"/api/external/timezone":{"get":{"operationId":"getTimezone","tags":["account"],"summary":"Read the creator’s timezone","description":"Returns the creator's IANA timezone, defaulting to `\"UTC\"` when they never set one.\n\nUse it as the `tz` parameter on [GET /api/external/earnings](/developers/reference/get-earnings) so your day buckets line up with what the creator sees on their own dashboard.","responses":{"200":{"description":"The creator’s timezone.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Timezone"},"example":{"timezone":"Europe/Stockholm"}}}},"401":{"description":"Missing or invalid API key.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorWithCode"},"example":{"error":"Unauthorized","code":"unauthorized"}}}},"403":{"$ref":"#/components/responses/AppSuspended"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl \"https://www.dropfans.io/api/external/timezone\" \\\n  -H \"Authorization: Bearer $DROPFANS_API_KEY\""},{"lang":"JavaScript","label":"Node","source":"const res = await fetch(`https://www.dropfans.io/api/external/timezone`, {\n  headers: {\n    Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`,\n  },\n});\nconsole.log(await res.json());"},{"lang":"Python","source":"import os\nimport requests\n\nres = requests.get(\n    \"https://www.dropfans.io/api/external/timezone\",\n    headers={\"Authorization\": f\"Bearer {os.environ['DROPFANS_API_KEY']}\"},\n)\nprint(res.json())"}],"x-dropfans-docs":"https://www.dropfans.io/developers/reference/get-timezone"},"put":{"operationId":"updateTimezone","tags":["account"],"summary":"Set the creator’s timezone","description":"Sets the creator's timezone — the same setting their dashboard uses for day bucketing.\n\nThe 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.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"timezone":{"type":"string","description":"IANA timezone name, e.g. \"Europe/Stockholm\". Abbreviations like \"CET\" are not accepted.","format":"iana-timezone","example":"Europe/Stockholm"}},"required":["timezone"]},"examples":{"stockholm":{"summary":"Set to Stockholm time","value":{"timezone":"Europe/Stockholm"}}}}}},"responses":{"200":{"description":"The saved timezone, echoed.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Timezone"},"example":{"timezone":"Europe/Stockholm"}}}},"400":{"description":"Missing, non-string, or not a recognised IANA timezone name. Also: The request body is not valid JSON.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Invalid timezone"}}}},"401":{"description":"Missing or invalid API key.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorWithCode"},"example":{"error":"Unauthorized","code":"unauthorized"}}}},"403":{"$ref":"#/components/responses/AppSuspended"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl -X PUT \"https://www.dropfans.io/api/external/timezone\" \\\n  -H \"Authorization: Bearer $DROPFANS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n  \"timezone\": \"Europe/Stockholm\"\n}'"},{"lang":"JavaScript","label":"Node","source":"const res = await fetch(`https://www.dropfans.io/api/external/timezone`, {\n  method: 'PUT',\n  headers: {\n    Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"timezone\": \"Europe/Stockholm\"\n  }),\n});\nconsole.log(await res.json());"},{"lang":"Python","source":"import os\nimport requests\n\nres = requests.put(\n    \"https://www.dropfans.io/api/external/timezone\",\n    headers={\"Authorization\": f\"Bearer {os.environ['DROPFANS_API_KEY']}\"},\n    json={\n        \"timezone\": \"Europe/Stockholm\",\n    },\n)\nprint(res.json())"}],"x-dropfans-docs":"https://www.dropfans.io/developers/reference/update-timezone"}},"/api/external/balance":{"get":{"operationId":"getBalance","tags":["account"],"summary":"The creator’s payout balance","description":"The creator's payout balance buckets — the same numbers their Payouts page shows.\n\n> [!WARNING] Dollars, not cents\n> This endpoint returns USD **dollars**. The earnings and check-status endpoints return **cents**. Do not mix the units.\n\nBucket semantics: `pending` is revenue still inside the chargeback hold window; `available` has cleared the hold and pays out in the next batch; `processing` is inside a payout currently being executed; `paidOut` is the lifetime total already paid. `available` can be negative for agency accounts.","responses":{"200":{"description":"Balance buckets in USD dollars.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Balance"},"example":{"currency":"USD","pending":120.5,"available":342.1,"processing":0,"paidOut":1875}}}},"401":{"description":"Missing or invalid API key.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorWithCode"},"example":{"error":"Unauthorized","code":"unauthorized"}}}},"403":{"$ref":"#/components/responses/AppSuspended"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"description":"Balance computation failed — retry later.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Failed to fetch balance"}}}}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl \"https://www.dropfans.io/api/external/balance\" \\\n  -H \"Authorization: Bearer $DROPFANS_API_KEY\""},{"lang":"JavaScript","label":"Node","source":"const res = await fetch(`https://www.dropfans.io/api/external/balance`, {\n  headers: {\n    Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`,\n  },\n});\nconsole.log(await res.json());"},{"lang":"Python","source":"import os\nimport requests\n\nres = requests.get(\n    \"https://www.dropfans.io/api/external/balance\",\n    headers={\"Authorization\": f\"Bearer {os.environ['DROPFANS_API_KEY']}\"},\n)\nprint(res.json())"}],"x-dropfans-docs":"https://www.dropfans.io/developers/reference/get-balance"}},"/api/external/vault":{"get":{"operationId":"listVault","tags":["vault"],"summary":"List your vault items — the source of media for posts","description":"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).\n\nEvery drop and every MEDIA post is built from vault item ids, so this list is the starting point of almost every flow. Items are newest first. The response also carries all of the creator's folders — but note the folders' `itemCount` here respects the moderation filter, while [GET /api/external/vault/folders](/developers/reference/list-folders) counts every non-hidden item, so the two can disagree for the same folder.","parameters":[{"name":"page","in":"query","required":false,"description":"Page number, 1-based.","schema":{"type":"integer","default":1},"example":1},{"name":"limit","in":"query","required":false,"description":"Items per page. Hard cap 50 — larger values are clamped, not rejected.","schema":{"type":"integer","default":50,"maximum":50}},{"name":"folderId","in":"query","required":false,"description":"`\"all\"` (default), `\"unfiled\"`, or a folder id from the folders list.","schema":{"type":"string"},"example":"all"},{"name":"includePending","in":"query","required":false,"description":"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.","schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"Vault items, folders and pagination.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultListResponse"},"example":{"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}}}},"401":{"description":"Missing or invalid API key.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorWithCode"},"example":{"error":"Unauthorized","code":"unauthorized"}}}},"403":{"$ref":"#/components/responses/AppSuspended"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"description":"Query failed — retry later.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Failed to fetch vault items"}}}}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl \"https://www.dropfans.io/api/external/vault?page=1&folderId=all\" \\\n  -H \"Authorization: Bearer $DROPFANS_API_KEY\""},{"lang":"JavaScript","label":"Node","source":"const res = await fetch(`https://www.dropfans.io/api/external/vault?page=1&folderId=all`, {\n  headers: {\n    Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`,\n  },\n});\nconsole.log(await res.json());"},{"lang":"Python","source":"import os\nimport requests\n\nres = requests.get(\n    \"https://www.dropfans.io/api/external/vault\",\n    headers={\"Authorization\": f\"Bearer {os.environ['DROPFANS_API_KEY']}\"},\n    params={\"page\": 1, \"folderId\": \"all\"},\n)\nprint(res.json())"}],"x-dropfans-docs":"https://www.dropfans.io/developers/reference/list-vault"},"post":{"operationId":"uploadVaultItem","tags":["vault"],"summary":"Upload an image, small video or voice message","description":"Uploads one file into the creator's vault as `multipart/form-data`.\n\nThree kinds, three shapes: **images** need two parts — a pre-compressed `displayFile` and a `thumbnailFile` (both JPEG); **audio** (voice messages) sends one `file` part (≤20MB, ≤60 min); **video** sends one `file` part — but the whole request must stay under ~4MB (a platform body cap rejects bigger requests before the app runs), so for real videos use the three-step TUS flow starting at [POST /api/external/vault/video-upload](/developers/reference/start-video-upload) instead.\n\nEvery 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.","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"fileType":{"type":"string","description":"What you are uploading.","enum":["image","video","audio"]},"originalName":{"type":"string","description":"The file’s name, stored as fileName.","example":"beach-set-01.jpg"},"folderId":{"type":"string","description":"Optional folder to file the item into."},"durationSeconds":{"type":"integer","description":"Audio only — the voice message length in seconds (capped at 3600).","maximum":3600},"displayFile":{"type":"string","contentMediaType":"application/octet-stream","description":"Images only (required): the pre-compressed JPEG display copy."},"thumbnailFile":{"type":"string","contentMediaType":"application/octet-stream","description":"Images only (required): the JPEG thumbnail."},"file":{"type":"string","contentMediaType":"application/octet-stream","description":"Video/audio only (required): the media file. Audio ≤20MB; video effectively ≤~4MB here — use the TUS flow for anything bigger."}},"required":["fileType","originalName"]},"examples":{"image":{"summary":"Image upload (display + thumbnail parts)","value":{"fileType":"image","originalName":"beach-set-01.jpg","displayFile":"@photo.jpg","thumbnailFile":"@photo-thumb.jpg"}},"audio":{"summary":"Voice message","value":{"fileType":"audio","originalName":"voice-note.ogg","durationSeconds":42,"file":"@voice-note.ogg"}}}}}},"responses":{"200":{"description":"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).","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","description":"Always true.","example":true},"item":{"$ref":"#/components/schemas/VaultItemUploaded"}},"required":["success","item"]},"example":{"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"}}}}},"400":{"description":"Either required form field is absent. Also: `fileType` is anything other than image, video or audio. Also: Image upload without both file parts. Also: fileType=video without a `file` part. Also: fileType=audio without a `file` part.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Missing required fields: fileType and originalName"}}}},"401":{"description":"Missing or invalid API key.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorWithCode"},"example":{"error":"Unauthorized","code":"unauthorized"}}}},"403":{"$ref":"#/components/responses/AppSuspended"},"413":{"description":"Audio file over 20MB.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Audio too large"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"description":"Storage or moderation kickoff failed — retry later.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Failed to upload file"}}}}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl -X POST \"https://www.dropfans.io/api/external/vault\" \\\n  -H \"Authorization: Bearer $DROPFANS_API_KEY\" \\\n  -F \"fileType=image\" \\\n  -F \"originalName=beach-set-01.jpg\" \\\n  -F \"displayFile=@photo.jpg\" \\\n  -F \"thumbnailFile=@photo-thumb.jpg\""},{"lang":"JavaScript","label":"Node","source":"import { readFile } from 'node:fs/promises';\n\nconst form = new FormData();\nform.append('fileType', \"image\");\nform.append('originalName', \"beach-set-01.jpg\");\nform.append('displayFile', new Blob([await readFile('photo.jpg')]), 'photo.jpg');\nform.append('thumbnailFile', new Blob([await readFile('photo-thumb.jpg')]), 'photo-thumb.jpg');\n\nconst res = await fetch(`https://www.dropfans.io/api/external/vault`, {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`,\n  },\n  body: form,\n});\nconsole.log(await res.json());"},{"lang":"Python","source":"import os\nimport requests\n\nres = requests.post(\n    \"https://www.dropfans.io/api/external/vault\",\n    headers={\"Authorization\": f\"Bearer {os.environ['DROPFANS_API_KEY']}\"},\n    files={\n        \"displayFile\": open(\"photo.jpg\", \"rb\"),\n        \"thumbnailFile\": open(\"photo-thumb.jpg\", \"rb\"),\n    },\n    data={\n        \"fileType\": \"image\",\n        \"originalName\": \"beach-set-01.jpg\",\n    },\n)\nprint(res.json())"}],"x-dropfans-docs":"https://www.dropfans.io/developers/reference/upload-vault-item"}},"/api/external/vault/{id}":{"delete":{"operationId":"deleteVaultItem","tags":["vault"],"summary":"Delete (hide) a vault item","description":"Soft-deletes a vault item you own: nothing is removed from storage, the item is hidden and disappears from every list.\n\nIdempotent — deleting an already-hidden item is a success, so blind retries are harmless.","parameters":[{"name":"id","in":"path","required":true,"description":"The vault item id.","schema":{"type":"string"},"example":"clxv1a2b30001item"}],"responses":{"200":{"description":"Hidden (or already hidden).","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Success"},"example":{"success":true}}}},"401":{"description":"Missing or invalid API key.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorWithCode"},"example":{"error":"Unauthorized","code":"unauthorized"}}}},"403":{"$ref":"#/components/responses/AppSuspended"},"404":{"description":"No item with that id on this creator’s account (other creators’ ids also 404 — never 403).","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Vault item not found"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"description":"Write failed — retry later.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Failed to delete vault item"}}}}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl -X DELETE \"https://www.dropfans.io/api/external/vault/$VAULT_ITEM_ID\" \\\n  -H \"Authorization: Bearer $DROPFANS_API_KEY\""},{"lang":"JavaScript","label":"Node","source":"const vaultItemId = '…'; // from an earlier response\n\nconst res = await fetch(`https://www.dropfans.io/api/external/vault/${vaultItemId}`, {\n  method: 'DELETE',\n  headers: {\n    Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`,\n  },\n});\nconsole.log(await res.json());"},{"lang":"Python","source":"import os\nimport requests\n\nvault_item_id = \"…\"  # from an earlier response\n\nres = requests.delete(\n    f\"https://www.dropfans.io/api/external/vault/{vault_item_id}\",\n    headers={\"Authorization\": f\"Bearer {os.environ['DROPFANS_API_KEY']}\"},\n)\nprint(res.json())"}],"x-dropfans-docs":"https://www.dropfans.io/developers/reference/delete-vault-item"}},"/api/external/vault/{id}/folder":{"patch":{"operationId":"moveVaultItem","tags":["vault"],"summary":"Move a vault item to a folder","description":"Files a vault item into a folder, or unfiles it back to \"All\".\n\nOmitting `folderId` (or sending `null`, `\"\"`, an empty body — even a malformed body) unfiles the item. That leniency is deliberate: `{}` is the documented unfile signal.","parameters":[{"name":"id","in":"path","required":true,"description":"The vault item id.","schema":{"type":"string"},"example":"clxv1a2b30001item"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"folderId":{"type":["string","null"],"description":"Destination folder id. Omit/null/\"\" to unfile.","example":"clxf0ld3r0001abcd"}}},"examples":{"move":{"summary":"Move into a folder","value":{"folderId":"clxf0ld3r0001abcd"}},"unfile":{"summary":"Move back to All","value":{}}}}}},"responses":{"200":{"description":"Moved.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Success"},"example":{"success":true}}}},"400":{"description":"`folderId` is present but not a string (e.g. a number). Also: The destination is a system folder.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"folderId must be a string"}}}},"401":{"description":"Missing or invalid API key.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorWithCode"},"example":{"error":"Unauthorized","code":"unauthorized"}}}},"403":{"$ref":"#/components/responses/AppSuspended"},"404":{"description":"No such item on this account. Also: No such folder on this account.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Vault item not found"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"description":"Write failed — retry later.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Failed to move vault item"}}}}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl -X PATCH \"https://www.dropfans.io/api/external/vault/$VAULT_ITEM_ID/folder\" \\\n  -H \"Authorization: Bearer $DROPFANS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n  \"folderId\": \"clxf0ld3r0001abcd\"\n}'"},{"lang":"JavaScript","label":"Node","source":"const vaultItemId = '…'; // from an earlier response\n\nconst res = await fetch(`https://www.dropfans.io/api/external/vault/${vaultItemId}/folder`, {\n  method: 'PATCH',\n  headers: {\n    Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"folderId\": \"clxf0ld3r0001abcd\"\n  }),\n});\nconsole.log(await res.json());"},{"lang":"Python","source":"import os\nimport requests\n\nvault_item_id = \"…\"  # from an earlier response\n\nres = requests.patch(\n    f\"https://www.dropfans.io/api/external/vault/{vault_item_id}/folder\",\n    headers={\"Authorization\": f\"Bearer {os.environ['DROPFANS_API_KEY']}\"},\n    json={\n        \"folderId\": \"clxf0ld3r0001abcd\",\n    },\n)\nprint(res.json())"}],"x-dropfans-docs":"https://www.dropfans.io/developers/reference/move-vault-item"}},"/api/external/vault/{id}/tags":{"patch":{"operationId":"setVaultItemTags","tags":["vault"],"summary":"Replace a vault item’s content tags","description":"Replaces (not merges) the content tags on a vault item. Send the full list every time.\n\nTags 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.\n\n> [!NOTE] Field name\n> The body field is `contentTags`, not `tags`.","parameters":[{"name":"id","in":"path","required":true,"description":"The vault item id.","schema":{"type":"string"},"example":"clxv1a2b30001item"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"contentTags":{"type":"array","items":{"type":"string","description":"A tag (longer ones are cut to 64 chars).","maxLength":64},"description":"The complete new tag list (≤50 kept).","maxItems":50}},"required":["contentTags"]},"examples":{"tags":{"summary":"Set two tags","value":{"contentTags":["beach","bikini"]}}}}}},"responses":{"200":{"description":"Stored — `contentTags` echoes the normalised list.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","description":"Always true.","example":true},"contentTags":{"type":"array","items":{"type":"string","description":"A stored tag."},"description":"The tags after trimming, de-duplication and capping."}},"required":["success","contentTags"]},"example":{"success":true,"contentTags":["beach","bikini"]}}}},"400":{"description":"The body is not valid JSON. Also: Missing, not an array, or contains non-strings.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Invalid JSON body"}}}},"401":{"description":"Missing or invalid API key.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorWithCode"},"example":{"error":"Unauthorized","code":"unauthorized"}}}},"403":{"$ref":"#/components/responses/AppSuspended"},"404":{"description":"No such item on this account.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Vault item not found"}}}},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl -X PATCH \"https://www.dropfans.io/api/external/vault/$VAULT_ITEM_ID/tags\" \\\n  -H \"Authorization: Bearer $DROPFANS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n  \"contentTags\": [\n    \"beach\",\n    \"bikini\"\n  ]\n}'"},{"lang":"JavaScript","label":"Node","source":"const vaultItemId = '…'; // from an earlier response\n\nconst res = await fetch(`https://www.dropfans.io/api/external/vault/${vaultItemId}/tags`, {\n  method: 'PATCH',\n  headers: {\n    Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"contentTags\": [\n      \"beach\",\n      \"bikini\"\n    ]\n  }),\n});\nconsole.log(await res.json());"},{"lang":"Python","source":"import os\nimport requests\n\nvault_item_id = \"…\"  # from an earlier response\n\nres = requests.patch(\n    f\"https://www.dropfans.io/api/external/vault/{vault_item_id}/tags\",\n    headers={\"Authorization\": f\"Bearer {os.environ['DROPFANS_API_KEY']}\"},\n    json={\n        \"contentTags\": [\n            \"beach\",\n            \"bikini\",\n        ],\n    },\n)\nprint(res.json())"}],"x-dropfans-docs":"https://www.dropfans.io/developers/reference/set-vault-item-tags"}},"/api/external/vault/folders":{"get":{"operationId":"listFolders","tags":["vault"],"summary":"List vault folders","description":"Every folder the creator has, alphabetically. **Unpaginated** — you always get the full list.\n\n`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).","responses":{"200":{"description":"All folders.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"type":"object","properties":{"folders":{"type":"array","items":{"$ref":"#/components/schemas/Folder"},"description":"Every folder, A→Z."}},"required":["folders"]},"example":{"folders":[{"id":"clxf0ld3r0001abcd","name":"Beach set","itemCount":14}]}}}},"401":{"description":"Missing or invalid API key.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorWithCode"},"example":{"error":"Unauthorized","code":"unauthorized"}}}},"403":{"$ref":"#/components/responses/AppSuspended"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"description":"Query failed — retry later.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Failed to fetch vault folders"}}}}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl \"https://www.dropfans.io/api/external/vault/folders\" \\\n  -H \"Authorization: Bearer $DROPFANS_API_KEY\""},{"lang":"JavaScript","label":"Node","source":"const res = await fetch(`https://www.dropfans.io/api/external/vault/folders`, {\n  headers: {\n    Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`,\n  },\n});\nconsole.log(await res.json());"},{"lang":"Python","source":"import os\nimport requests\n\nres = requests.get(\n    \"https://www.dropfans.io/api/external/vault/folders\",\n    headers={\"Authorization\": f\"Bearer {os.environ['DROPFANS_API_KEY']}\"},\n)\nprint(res.json())"}],"x-dropfans-docs":"https://www.dropfans.io/developers/reference/list-folders"},"post":{"operationId":"createFolder","tags":["vault"],"summary":"Create a vault folder","description":"Creates a folder. Names are unique per creator (case-sensitive) and at most 50 characters.\n\nThe response is the **bare folder object** — not wrapped in `{success}` or `{folder}` like most other write endpoints.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Folder name, trimmed, 1–50 chars, unique per creator.","example":"Beach set","maxLength":50}},"required":["name"]},"examples":{"create":{"summary":"Create a folder","value":{"name":"Beach set"}}}}}},"responses":{"200":{"description":"Created — the bare folder object.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Folder"},"example":{"id":"clxf0ld3r0001abcd","name":"Beach set","itemCount":0}}}},"400":{"description":"The body is not valid JSON. Also: `name` missing or not a string. Also: `name` is whitespace only. Also: Longer than 50 characters.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Invalid JSON body"}}}},"401":{"description":"Missing or invalid API key.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorWithCode"},"example":{"error":"Unauthorized","code":"unauthorized"}}}},"403":{"$ref":"#/components/responses/AppSuspended"},"409":{"description":"Duplicate name (also returned on a create race).","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"A folder with this name already exists"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"description":"Write failed — retry later.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Failed to create vault folder"}}}}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl -X POST \"https://www.dropfans.io/api/external/vault/folders\" \\\n  -H \"Authorization: Bearer $DROPFANS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n  \"name\": \"Beach set\"\n}'"},{"lang":"JavaScript","label":"Node","source":"const res = await fetch(`https://www.dropfans.io/api/external/vault/folders`, {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"name\": \"Beach set\"\n  }),\n});\nconsole.log(await res.json());"},{"lang":"Python","source":"import os\nimport requests\n\nres = requests.post(\n    \"https://www.dropfans.io/api/external/vault/folders\",\n    headers={\"Authorization\": f\"Bearer {os.environ['DROPFANS_API_KEY']}\"},\n    json={\n        \"name\": \"Beach set\",\n    },\n)\nprint(res.json())"}],"x-dropfans-docs":"https://www.dropfans.io/developers/reference/create-folder"}},"/api/external/vault/folders/{folderId}":{"delete":{"operationId":"deleteFolder","tags":["vault"],"summary":"Delete a vault folder","description":"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.","parameters":[{"name":"folderId","in":"path","required":true,"description":"The folder id.","schema":{"type":"string"},"example":"clxf0ld3r0001abcd"}],"responses":{"200":{"description":"Deleted; contained items are now unfiled.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Success"},"example":{"success":true}}}},"400":{"description":"The folder is a system folder.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"This folder is managed automatically and cannot be deleted."}}}},"401":{"description":"Missing or invalid API key.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorWithCode"},"example":{"error":"Unauthorized","code":"unauthorized"}}}},"403":{"$ref":"#/components/responses/AppSuspended"},"404":{"description":"No such folder on this account.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Folder not found"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"description":"Write failed — retry later.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Failed to delete vault folder"}}}}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl -X DELETE \"https://www.dropfans.io/api/external/vault/folders/$FOLDER_ID\" \\\n  -H \"Authorization: Bearer $DROPFANS_API_KEY\""},{"lang":"JavaScript","label":"Node","source":"const folderId = '…'; // from an earlier response\n\nconst res = await fetch(`https://www.dropfans.io/api/external/vault/folders/${folderId}`, {\n  method: 'DELETE',\n  headers: {\n    Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`,\n  },\n});\nconsole.log(await res.json());"},{"lang":"Python","source":"import os\nimport requests\n\nfolder_id = \"…\"  # from an earlier response\n\nres = requests.delete(\n    f\"https://www.dropfans.io/api/external/vault/folders/{folder_id}\",\n    headers={\"Authorization\": f\"Bearer {os.environ['DROPFANS_API_KEY']}\"},\n)\nprint(res.json())"}],"x-dropfans-docs":"https://www.dropfans.io/developers/reference/delete-folder"}},"/api/external/vault/video-upload":{"post":{"operationId":"startVideoUpload","tags":["vault"],"summary":"Start a video upload (step 1 of 3)","description":"Starts a direct-to-CDN video upload and returns presigned TUS credentials.\n\nVideos cannot ride through [POST /api/external/vault](/developers/reference/upload-vault-item) — 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](/developers/reference/complete-video-upload) with the `completionToken`. The full sequence with code is in the [upload guide](/developers/guides/upload-media).\n\nThe CDN API key itself is never exposed — only a sha256 signature valid ~6 hours. The completion token is valid 8 hours.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"originalName":{"type":"string","description":"The video file name, stored as fileName.","example":"teaser.mp4"},"fileSize":{"type":"integer","description":"Optional advisory byte count — obvious oversizes are rejected up front. The authoritative check runs at completion. Max 500MB.","example":52428800,"maximum":524288000}},"required":["originalName"]},"examples":{"start":{"summary":"Start an upload","value":{"originalName":"teaser.mp4","fileSize":52428800}}}}}},"responses":{"200":{"description":"Upload created — feed these credentials to your TUS client.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VideoUploadCredentials"},"example":{"videoId":"c2f7f9e2-1111-4222-b333-4d55e6f7a8b9","tusEndpoint":"https://video.bunnycdn.com/tusupload","libraryId":"572783","signature":"9b2f…64 hex…c1a0","expires":1755640800,"completionToken":"eyJ…"}}}},"400":{"description":"`originalName` absent or blank.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Missing required field: originalName"}}}},"401":{"description":"Missing or invalid API key.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorWithCode"},"example":{"error":"Unauthorized","code":"unauthorized"}}}},"403":{"$ref":"#/components/responses/AppSuspended"},"413":{"description":"The advisory fileSize exceeds 500MB.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Video too large. Max 500MB."}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"description":"CDN video creation failed — retry later.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Could not start the video upload"}}}}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl -X POST \"https://www.dropfans.io/api/external/vault/video-upload\" \\\n  -H \"Authorization: Bearer $DROPFANS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n  \"originalName\": \"teaser.mp4\",\n  \"fileSize\": 52428800\n}'"},{"lang":"JavaScript","label":"Node","source":"const res = await fetch(`https://www.dropfans.io/api/external/vault/video-upload`, {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"originalName\": \"teaser.mp4\",\n    \"fileSize\": 52428800\n  }),\n});\nconsole.log(await res.json());"},{"lang":"Python","source":"import os\nimport requests\n\nres = requests.post(\n    \"https://www.dropfans.io/api/external/vault/video-upload\",\n    headers={\"Authorization\": f\"Bearer {os.environ['DROPFANS_API_KEY']}\"},\n    json={\n        \"originalName\": \"teaser.mp4\",\n        \"fileSize\": 52428800,\n    },\n)\nprint(res.json())"}],"x-dropfans-docs":"https://www.dropfans.io/developers/reference/start-video-upload"}},"/api/external/vault/video-upload/complete":{"post":{"operationId":"completeVideoUpload","tags":["vault"],"summary":"Finish a video upload (step 3 of 3)","description":"Registers a finished TUS upload as a vault item and kicks moderation.\n\n**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.\n\nThe item starts `PENDING` and finalizes asynchronously (the NSFW pipeline plus transcoding). Poll [video-status](/developers/reference/video-status) with the `bunnyStreamId` to know when the video is playable, and the vault list for moderation.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"videoId":{"type":"string","description":"The `videoId` from step 1.","example":"c2f7f9e2-1111-4222-b333-4d55e6f7a8b9"},"originalName":{"type":"string","description":"Same file name you sent in step 1.","example":"teaser.mp4"},"completionToken":{"type":"string","description":"The token from step 1 (valid 8h). Proves this key started the upload."},"folderId":{"type":["string","null"],"description":"Optional folder — silently ignored if you don’t own it."}},"required":["videoId","originalName","completionToken"]},"examples":{"complete":{"summary":"Register the uploaded video","value":{"videoId":"c2f7f9e2-1111-4222-b333-4d55e6f7a8b9","originalName":"teaser.mp4","completionToken":"eyJ…"}}}}}},"responses":{"200":{"description":"Registered (or already registered — idempotent).","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","description":"Always true.","example":true},"item":{"$ref":"#/components/schemas/VideoVaultItem"}},"required":["success","item"]},"example":{"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}}}}},"400":{"description":"Either field absent or blank.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Missing required fields: videoId and originalName"}}}},"401":{"description":"Missing or invalid API key.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorWithCode"},"example":{"error":"Unauthorized","code":"unauthorized"}}}},"403":{"description":"The completionToken is wrong, for another key, or older than 8h — start over from step 1.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Invalid or expired upload token — please retry the upload"}}}},"409":{"description":"The CDN has not finished receiving the bytes — back off and retry the same call.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"The video upload has not finished — please retry."}}}},"413":{"description":"The stored byte count exceeds the cap — the upload is discarded.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Video too large. Max 500MB."}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"description":"Registration failed — retry later.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Failed to register the uploaded video"}}}}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl -X POST \"https://www.dropfans.io/api/external/vault/video-upload/complete\" \\\n  -H \"Authorization: Bearer $DROPFANS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n  \"videoId\": \"c2f7f9e2-1111-4222-b333-4d55e6f7a8b9\",\n  \"originalName\": \"teaser.mp4\",\n  \"completionToken\": \"eyJ…\"\n}'"},{"lang":"JavaScript","label":"Node","source":"// Step 2 happens in your client with a TUS library (npm i tus-js-client),\n// then step 3 registers the finished upload.\nimport * as tus from 'tus-js-client';\nimport { readFile } from 'node:fs/promises';\n\nconst creds = await ( // step 1\n  await fetch('https://www.dropfans.io/api/external/vault/video-upload', {\n    method: 'POST',\n    headers: {\n      Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`,\n      'Content-Type': 'application/json',\n    },\n    body: JSON.stringify({ originalName: 'teaser.mp4' }),\n  })\n).json();\n\nconst file = await readFile('teaser.mp4');\nawait new Promise((resolve, reject) => {   // step 2 — raw bytes straight to the CDN\n  new tus.Upload(file, {\n    endpoint: creds.tusEndpoint,\n    headers: {\n      AuthorizationSignature: creds.signature,\n      AuthorizationExpire: String(creds.expires),\n      VideoId: creds.videoId,\n      LibraryId: creds.libraryId,\n    },\n    metadata: { filetype: 'video/mp4', title: 'teaser.mp4' },\n    onError: reject,\n    onSuccess: resolve,\n  }).start();\n});\n\n// step 3 — 409 means \"not finished yet\": back off and retry the same call\nfor (const waitMs of [0, 2000, 5000, 10000, 30000]) {\n  if (waitMs) await new Promise((r) => setTimeout(r, waitMs));\n  const res = await fetch(\n    'https://www.dropfans.io/api/external/vault/video-upload/complete',\n    {\n      method: 'POST',\n      headers: {\n        Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`,\n        'Content-Type': 'application/json',\n      },\n      body: JSON.stringify({\n        videoId: creds.videoId,\n        originalName: 'teaser.mp4',\n        completionToken: creds.completionToken,\n      }),\n    },\n  );\n  if (res.status !== 409) {\n    console.log(await res.json());\n    break;\n  }\n}"},{"lang":"Python","source":"import os\nimport requests\n\nres = requests.post(\n    \"https://www.dropfans.io/api/external/vault/video-upload/complete\",\n    headers={\"Authorization\": f\"Bearer {os.environ['DROPFANS_API_KEY']}\"},\n    json={\n        \"videoId\": \"c2f7f9e2-1111-4222-b333-4d55e6f7a8b9\",\n        \"originalName\": \"teaser.mp4\",\n        \"completionToken\": \"eyJ…\",\n    },\n)\nprint(res.json())"}],"x-dropfans-docs":"https://www.dropfans.io/developers/reference/complete-video-upload"}},"/api/external/vault/video-status":{"post":{"operationId":"videoStatus","tags":["vault"],"summary":"Check video transcoding status (batch)","description":"Transcoding status + duration for a batch of video GUIDs (`bunnyStreamId` values from your vault items).\n\nA 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.\n\n> [!WARNING] Silent truncation and omission\n> 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\".","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"videoIds":{"type":"array","items":{"type":"string","description":"A Bunny Stream GUID (vault item `bunnyStreamId`)."},"description":"Stream GUIDs to check (≤50 — extras silently dropped). Non-strings are filtered out; an empty array returns {\"statuses\":{}}.","maxItems":50}},"required":["videoIds"]},"examples":{"batch":{"summary":"Check two videos","value":{"videoIds":["c2f7f9e2-1111-4222-b333-4d55e6f7a8b9","0f3d9a11-2222-4333-a444-5e66f7a8b9c0"]}}}}}},"responses":{"200":{"description":"Status map keyed by the GUIDs you sent (unresolvable ids omitted).","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"type":"object","properties":{"statuses":{"$ref":"#/components/schemas/VideoStatusMap"}},"required":["statuses"]},"example":{"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}}}}}},"400":{"description":"The body is not valid JSON.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Invalid JSON body"}}}},"401":{"description":"Missing or invalid API key.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorWithCode"},"example":{"error":"Unauthorized","code":"unauthorized"}}}},"403":{"$ref":"#/components/responses/AppSuspended"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl -X POST \"https://www.dropfans.io/api/external/vault/video-status\" \\\n  -H \"Authorization: Bearer $DROPFANS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n  \"videoIds\": [\n    \"c2f7f9e2-1111-4222-b333-4d55e6f7a8b9\",\n    \"0f3d9a11-2222-4333-a444-5e66f7a8b9c0\"\n  ]\n}'"},{"lang":"JavaScript","label":"Node","source":"const res = await fetch(`https://www.dropfans.io/api/external/vault/video-status`, {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"videoIds\": [\n      \"c2f7f9e2-1111-4222-b333-4d55e6f7a8b9\",\n      \"0f3d9a11-2222-4333-a444-5e66f7a8b9c0\"\n    ]\n  }),\n});\nconsole.log(await res.json());"},{"lang":"Python","source":"import os\nimport requests\n\nres = requests.post(\n    \"https://www.dropfans.io/api/external/vault/video-status\",\n    headers={\"Authorization\": f\"Bearer {os.environ['DROPFANS_API_KEY']}\"},\n    json={\n        \"videoIds\": [\n            \"c2f7f9e2-1111-4222-b333-4d55e6f7a8b9\",\n            \"0f3d9a11-2222-4333-a444-5e66f7a8b9c0\",\n        ],\n    },\n)\nprint(res.json())"}],"x-dropfans-docs":"https://www.dropfans.io/developers/reference/video-status"}},"/api/external/drops":{"post":{"operationId":"createDrop","tags":["drops"],"summary":"Create a sellable drop from vault items","description":"Packages up to 10 vault items into a drop and returns a checkout URL.\n\nPrice is USD **dollars**: either `0` (free) or between $5 and $750. Use APPROVED vault items — the drop inherits its moderation status from its media, so a drop built from approved items is sellable (and attachable to a post) immediately, while one containing PENDING items waits for review. Hand the buyer the returned `buyUrl`, or build a Telegram link from [GET /api/external/links](/developers/reference/get-links)' `telegram.buyTemplate`.\n\n> [!WARNING] `description` is validated but NOT stored\n> 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.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"Drop title, shown at checkout. Falsy values are stored as null.","example":"Beach set — 6 photos"},"description":{"type":"string","description":"Checked for prohibited words, then discarded — never persisted or displayed."},"price":{"type":"number","description":"USD dollars. 0 = free; otherwise $5–$750.","example":25,"minimum":0,"maximum":750},"allowDownload":{"type":"boolean","description":"Whether buyers may download the files after purchase.","default":true},"vaultItemIds":{"type":"array","items":{"type":"string","description":"A vault item you own."},"description":"1–10 vault item ids, in display order. The first becomes the cover.","maxItems":10}},"required":["price","vaultItemIds"]},"examples":{"paidDrop":{"summary":"A $25 three-item drop","value":{"name":"Beach set — 6 photos","price":25,"vaultItemIds":["clxv1a2b30001item","clxv1a2b30002item","clxv1a2b30003item"]}}}}}},"responses":{"200":{"description":"Created. Keep the productId — sales polling, previews and post attachment all key on it.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DropCreated"},"example":{"productId":"clxdr0p000001prod","buyUrl":"https://www.dropfans.io/buy/clxdr0p000001prod","mediaCount":3}}}},"400":{"description":"`price` missing, not a number, NaN or negative. Also: Priced above 0 but below $5. Also: Priced above $750. Also: 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). Also: Missing or empty media list. Also: More than 10 ids. Also: One of the items was deleted (hidden).","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Invalid price"}}}},"401":{"description":"Missing or invalid API key.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorWithCode"},"example":{"error":"Unauthorized","code":"unauthorized"}}}},"403":{"description":"An id belongs to another creator.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"You do not own all selected vault items"}}}},"404":{"description":"An id does not exist.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"One or more vault items not found"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"description":"Write failed — retry later.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Failed to create drop"}}}}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl -X POST \"https://www.dropfans.io/api/external/drops\" \\\n  -H \"Authorization: Bearer $DROPFANS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n  \"name\": \"Beach set — 6 photos\",\n  \"price\": 25,\n  \"vaultItemIds\": [\n    \"clxv1a2b30001item\",\n    \"clxv1a2b30002item\",\n    \"clxv1a2b30003item\"\n  ]\n}'"},{"lang":"JavaScript","label":"Node","source":"const res = await fetch(`https://www.dropfans.io/api/external/drops`, {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"name\": \"Beach set — 6 photos\",\n    \"price\": 25,\n    \"vaultItemIds\": [\n      \"clxv1a2b30001item\",\n      \"clxv1a2b30002item\",\n      \"clxv1a2b30003item\"\n    ]\n  }),\n});\nconsole.log(await res.json());"},{"lang":"Python","source":"import os\nimport requests\n\nres = requests.post(\n    \"https://www.dropfans.io/api/external/drops\",\n    headers={\"Authorization\": f\"Bearer {os.environ['DROPFANS_API_KEY']}\"},\n    json={\n        \"name\": \"Beach set — 6 photos\",\n        \"price\": 25,\n        \"vaultItemIds\": [\n            \"clxv1a2b30001item\",\n            \"clxv1a2b30002item\",\n            \"clxv1a2b30003item\",\n        ],\n    },\n)\nprint(res.json())"}],"x-dropfans-docs":"https://www.dropfans.io/developers/reference/create-drop"}},"/api/external/drops/{id}":{"get":{"operationId":"getDrop","tags":["drops"],"summary":"Read back a drop","description":"One drop you created: price, moderation status (overall and per media item), checkout URL, whether previews are attached, and a sales summary.\n\n`salesCount` counts paid orders **excluding refunds and chargebacks** — stricter than [check-status](/developers/reference/check-drop-status), 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.","parameters":[{"name":"id","in":"path","required":true,"description":"The product (drop) id from create-drop.","schema":{"type":"string"},"example":"clxdr0p000001prod"}],"responses":{"200":{"description":"The drop.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Drop"},"example":{"id":"clxdr0p000001prod","name":"Beach set — 6 photos","price":25,"currency":"USD","status":"APPROVED","moderationReason":null,"buyUrl":"https://www.dropfans.io/buy/clxdr0p000001prod","allowDownload":true,"mediaCount":3,"media":[{"vaultItemId":"clxv1a2b30001item","order":0,"fileType":"image","moderationStatus":"APPROVED","hasPreview":true},{"vaultItemId":"clxv1a2b30002item","order":1,"fileType":"image","moderationStatus":"APPROVED","hasPreview":true},{"vaultItemId":"clxv1a2b30003item","order":2,"fileType":"video","moderationStatus":"APPROVED","hasPreview":false}],"salesCount":2,"lastSaleAt":"2026-08-18T21:03:00.000Z","createdAt":"2026-08-15T09:00:00.000Z"}}}},"401":{"description":"Missing or invalid API key.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorWithCode"},"example":{"error":"Unauthorized","code":"unauthorized"}}}},"403":{"$ref":"#/components/responses/AppSuspended"},"404":{"description":"No such drop on this account (another creator’s drop also 404s — never 403).","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Drop not found"}}}},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl \"https://www.dropfans.io/api/external/drops/$PRODUCT_ID\" \\\n  -H \"Authorization: Bearer $DROPFANS_API_KEY\""},{"lang":"JavaScript","label":"Node","source":"const productId = '…'; // from an earlier response\n\nconst res = await fetch(`https://www.dropfans.io/api/external/drops/${productId}`, {\n  headers: {\n    Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`,\n  },\n});\nconsole.log(await res.json());"},{"lang":"Python","source":"import os\nimport requests\n\nproduct_id = \"…\"  # from an earlier response\n\nres = requests.get(\n    f\"https://www.dropfans.io/api/external/drops/{product_id}\",\n    headers={\"Authorization\": f\"Bearer {os.environ['DROPFANS_API_KEY']}\"},\n)\nprint(res.json())"}],"x-dropfans-docs":"https://www.dropfans.io/developers/reference/get-drop"}},"/api/external/drops/{id}/previews":{"post":{"operationId":"attachDropPreviews","tags":["drops"],"summary":"Attach baked blur previews to a drop","description":"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.\n\nThe body is `multipart/form-data` with one part per media item, named by vault item id: `previewBlob_<vaultItemId>` (the baked JPEG) and optionally `blurMeta_<vaultItemId>` (a descriptor like `\"partial\"`, `\"pixelated:59\"` or `\"full\"`). Without previews, paid media falls back to a generic gaussian blur.\n\n> [!WARNING] Best-effort — skipped items fail silently\n> 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.","parameters":[{"name":"id","in":"path","required":true,"description":"The product (drop) id.","schema":{"type":"string"},"example":"clxdr0p000001prod"}],"requestBody":{"required":true,"description":"Field names embed the vault item id — one previewBlob per media item you want a preview on.","content":{"multipart/form-data":{"schema":{"type":"object","properties":{"previewBlob_<vaultItemId>":{"type":"string","contentMediaType":"application/octet-stream","description":"The baked JPEG teaser for that media item. Must be image/jpeg, ≤8MB."},"blurMeta_<vaultItemId>":{"type":"string","description":"Optional blur descriptor recorded alongside: \"full\", \"partial\" or \"pixelated:<intensity>\".","example":"pixelated:59"}}},"examples":{"previews":{"summary":"Baked previews for two media items","value":{"previewBlob_clxv1a2b30001item":"@preview-1.jpg","blurMeta_clxv1a2b30001item":"partial","previewBlob_clxv1a2b30002item":"@preview-2.jpg","blurMeta_clxv1a2b30002item":"pixelated:59"}}}}}},"responses":{"200":{"description":"`updated` = how many previews were actually stored. If it is lower than the number of parts you sent, the difference was silently skipped.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","description":"Always true.","example":true},"updated":{"type":"integer","description":"Previews stored on this call.","example":2}},"required":["success","updated"]},"example":{"success":true,"updated":2}}}},"401":{"description":"Missing or invalid API key.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorWithCode"},"example":{"error":"Unauthorized","code":"unauthorized"}}}},"403":{"description":"The drop belongs to another creator.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"You do not own this drop"}}}},"404":{"description":"No such drop.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Drop not found"}}}},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"description":"Unexpected failure — retry later.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Failed to attach previews"}}}}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl -X POST \"https://www.dropfans.io/api/external/drops/$PRODUCT_ID/previews\" \\\n  -H \"Authorization: Bearer $DROPFANS_API_KEY\" \\\n  -F \"previewBlob_clxv1a2b30001item=@preview-1.jpg;type=image/jpeg\" \\\n  -F \"blurMeta_clxv1a2b30001item=partial\" \\\n  -F \"previewBlob_clxv1a2b30002item=@preview-2.jpg;type=image/jpeg\" \\\n  -F \"blurMeta_clxv1a2b30002item=pixelated:59\""},{"lang":"JavaScript","label":"Node","source":"import { readFile } from 'node:fs/promises';\n\nconst form = new FormData();\nform.append(\n  'previewBlob_clxv1a2b30001item',\n  new Blob([await readFile('preview-1.jpg')], { type: 'image/jpeg' }),\n  'preview-1.jpg',\n);\nform.append('blurMeta_clxv1a2b30001item', 'partial');\n\nconst res = await fetch(\n  `https://www.dropfans.io/api/external/drops/${productId}/previews`,\n  {\n    method: 'POST',\n    headers: { Authorization: `Bearer ${process.env.DROPFANS_API_KEY}` },\n    body: form,\n  },\n);\nconsole.log(await res.json()); // { success: true, updated: 1 }"},{"lang":"Python","source":"import os, requests\n\nres = requests.post(\n    f\"https://www.dropfans.io/api/external/drops/{product_id}/previews\",\n    headers={\"Authorization\": f\"Bearer {os.environ['DROPFANS_API_KEY']}\"},\n    files={\n        \"previewBlob_clxv1a2b30001item\": (\"preview-1.jpg\", open(\"preview-1.jpg\", \"rb\"), \"image/jpeg\"),\n    },\n    data={\"blurMeta_clxv1a2b30001item\": \"partial\"},\n)\nprint(res.json())  # { \"success\": true, \"updated\": 1 }"}],"x-dropfans-docs":"https://www.dropfans.io/developers/reference/attach-drop-previews"}},"/api/external/drops/check-status":{"post":{"operationId":"checkDropStatus","tags":["drops"],"summary":"Check which drops sold (batch)","description":"Sale info for a batch of product ids — the polling half of sale tracking.\n\nPoll it periodically (every few minutes is plenty) with the productIds you are tracking. Amounts are gross buyer-paid totals in **cents**.\n\n> [!WARNING] Truncation, omission, refunds\n> At most 200 ids are processed per call — extras are **dropped without an error**, so chunk larger lists. Unsold and unknown ids are **omitted** from the response (only `paid: true` entries appear). And unlike earnings, this endpoint does **NOT exclude refunded or charged-back orders** — a refunded sale still reports paid. Reconcile against [GET /api/external/earnings](/developers/reference/get-earnings) for net truth.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"productIds":{"type":"array","items":{"type":"string","description":"A product id from create-drop."},"description":"Product ids to check (≤200 — extras silently dropped). Non-strings filtered out; an empty array returns {\"sales\":{}}.","maxItems":200}},"required":["productIds"]},"examples":{"batch":{"summary":"Check two drops","value":{"productIds":["clxdr0p000001prod","clxdr0p000002prod"]}}}}}},"responses":{"200":{"description":"Sales map — only sold products appear. Multiple paid orders on one product report the most recent.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"type":"object","properties":{"sales":{"$ref":"#/components/schemas/SalesMap"}},"required":["sales"]},"example":{"sales":{"clxdr0p000001prod":{"paid":true,"saleAmountCents":2500,"buyerEmail":"buyer@example.com"}}}}}},"400":{"description":"The body is not valid JSON.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Invalid JSON body"}}}},"401":{"description":"Missing or invalid API key.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorWithCode"},"example":{"error":"Unauthorized","code":"unauthorized"}}}},"403":{"$ref":"#/components/responses/AppSuspended"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"description":"Query failed — retry later.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Failed to check drop status"}}}}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl -X POST \"https://www.dropfans.io/api/external/drops/check-status\" \\\n  -H \"Authorization: Bearer $DROPFANS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n  \"productIds\": [\n    \"clxdr0p000001prod\",\n    \"clxdr0p000002prod\"\n  ]\n}'"},{"lang":"JavaScript","label":"Node","source":"const res = await fetch(`https://www.dropfans.io/api/external/drops/check-status`, {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"productIds\": [\n      \"clxdr0p000001prod\",\n      \"clxdr0p000002prod\"\n    ]\n  }),\n});\nconsole.log(await res.json());"},{"lang":"Python","source":"import os\nimport requests\n\nres = requests.post(\n    \"https://www.dropfans.io/api/external/drops/check-status\",\n    headers={\"Authorization\": f\"Bearer {os.environ['DROPFANS_API_KEY']}\"},\n    json={\n        \"productIds\": [\n            \"clxdr0p000001prod\",\n            \"clxdr0p000002prod\",\n        ],\n    },\n)\nprint(res.json())"}],"x-dropfans-docs":"https://www.dropfans.io/developers/reference/check-drop-status"}},"/api/external/posts":{"post":{"operationId":"createPost","tags":["posts"],"summary":"Publish a post to the For You feed","description":"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.\n\nThree shapes:\n  • TEXT   — caption only.\n  • MEDIA  — a gallery of 1-10 images from your vault, each free or paid.\n  • DROP   — attach an existing approved drop by productId.\n\nFor MEDIA, list your vault first (GET /api/external/vault) and use the id\nof an item whose moderationStatus is APPROVED to publish without waiting.\n\nEverything 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.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"caption":{"type":"string","description":"Post text. Required for kind=TEXT. Silently truncated to 2000 characters.","example":"im 5 min away, wyd?","maxLength":2000},"kind":{"type":"string","description":"Post shape. DROP is not a request value — send productId instead.","enum":["TEXT","MEDIA","SUBSCRIPTION","COMMUNITY"],"default":"TEXT"},"productId":{"type":"string","description":"Attach an existing approved drop. Forces kind=DROP. Must belong to you."},"media":{"type":"array","items":{"type":"object","properties":{"vaultItemId":{"type":"string","description":"A vault item you own (GET /api/external/vault)."},"isPaid":{"type":"boolean","description":"Paid items are shown blurred and mint a PPV unlock. Note: posting via API bakes no custom blur preview — paid items fall back to a gaussian blur.","default":false},"price":{"type":"number","description":"USD. Required when isPaid is true. Minimum $5.","minimum":5},"order":{"type":"integer","description":"Display order. Defaults to array index."}},"required":["vaultItemId"],"description":"One gallery entry."},"description":"Required when kind=MEDIA.","maxItems":10},"scheduledAt":{"type":"string","description":"ISO-8601. At least 1 minute ahead, at most 30 days. Omit to publish now.","format":"date-time"}}},"examples":{"text":{"summary":"Text post","value":{"caption":"im 5 min away, wyd?"}},"mediaFree":{"summary":"Free image from the vault","value":{"kind":"MEDIA","caption":"rate my fit 1-10","media":[{"vaultItemId":"clxv1a2b30001item","isPaid":false,"order":0}]}},"mediaPaid":{"summary":"Free teaser + paid unlock, scheduled","value":{"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":{"description":"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.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"The new post id — poll GET /api/external/posts/{id} with it.","example":"clxp0st000001feed"},"status":{"type":"string","description":"PENDING is normal, not an error: media posts wait on the NSFW pipeline and a flagged caption waits on a human.","enum":["PENDING","APPROVED"]},"pending":{"type":"boolean","description":"Convenience mirror of status === \"PENDING\"."},"scheduledAt":{"type":["string","null"],"description":"Echoed schedule time, or null for immediate posts.","format":"date-time"},"url":{"type":["string","null"],"description":"The creator’s profile URL (where the post appears once live). Null when the creator has no username yet.","format":"uri"}},"required":["id","status","pending"]},"example":{"id":"clxp0st000001feed","status":"PENDING","pending":true,"scheduledAt":null,"url":"https://www.dropfans.io/u/valeria"}}}},"400":{"description":"The body is not valid JSON. Also: kind=MEDIA without media. Also: More than 10 media entries. Also: A media entry lacks vaultItemId (index varies). Also: isPaid without a valid price (index varies). Also: scheduledAt is not a parseable date. Also: scheduledAt is in the past or under a minute ahead. Also: scheduledAt beyond 30 days. Also: kind=TEXT with an empty caption. Also: productId points at a drop still in moderation — check GET /api/external/drops/{id}.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Invalid JSON body"}}}},"401":{"description":"Missing or invalid API key.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorWithCode"},"example":{"error":"Unauthorized","code":"unauthorized"}}}},"403":{"description":"The key belongs to a CONSUMER account.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Only creators can post"}}}},"404":{"description":"productId does not exist on this account.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Drop not found"}}}},"422":{"description":"Caption tripped the prohibited-word filter — rewrite it and retry.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorWithMatchedWord"},"example":{"error":"Your post contains a word that isn’t allowed.","matchedWord":"…"}}}},"429":{"description":"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\").","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"},"Retry-After":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"You’ve reached the posting limit (5 per day). Try again in …"}}}}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl -X POST \"https://www.dropfans.io/api/external/posts\" \\\n  -H \"Authorization: Bearer $DROPFANS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n  \"caption\": \"im 5 min away, wyd?\"\n}'"},{"lang":"JavaScript","label":"Node","source":"const res = await fetch(`https://www.dropfans.io/api/external/posts`, {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"caption\": \"im 5 min away, wyd?\"\n  }),\n});\nconsole.log(await res.json());"},{"lang":"Python","source":"import os\nimport requests\n\nres = requests.post(\n    \"https://www.dropfans.io/api/external/posts\",\n    headers={\"Authorization\": f\"Bearer {os.environ['DROPFANS_API_KEY']}\"},\n    json={\n        \"caption\": \"im 5 min away, wyd?\",\n    },\n)\nprint(res.json())"}],"x-dropfans-docs":"https://www.dropfans.io/developers/reference/create-post"},"get":{"operationId":"listPosts","tags":["posts"],"summary":"List your posts and their moderation status","description":"Your posts, newest first, plus the current posting limits — read `limits` from the response rather than hardcoding numbers.\n\nThe `status` filter takes one moderation state; unrecognised values are silently ignored (you get the unfiltered list, not an error).","parameters":[{"name":"page","in":"query","required":false,"description":"Page number, 1-based.","schema":{"type":"integer","default":1}},{"name":"limit","in":"query","required":false,"description":"Posts per page (default 20, cap 50 — larger values clamped).","schema":{"type":"integer","default":20,"maximum":50}},{"name":"status","in":"query","required":false,"description":"Filter by moderation state (case-insensitive). Unrecognised values are ignored.","schema":{"type":"string","enum":["PENDING","APPROVED","REJECTED","FLAGGED"]}}],"responses":{"200":{"description":"Your posts, newest first, plus the current limits.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PostListResponse"},"example":{"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}}}}},"401":{"description":"Missing or invalid API key.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorWithCode"},"example":{"error":"Unauthorized","code":"unauthorized"}}}},"403":{"$ref":"#/components/responses/AppSuspended"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl \"https://www.dropfans.io/api/external/posts\" \\\n  -H \"Authorization: Bearer $DROPFANS_API_KEY\""},{"lang":"JavaScript","label":"Node","source":"const res = await fetch(`https://www.dropfans.io/api/external/posts`, {\n  headers: {\n    Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`,\n  },\n});\nconsole.log(await res.json());"},{"lang":"Python","source":"import os\nimport requests\n\nres = requests.get(\n    \"https://www.dropfans.io/api/external/posts\",\n    headers={\"Authorization\": f\"Bearer {os.environ['DROPFANS_API_KEY']}\"},\n)\nprint(res.json())"}],"x-dropfans-docs":"https://www.dropfans.io/developers/reference/list-posts"}},"/api/external/posts/{id}":{"get":{"operationId":"getPost","tags":["posts"],"summary":"Check one post’s moderation status","description":"One post, as a **bare object** (not wrapped). The polling endpoint after a PENDING create: check `status` and `live`.\n\nA sensible poll cadence is every 30–60 seconds while PENDING; media moderation usually resolves within minutes.","parameters":[{"name":"id","in":"path","required":true,"description":"The post id.","schema":{"type":"string"},"example":"clxp0st000001feed"}],"responses":{"200":{"description":"The post.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Post"},"example":{"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"}]}}}},"401":{"description":"Missing or invalid API key.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorWithCode"},"example":{"error":"Unauthorized","code":"unauthorized"}}}},"403":{"$ref":"#/components/responses/AppSuspended"},"404":{"description":"No such post on your account. Deliberately 404 (not 403) for another creator’s post — ids cannot be probed.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Post not found"}}}},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl \"https://www.dropfans.io/api/external/posts/$POST_ID\" \\\n  -H \"Authorization: Bearer $DROPFANS_API_KEY\""},{"lang":"JavaScript","label":"Node","source":"const postId = '…'; // from an earlier response\n\nconst res = await fetch(`https://www.dropfans.io/api/external/posts/${postId}`, {\n  headers: {\n    Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`,\n  },\n});\nconsole.log(await res.json());"},{"lang":"Python","source":"import os\nimport requests\n\npost_id = \"…\"  # from an earlier response\n\nres = requests.get(\n    f\"https://www.dropfans.io/api/external/posts/{post_id}\",\n    headers={\"Authorization\": f\"Bearer {os.environ['DROPFANS_API_KEY']}\"},\n)\nprint(res.json())"}],"x-dropfans-docs":"https://www.dropfans.io/developers/reference/get-post"},"delete":{"operationId":"deletePost","tags":["posts"],"summary":"Delete a post (or cancel a scheduled one)","description":"Hard-deletes one of your posts. Deleting a scheduled post before it goes live cancels it.\n\nReturns `{\"ok\":true}` — note this family uses `ok` where the vault endpoints use `success`.","parameters":[{"name":"id","in":"path","required":true,"description":"The post id.","schema":{"type":"string"},"example":"clxp0st000001feed"}],"responses":{"200":{"description":"Deleted.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ok"},"example":{"ok":true}}}},"401":{"description":"Missing or invalid API key.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorWithCode"},"example":{"error":"Unauthorized","code":"unauthorized"}}}},"403":{"$ref":"#/components/responses/AppSuspended"},"404":{"description":"No such post on your account (another creator’s post also 404s).","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Post not found"}}}},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl -X DELETE \"https://www.dropfans.io/api/external/posts/$POST_ID\" \\\n  -H \"Authorization: Bearer $DROPFANS_API_KEY\""},{"lang":"JavaScript","label":"Node","source":"const postId = '…'; // from an earlier response\n\nconst res = await fetch(`https://www.dropfans.io/api/external/posts/${postId}`, {\n  method: 'DELETE',\n  headers: {\n    Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`,\n  },\n});\nconsole.log(await res.json());"},{"lang":"Python","source":"import os\nimport requests\n\npost_id = \"…\"  # from an earlier response\n\nres = requests.delete(\n    f\"https://www.dropfans.io/api/external/posts/{post_id}\",\n    headers={\"Authorization\": f\"Bearer {os.environ['DROPFANS_API_KEY']}\"},\n)\nprint(res.json())"}],"x-dropfans-docs":"https://www.dropfans.io/developers/reference/delete-post"}},"/api/external/earnings":{"get":{"operationId":"getEarnings","tags":["earnings"],"summary":"Earnings stats, chart and recent transactions","description":"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.\n\nEverything here is in **cents** (the balance endpoint is dollars). `stats.totalEarningsCents` is NET (after the platform fee); `grossEarningsCents` and the chart series are seller GROSS (buyer-paid minus tax, before the fee) so per-day merges against your own gross ledgers compare like for like. Pass `tz` = the creator's timezone (read it from [GET /api/external/timezone](/developers/reference/get-timezone)) or your day buckets won't match their dashboard.\n\n> [!NOTE] transactions is a fixed cap, not a page\n> `transactions` is always the newest 50 across all three sources — there is no pagination. To get a complete ledger, narrow the date window until fewer than 50 come back.","parameters":[{"name":"startDate","in":"query","required":true,"description":"Window start, YYYY-MM-DD, interpreted in `tz`.","schema":{"type":"string","format":"date"},"example":"2026-08-01"},{"name":"endDate","in":"query","required":true,"description":"Window end (inclusive), YYYY-MM-DD, interpreted in `tz`.","schema":{"type":"string","format":"date"},"example":"2026-08-19"},{"name":"tz","in":"query","required":false,"description":"IANA timezone for day bucketing. **Invalid values silently fall back to UTC** — no error.","schema":{"type":"string","default":"UTC","format":"iana-timezone"},"example":"Europe/Stockholm"}],"responses":{"200":{"description":"Stats, chart and the newest transactions. All money in cents.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EarningsResponse"},"example":{"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"}]}}}},"400":{"description":"Either date param is missing.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"startDate and endDate query params are required (YYYY-MM-DD)"}}}},"401":{"description":"Missing or invalid API key.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorWithCode"},"example":{"error":"Unauthorized","code":"unauthorized"}}}},"403":{"$ref":"#/components/responses/AppSuspended"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"description":"Query failed — retry later.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Failed to fetch earnings"}}}}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl \"https://www.dropfans.io/api/external/earnings?startDate=2026-08-01&endDate=2026-08-19&tz=Europe%2FStockholm\" \\\n  -H \"Authorization: Bearer $DROPFANS_API_KEY\""},{"lang":"JavaScript","label":"Node","source":"const res = await fetch(`https://www.dropfans.io/api/external/earnings?startDate=2026-08-01&endDate=2026-08-19&tz=Europe%2FStockholm`, {\n  headers: {\n    Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`,\n  },\n});\nconsole.log(await res.json());"},{"lang":"Python","source":"import os\nimport requests\n\nres = requests.get(\n    \"https://www.dropfans.io/api/external/earnings\",\n    headers={\"Authorization\": f\"Bearer {os.environ['DROPFANS_API_KEY']}\"},\n    params={\"startDate\": \"2026-08-01\", \"endDate\": \"2026-08-19\", \"tz\": \"Europe/Stockholm\"},\n)\nprint(res.json())"}],"x-dropfans-docs":"https://www.dropfans.io/developers/reference/get-earnings"}},"/api/external/links":{"get":{"operationId":"getLinks","tags":["links"],"summary":"Canonical profile, tip, subscribe and buy links","description":"Every shareable link for the creator behind the key, in one call — never template Dropfans URLs by hand.\n\nThe `web` block always exists. The `telegram` block (Mini App deep links) is **null when the shared Dropfans bot is not configured** — fall back to the web links. Fields ending in `Template` contain literal placeholders to substitute: `{usd}` (whole dollars, web tip), `{cents}` (Telegram tip — note the unit difference), `{productId}` (buy links).\n\nTip prefill bounds: web `?tip=<usd>` opens the sheet prefilled when the amount is between $5 and $750; out-of-range amounts open the sheet unfilled rather than erroring.","responses":{"200":{"description":"The creator’s links.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Links"},"example":{"username":"valeria","web":{"profile":"https://www.dropfans.io/u/valeria","tip":"https://www.dropfans.io/u/valeria?tip=1","tipTemplate":"https://www.dropfans.io/u/valeria?tip={usd}","subscribe":"https://www.dropfans.io/u/valeria?subscribe=1","buyTemplate":"https://www.dropfans.io/buy/{productId}"},"telegram":{"bot":"DropfansBot","profile":"https://t.me/DropfansBot/app?startapp=p_valeria","tip":"https://t.me/DropfansBot/app?startapp=t_valeria","tipTemplate":"https://t.me/DropfansBot/app?startapp=pt_{cents}_valeria","subscribe":"https://t.me/DropfansBot/app?startapp=s_valeria","spin":"https://t.me/DropfansBot/app?startapp=w_valeria","buyTemplate":"https://t.me/DropfansBot/app?startapp=b_{productId}"}}}}},"401":{"description":"Missing or invalid API key.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorWithCode"},"example":{"error":"Unauthorized","code":"unauthorized"}}}},"403":{"$ref":"#/components/responses/AppSuspended"},"409":{"description":"The creator has no username yet — every link embeds it. Have the creator pick a handle in their Dropfans settings, then retry.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorWithCode"},"example":{"error":"Set a username first","code":"username_required"}}}},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl \"https://www.dropfans.io/api/external/links\" \\\n  -H \"Authorization: Bearer $DROPFANS_API_KEY\""},{"lang":"JavaScript","label":"Node","source":"const res = await fetch(`https://www.dropfans.io/api/external/links`, {\n  headers: {\n    Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`,\n  },\n});\nconsole.log(await res.json());"},{"lang":"Python","source":"import os\nimport requests\n\nres = requests.get(\n    \"https://www.dropfans.io/api/external/links\",\n    headers={\"Authorization\": f\"Bearer {os.environ['DROPFANS_API_KEY']}\"},\n)\nprint(res.json())"}],"x-dropfans-docs":"https://www.dropfans.io/developers/reference/get-links"}},"/api/external/notifications":{"get":{"operationId":"getNotifications","tags":["notifications"],"summary":"Read the Telegram notification status","description":"Where the creator's sale notifications currently go: their saved handle, whether a personal chat is connected, and whether a group/channel is connected.\n\nRead-only and safe. Use it before any write in this group so you never clobber an existing setup.","responses":{"200":{"description":"The current connection state.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NotificationsStatus"},"example":{"telegramHandle":"valeria_tg","personalConnected":true,"groupConnected":false,"groupChatId":null,"groupName":null}}}},"401":{"description":"Missing or invalid API key.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorWithCode"},"example":{"error":"Unauthorized","code":"unauthorized"}}}},"403":{"$ref":"#/components/responses/AppSuspended"},"404":{"description":"The key’s account no longer exists.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"User not found"}}}},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl \"https://www.dropfans.io/api/external/notifications\" \\\n  -H \"Authorization: Bearer $DROPFANS_API_KEY\""},{"lang":"JavaScript","label":"Node","source":"const res = await fetch(`https://www.dropfans.io/api/external/notifications`, {\n  headers: {\n    Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`,\n  },\n});\nconsole.log(await res.json());"},{"lang":"Python","source":"import os\nimport requests\n\nres = requests.get(\n    \"https://www.dropfans.io/api/external/notifications\",\n    headers={\"Authorization\": f\"Bearer {os.environ['DROPFANS_API_KEY']}\"},\n)\nprint(res.json())"}],"x-dropfans-docs":"https://www.dropfans.io/developers/reference/get-notifications"},"put":{"operationId":"updateNotifications","tags":["notifications"],"summary":"Update Telegram notification settings","description":"An action-dispatch endpoint — the `action` field picks one of three operations:\n\n- `save-handle` — store the creator's Telegram @handle (`telegramHandle`, leading @ stripped).\n- `disconnect` — clear the personal chat, the group, or both (`type`: \"personal\" | \"group\" | \"all\").\n- `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.\n\n> [!WARNING] This rewrites the creator's live notification settings\n> 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.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"action":{"type":"string","description":"Which operation to perform.","enum":["save-handle","disconnect","add-group"]},"telegramHandle":{"type":"string","description":"save-handle only: the @handle (leading @ is stripped).","example":"valeria_tg"},"type":{"type":"string","description":"disconnect only: what to clear.","enum":["personal","group","all"]},"groupChatId":{"type":"string","description":"add-group only: the Telegram chat id of a group/channel the bot is in.","example":"-1001234567890"}},"required":["action"]},"examples":{"saveHandle":{"summary":"Save the creator’s handle","value":{"action":"save-handle","telegramHandle":"valeria_tg"}},"addGroup":{"summary":"Register a notification group","value":{"action":"add-group","groupChatId":"-1001234567890"}},"disconnect":{"summary":"Disconnect everything","value":{"action":"disconnect","type":"all"}}}}}},"responses":{"200":{"description":"Action applied. save-handle echoes `telegramHandle`; add-group echoes `groupName`; disconnect returns `{success:true}` alone.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","description":"Always true.","example":true},"telegramHandle":{"type":"string","description":"save-handle only: the stored handle (without @)."},"groupName":{"type":"string","description":"add-group only: the group’s title as Telegram reports it."}},"required":["success"]},"example":{"success":true,"groupName":"Valeria sales"}}}},"400":{"description":"The body is not valid JSON. Also: save-handle without a handle. Also: save-handle with an empty handle after stripping @. Also: disconnect with a bad type. Also: add-group without a chat id. Also: Telegram does not know the chat, or the bot is not a member. Also: add-group with a personal chat id. Also: `action` is none of the three.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Invalid JSON body"}}}},"401":{"description":"Missing or invalid API key.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorWithCode"},"example":{"error":"Unauthorized","code":"unauthorized"}}}},"403":{"$ref":"#/components/responses/AppSuspended"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl -X PUT \"https://www.dropfans.io/api/external/notifications\" \\\n  -H \"Authorization: Bearer $DROPFANS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n  \"action\": \"save-handle\",\n  \"telegramHandle\": \"valeria_tg\"\n}'"},{"lang":"JavaScript","label":"Node","source":"const res = await fetch(`https://www.dropfans.io/api/external/notifications`, {\n  method: 'PUT',\n  headers: {\n    Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"action\": \"save-handle\",\n    \"telegramHandle\": \"valeria_tg\"\n  }),\n});\nconsole.log(await res.json());"},{"lang":"Python","source":"import os\nimport requests\n\nres = requests.put(\n    \"https://www.dropfans.io/api/external/notifications\",\n    headers={\"Authorization\": f\"Bearer {os.environ['DROPFANS_API_KEY']}\"},\n    json={\n        \"action\": \"save-handle\",\n        \"telegramHandle\": \"valeria_tg\",\n    },\n)\nprint(res.json())"}],"x-dropfans-docs":"https://www.dropfans.io/developers/reference/update-notifications"}},"/api/external/register-telegram-chat":{"post":{"operationId":"registerTelegramChat","tags":["notifications"],"summary":"Register the creator’s personal notification chat","description":"Stores a Telegram chat id as the creator's **personal** notification target — the DM their sale notifications go to.\n\nUnlike 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).\n\n> [!WARNING] This rewrites the creator's live notification settings\n> 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.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"telegramChatId":{"type":"string","description":"The Telegram chat id of the creator’s DM with the notification bot. Stored as-is, unverified.","example":"123456789"}},"required":["telegramChatId"]},"examples":{"register":{"summary":"Register a personal chat","value":{"telegramChatId":"123456789"}}}}}},"responses":{"200":{"description":"Stored.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Success"},"example":{"success":true}}}},"400":{"description":"The body is not valid JSON. Also: Missing or non-string chat id.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Invalid JSON body"}}}},"401":{"description":"Missing or invalid API key.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorWithCode"},"example":{"error":"Unauthorized","code":"unauthorized"}}}},"403":{"$ref":"#/components/responses/AppSuspended"},"429":{"$ref":"#/components/responses/RateLimited"}},"x-codeSamples":[{"lang":"cURL","label":"curl","source":"curl -X POST \"https://www.dropfans.io/api/external/register-telegram-chat\" \\\n  -H \"Authorization: Bearer $DROPFANS_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n  \"telegramChatId\": \"123456789\"\n}'"},{"lang":"JavaScript","label":"Node","source":"const res = await fetch(`https://www.dropfans.io/api/external/register-telegram-chat`, {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${process.env.DROPFANS_API_KEY}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    \"telegramChatId\": \"123456789\"\n  }),\n});\nconsole.log(await res.json());"},{"lang":"Python","source":"import os\nimport requests\n\nres = requests.post(\n    \"https://www.dropfans.io/api/external/register-telegram-chat\",\n    headers={\"Authorization\": f\"Bearer {os.environ['DROPFANS_API_KEY']}\"},\n    json={\n        \"telegramChatId\": \"123456789\",\n    },\n)\nprint(res.json())"}],"x-dropfans-docs":"https://www.dropfans.io/developers/reference/register-telegram-chat"}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"dpfn_ + 64 hex characters","description":"Your Dropfans API key, sent as `Authorization: Bearer dpfn_...`. A key is bound to one\ncreator account. Creators generate keys in the Dropfans dashboard under Vault → API\nConnect (direct link: https://www.dropfans.io/dashboard/vault?apiConnect=1) and pick\nwhich app the key is for at mint. Treat keys like passwords — never paste one into a\nshared or public chat."}},"headers":{"XRateLimitTier":{"description":"Rate-limit tier of the key: personal, app or first_party (first_party is unlimited).","schema":{"type":"string","enum":["personal","app","first_party"]}},"XRateLimitLimit":{"description":"Requests allowed per minute for this key (absent on first_party).","schema":{"type":"integer"}},"XRateLimitRemaining":{"description":"Requests left in the current minute window.","schema":{"type":"integer"}},"XRateLimitReset":{"description":"Epoch seconds when the minute window resets.","schema":{"type":"integer"}},"XRateLimitLimitDay":{"description":"Requests allowed per UTC day for this key (absent on first_party).","schema":{"type":"integer"}},"XRateLimitRemainingDay":{"description":"Requests left in the current UTC day window.","schema":{"type":"integer"}},"XRateLimitResetDay":{"description":"Epoch seconds when the day window resets.","schema":{"type":"integer"}},"RetryAfter":{"description":"Seconds to wait before retrying (sent with 429s).","schema":{"type":"integer"}}},"responses":{"Unauthorized":{"description":"Missing or invalid API key.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorWithCode"},"example":{"error":"Unauthorized","code":"unauthorized"}}}},"AppSuspended":{"description":"The app this key belongs to has been suspended by Dropfans. Every request fails with this until the app is reinstated — surface it to the creator and contact Dropfans.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorWithCode"},"example":{"error":"This integration has been suspended by Dropfans. Contact the app developer.","code":"app_suspended"}}}},"RateLimited":{"description":"Rate limit exceeded for the current minute or day window. Wait Retry-After seconds and retry.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"},"X-RateLimit-Limit":{"$ref":"#/components/headers/XRateLimitLimit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/XRateLimitRemaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/XRateLimitReset"},"X-RateLimit-Limit-Day":{"$ref":"#/components/headers/XRateLimitLimitDay"},"X-RateLimit-Remaining-Day":{"$ref":"#/components/headers/XRateLimitRemainingDay"},"X-RateLimit-Reset-Day":{"$ref":"#/components/headers/XRateLimitResetDay"},"Retry-After":{"$ref":"#/components/headers/RetryAfter"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorWithCode"},"example":{"error":"Rate limit exceeded","code":"rate_limited"}}}},"InvalidJson":{"description":"The request body is not valid JSON.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Invalid JSON body"}}}},"NotFound":{"description":"The resource does not exist on this creator’s account. Another creator’s ids also 404 — never 403 — so ids cannot be probed.","headers":{"X-RateLimit-Tier":{"$ref":"#/components/headers/XRateLimitTier"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not found"}}}}},"schemas":{"Me":{"type":"object","properties":{"id":{"type":"string","description":"The creator’s Dropfans user id. Stable — safe to key your own records on.","example":"clx2f8a1b0001qw3k"},"username":{"type":["string","null"],"description":"The creator’s public @handle, without the @. Null until the creator picks one — several link surfaces (GET /api/external/links, the post `url`) need it.","example":"valeria"},"name":{"type":["string","null"],"description":"Display name. Null when unset.","example":"Valeria"},"image":{"type":["string","null"],"description":"Profile image URL. Null when the creator has no avatar.","format":"uri"},"accountType":{"type":"string","description":"Account class of the key’s owner. Posting to the For You feed requires CREATOR or AGENCY.","enum":["CONSUMER","CREATOR","AGENCY"],"example":"CREATOR"},"key":{"type":"object","properties":{"name":{"type":"string","description":"The key’s label, chosen at mint (defaults to the app name, or \"Personal\").","example":"KVIQA"},"app":{"type":["object","null"],"properties":{"slug":{"type":"string","description":"Stable app identifier.","example":"kviqa"},"name":{"type":"string","description":"App display name.","example":"KVIQA"}},"description":"The integration this key was minted for. Null for a Personal key."},"tier":{"type":"string","description":"Rate-limit tier of this key — the same value the X-RateLimit-Tier response header carries.","enum":["personal","app","first_party"],"example":"app"}},"description":"Metadata about the API key used on this request (added 2026-08-19)."}},"required":["id"]},"Timezone":{"type":"object","properties":{"timezone":{"type":"string","description":"IANA timezone name (e.g. \"Europe/Stockholm\"). Defaults to \"UTC\" when the creator never set one.","format":"iana-timezone","example":"Europe/Stockholm"}},"required":["timezone"]},"Balance":{"type":"object","properties":{"currency":{"type":"string","description":"Always \"USD\".","enum":["USD"]},"pending":{"type":"number","description":"USD **dollars** (not cents — unlike earnings and check-status). Earnings still inside the chargeback hold window; not payable yet.","example":120.5},"available":{"type":"number","description":"USD dollars. Cleared the hold — payable in the next payout batch. Can be negative for agency accounts.","example":342.1},"processing":{"type":"number","description":"USD dollars. Included in a payout that is currently PROCESSING.","example":0},"paidOut":{"type":"number","description":"USD dollars. Lifetime total of completed payouts.","example":1875}},"required":["currency","pending","available","processing","paidOut"]},"VaultItem":{"type":"object","properties":{"id":{"type":"string","description":"Vault item id — use as `vaultItemId` when creating drops and MEDIA posts.","example":"clxv1a2b30001item"},"fileName":{"type":"string","description":"Original file name as uploaded.","example":"beach-set-01.jpg"},"filePath":{"type":"string","description":"Display asset URL. Images: the CDN display copy. Audio: a token-signed URL valid ~12 hours, directly fetchable. Videos: the Bunny Stream playback URL (DRM-locked — use `downloadUrl` to fetch bytes). **Empty string `\"\"` for items that are not APPROVED** (visible only with includePending=true): unapproved content may never be forwarded."},"thumbnailPath":{"type":["string","null"],"description":"Thumbnail URL. For videos this is always rebuilt as an absolute Bunny Stream thumbnail from the current CDN host. Null for audio (voice messages have no visual)."},"fileType":{"type":"string","description":"Media kind.","enum":["image","video","audio"]},"fileSize":{"type":["integer","null"],"description":"Size in bytes of the stored display asset.","example":482113},"durationSeconds":{"type":["integer","null"],"description":"Audio (voice message) length in seconds. Null for images and videos.","example":42},"bunnyStreamId":{"type":["string","null"],"description":"Bunny Stream GUID for videos — the id you poll with POST /api/external/vault/video-status. Null for images and audio."},"createdAt":{"type":"string","description":"Upload time.","format":"date-time"},"folderId":{"type":["string","null"],"description":"Containing folder id, or null for unfiled (\"All\")."},"contentTags":{"type":"array","items":{"type":"string","description":"A content tag (≤64 chars)."},"description":"The item’s content tags (set via PATCH /api/external/vault/{id}/tags)."},"downloadUrl":{"type":["string","null"],"description":"Token-signed, directly fetchable original — valid ~12 hours. Videos: the DRM-free Bunny Storage original (null for videos uploaded before dual-store existed — those cannot be fetched). Audio: the same signed voice file as `filePath`. **Always null for images** (fetch `filePath` instead) and for non-APPROVED items."},"moderationStatus":{"type":"string","description":"**Only present when the request had includePending=true.** Default responses contain APPROVED items only and omit this field entirely, keeping the shape byte-identical for older consumers.","enum":["PENDING","APPROVED","REJECTED","FLAGGED"]}},"required":["id","fileName","filePath","fileType","createdAt","contentTags"]},"VaultItemUploaded":{"type":"object","properties":{"id":{"type":"string","description":"The new vault item id.","example":"clxv1a2b30001item"},"fileName":{"type":"string","description":"The `originalName` you sent.","example":"beach-set-01.jpg"},"filePath":{"type":"string","description":"CDN URL of the stored display asset (images/audio) or the Stream playback URL (videos)."},"thumbnailPath":{"type":["string","null"],"description":"Thumbnail URL. Null for audio."},"fileType":{"type":"string","description":"Media kind, echoing the request.","enum":["image","video","audio"]},"fileSize":{"type":["integer","null"],"description":"Stored size in bytes.","example":482113},"durationSeconds":{"type":["integer","null"],"description":"Voice-message length in seconds (audio only, when you sent it). Null otherwise."},"bunnyStreamId":{"type":["string","null"],"description":"Bunny Stream GUID (video uploads only)."},"createdAt":{"type":"string","description":"Creation time.","format":"date-time"}},"required":["id","fileName","filePath","fileType","createdAt"]},"VideoVaultItem":{"type":"object","properties":{"id":{"type":"string","description":"The new vault item id.","example":"clxv9z8y70002item"},"fileName":{"type":"string","description":"The `originalName` you sent.","example":"teaser.mp4"},"filePath":{"type":"string","description":"Signed playback URL, valid ~1 hour. For a long-lived URL, re-list the vault later."},"thumbnailPath":{"type":["string","null"],"description":"Signed Stream thumbnail (600px wide)."},"fileType":{"type":"string","description":"Always \"video\" here.","enum":["video"]},"fileSize":{"type":["integer","null"],"description":"Byte count Bunny reported for the finished upload.","example":52428800},"bunnyStreamId":{"type":["string","null"],"description":"The Stream GUID — poll POST /api/external/vault/video-status with it while the video transcodes."},"moderationStatus":{"type":"string","description":"Starts PENDING — the NSFW pipeline finalizes it asynchronously. The item only appears in the default vault list once APPROVED.","enum":["PENDING","APPROVED","REJECTED","FLAGGED"]},"moderationTags":{"type":"array","items":{"type":"string","description":"Moderation label."},"description":"Always empty at creation (moderation has not run yet)."},"createdAt":{"type":"string","description":"Creation time.","format":"date-time"},"aiEnhanced":{"type":"boolean","description":"True when the account is AI-labelled — every upload from such an account is force-marked AI."}},"required":["id","fileName","filePath","fileType","moderationStatus","moderationTags","createdAt","aiEnhanced"]},"Folder":{"type":"object","properties":{"id":{"type":"string","description":"Folder id.","example":"clxf0ld3r0001abcd"},"name":{"type":"string","description":"Folder name — unique per creator.","example":"Beach set"},"itemCount":{"type":"integer","description":"Items in the folder. NOTE: GET /api/external/vault counts only items visible at that call’s moderation filter, while GET /api/external/vault/folders counts every non-hidden item regardless of moderation status — the same folder can report two different counts.","example":12}},"required":["id","name","itemCount"]},"VaultListResponse":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/VaultItem"},"description":"The requested page, newest first."},"folders":{"type":"array","items":{"$ref":"#/components/schemas/Folder"},"description":"All of the creator’s folders (unpaginated), with counts under the current moderation filter."},"hasMore":{"type":"boolean","description":"True when more pages exist for the current filter."},"total":{"type":"integer","description":"Total items matching the filter.","example":128},"page":{"type":"integer","description":"Echoed page number (1-based).","example":1},"limit":{"type":"integer","description":"Echoed page size.","example":50}},"required":["items","folders","hasMore","total","page","limit"]},"VideoUploadCredentials":{"type":"object","properties":{"videoId":{"type":"string","description":"The Bunny Stream GUID created for this upload. Send it back to the complete step, and use it as the TUS `VideoId` metadata.","example":"c2f7f9e2-1111-4222-b333-4d55e6f7a8b9"},"tusEndpoint":{"type":"string","description":"The TUS upload endpoint (https://video.bunnycdn.com/tusupload). Upload the raw file bytes here with a TUS client.","format":"uri"},"libraryId":{"type":"string","description":"Bunny Stream library id — send as the `LibraryId` TUS header."},"signature":{"type":"string","description":"Presigned sha256 — send as the `AuthorizationSignature` TUS header. The Stream API key itself is never exposed."},"expires":{"type":"integer","description":"Unix timestamp (seconds) when the signature expires (~6 hours) — send as the `AuthorizationExpire` TUS header.","example":1755640800},"completionToken":{"type":"string","description":"HMAC token proving this upload was started by your key — required by the complete step. Valid 8 hours."}},"required":["videoId","tusEndpoint","libraryId","signature","expires","completionToken"]},"VideoStatusMap":{"type":"object","description":"Keyed by the Bunny Stream GUIDs you sent. Ids you don’t own, and ids whose Bunny lookup errored, are OMITTED (not reported as failed) — treat a missing key as \"still gated\".","additionalProperties":{"type":"object","properties":{"isReady":{"type":"boolean","description":"Transcoding finished — the video is playable and forwardable."},"isProcessing":{"type":"boolean","description":"Still transcoding — poll again later."},"isFailed":{"type":"boolean","description":"Encoding failed — re-upload."},"length":{"type":"integer","description":"Duration in seconds, once known.","example":74}},"required":["isReady","isProcessing","isFailed"],"description":"Keyed by the Bunny Stream GUIDs you sent. Ids you don’t own, and ids whose Bunny lookup errored, are OMITTED (not reported as failed) — treat a missing key as \"still gated\"."},"propertyNames":{"description":"Keys are the ids you sent ({videoId})."}},"Drop":{"type":"object","properties":{"id":{"type":"string","description":"The product (drop) id.","example":"clxdr0p000001prod"},"name":{"type":["string","null"],"description":"Drop title. Null when created without a name.","example":"Beach set — 6 photos"},"price":{"type":"number","description":"USD **dollars**. 0 means free.","example":25},"currency":{"type":"string","description":"Always \"USD\".","enum":["USD"]},"status":{"type":"string","description":"The drop’s overall moderation status. A drop built from already-APPROVED vault items is APPROVED at creation.","enum":["PENDING","APPROVED","REJECTED","FLAGGED"]},"moderationReason":{"type":["string","null"],"description":"Why the drop was rejected or flagged. Null otherwise."},"buyUrl":{"type":"string","description":"The web checkout page for this drop.","format":"uri","example":"https://www.dropfans.io/buy/clxdr0p000001prod"},"allowDownload":{"type":"boolean","description":"Whether buyers may download the files after purchase."},"mediaCount":{"type":"integer","description":"Number of media items attached.","example":6},"media":{"type":"array","items":{"type":"object","properties":{"vaultItemId":{"type":"string","description":"The source vault item."},"order":{"type":"integer","description":"Display position (0-based)."},"fileType":{"type":"string","description":"Media kind.","enum":["image","video","audio"]},"moderationStatus":{"type":"string","description":"Per-item moderation status.","enum":["PENDING","APPROVED","REJECTED","FLAGGED"]},"hasPreview":{"type":"boolean","description":"True once a baked blur preview was attached via POST /api/external/drops/{id}/previews."}},"required":["vaultItemId","order","fileType","moderationStatus","hasPreview"],"description":"One attached vault item."},"description":"Attached media in display order."},"salesCount":{"type":"integer","description":"Paid orders, excluding refunds and chargebacks (stricter than check-status, which does NOT exclude refunds).","example":2},"lastSaleAt":{"type":["string","null"],"description":"When the newest counted sale was paid. Null when never sold.","format":"date-time"},"createdAt":{"type":"string","description":"Creation time.","format":"date-time"}},"required":["id","price","currency","status","buyUrl","allowDownload","mediaCount","media","salesCount","createdAt"]},"DropCreated":{"type":"object","properties":{"productId":{"type":"string","description":"The new drop’s id — keep it: check-status, GET /drops/{id}, previews and post attachment all key on it.","example":"clxdr0p000001prod"},"buyUrl":{"type":"string","description":"Web checkout URL to hand to the buyer. For a Telegram Mini App link, substitute the productId into telegram.buyTemplate from GET /api/external/links.","format":"uri","example":"https://www.dropfans.io/buy/clxdr0p000001prod"},"mediaCount":{"type":"integer","description":"How many vault items were attached.","example":3}},"required":["productId","buyUrl","mediaCount"]},"SalesMap":{"type":"object","description":"Keyed by the product ids you sent. Unsold and unknown ids are OMITTED — only entries with paid:true come back. Multiple paid orders on one product report the most recent. Refunded/charged-back orders are NOT excluded here (unlike earnings) — cross-check GET /api/external/earnings for net truth.","additionalProperties":{"type":"object","properties":{"paid":{"type":"boolean","description":"Always true — unsold products simply do not appear.","example":true},"saleAmountCents":{"type":"integer","description":"Gross buyer-paid total in **cents** (order.totalCharged × 100).","example":2500},"buyerEmail":{"type":["string","null"],"description":"The buyer’s email when known, else null.","format":"email"}},"required":["paid","saleAmountCents"],"description":"Keyed by the product ids you sent. Unsold and unknown ids are OMITTED — only entries with paid:true come back. Multiple paid orders on one product report the most recent. Refunded/charged-back orders are NOT excluded here (unlike earnings) — cross-check GET /api/external/earnings for net truth."},"propertyNames":{"description":"Keys are the ids you sent ({productId})."}},"PostMedia":{"type":"object","properties":{"id":{"type":"string","description":"Post-media row id."},"vaultItemId":{"type":"string","description":"The source vault item."},"isPaid":{"type":"boolean","description":"True for PPV items shown blurred until unlocked."},"order":{"type":"integer","description":"Display position (0-based)."},"type":{"type":"string","description":"Media kind (image/video)."}},"required":["id","vaultItemId","isPaid","order","type"]},"Post":{"type":"object","properties":{"id":{"type":"string","description":"Post id.","example":"clxp0st000001feed"},"kind":{"type":"string","description":"Post shape. DROP is derived from productId at creation — it is not an accepted request value.","enum":["TEXT","DROP","SUBSCRIPTION","COMMUNITY","MEDIA"]},"caption":{"type":["string","null"],"description":"Post text. Null for caption-less media posts."},"status":{"type":"string","description":"Moderation state. PENDING is normal for media posts and is not an error — poll until it becomes APPROVED.","enum":["PENDING","APPROVED","REJECTED","FLAGGED"]},"live":{"type":"boolean","description":"True only when the post is APPROVED and its publish time has passed."},"scheduledAt":{"type":["string","null"],"description":"When the post is scheduled to go live, or null for immediate posts.","format":"date-time"},"publishedAt":{"type":"string","description":"Effective publish time.","format":"date-time"},"createdAt":{"type":"string","description":"Creation time.","format":"date-time"},"productId":{"type":["string","null"],"description":"The attached drop, for kind=DROP. Null otherwise."},"likes":{"type":"integer","description":"Like count.","example":14},"comments":{"type":"integer","description":"Comment count.","example":3},"media":{"type":"array","items":{"$ref":"#/components/schemas/PostMedia"},"description":"Media entries in display order. Empty for TEXT posts."}},"required":["id","kind","status","live","publishedAt","createdAt","likes","comments","media"]},"PostLimits":{"type":"object","properties":{"postsPerDay":{"type":"integer","description":"Posts per creator per rolling 24 hours (currently 5).","example":5},"maxCaptionChars":{"type":"integer","description":"Caption length cap (currently 2000).","example":2000},"maxMediaPerPost":{"type":"integer","description":"Media items per post (currently 10).","example":10},"maxScheduleDays":{"type":"integer","description":"How far ahead a post can be scheduled, in days (currently 30).","example":30},"minPaidPrice":{"type":"integer","description":"Minimum USD price of a paid media item (currently 5).","example":5}},"required":["postsPerDay","maxCaptionChars","maxMediaPerPost","maxScheduleDays","minPaidPrice"]},"Pagination":{"type":"object","properties":{"page":{"type":"integer","description":"Echoed page (1-based).","example":1},"limit":{"type":"integer","description":"Echoed page size.","example":20},"total":{"type":"integer","description":"Total rows matching the filter.","example":63},"hasMore":{"type":"boolean","description":"True when more pages exist."}},"required":["page","limit","total","hasMore"]},"PostListResponse":{"type":"object","properties":{"posts":{"type":"array","items":{"$ref":"#/components/schemas/Post"},"description":"Your posts, newest first."},"pagination":{"$ref":"#/components/schemas/Pagination"},"limits":{"$ref":"#/components/schemas/PostLimits"}},"required":["posts","pagination","limits"]},"EarningsStats":{"type":"object","properties":{"totalEarningsCents":{"type":"integer","description":"**NET** seller earnings in cents for the window (after platform fee).","example":182050},"grossEarningsCents":{"type":"integer","description":"Seller gross in cents (buyer-paid minus tax, before the platform fee) — matches the dashboard’s Gross toggle.","example":214180},"previousPeriodEarningsCents":{"type":"integer","description":"NET cents for the same-length window immediately before startDate."},"previousPeriodGrossEarningsCents":{"type":"integer","description":"Gross cents for the previous window."},"transactionCount":{"type":"integer","description":"Paid transactions in the window (drops + tips + subscription payments).","example":41},"avgTransactionCents":{"type":"integer","description":"Average NET cents per transaction."},"uniqueCustomers":{"type":"integer","description":"Distinct buyer emails in the window.","example":28},"typeTotals":{"type":"object","properties":{"drop":{"type":"object","properties":{"grossCents":{"type":"integer","description":"Gross cents."},"netCents":{"type":"integer","description":"Net cents."},"count":{"type":"integer","description":"Transaction count."}},"required":["grossCents","netCents","count"],"description":"Totals for drop revenue."},"tip":{"type":"object","properties":{"grossCents":{"type":"integer","description":"Gross cents."},"netCents":{"type":"integer","description":"Net cents."},"count":{"type":"integer","description":"Transaction count."}},"required":["grossCents","netCents","count"],"description":"Totals for tip revenue."},"subscription":{"type":"object","properties":{"grossCents":{"type":"integer","description":"Gross cents."},"netCents":{"type":"integer","description":"Net cents."},"count":{"type":"integer","description":"Transaction count."}},"required":["grossCents","netCents","count"],"description":"Totals for subscription revenue."}},"required":["drop","tip","subscription"],"description":"Per-type window totals (additive field, 2026-07-22)."}},"required":["totalEarningsCents","grossEarningsCents","previousPeriodEarningsCents","previousPeriodGrossEarningsCents","transactionCount","avgTransactionCents","uniqueCustomers","typeTotals"]},"EarningsChart":{"type":"object","properties":{"labels":{"type":"array","items":{"type":"string","description":"Human-readable bucket label."},"description":"Chart x-axis labels, aligned with `values` and `dates`."},"values":{"type":"array","items":{"type":"integer","description":"GROSS cents for the bucket."},"description":"**GROSS** cents per bucket (gross so per-day merges against your own gross ledgers compare like for like)."},"dates":{"type":"array","items":{"type":"string","description":"YYYY-MM-DD bucket key."},"description":"Bucket keys in the caller’s tz. Weekly buckets use the week-start date, monthly the 1st."},"groupBy":{"type":"string","description":"Bucket size: day when the span is ≤31 days, week ≤90, else month.","enum":["day","week","month"]},"typedValues":{"type":"object","properties":{"drop":{"type":"array","items":{"type":"integer","description":"Gross cents."},"description":"Gross cents per bucket from drops."},"tip":{"type":"array","items":{"type":"integer","description":"Gross cents."},"description":"Gross cents per bucket from tips."},"subscription":{"type":"array","items":{"type":"integer","description":"Gross cents."},"description":"Gross cents per bucket from subscriptions."}},"required":["drop","tip","subscription"],"description":"Per-type GROSS cent series aligned 1:1 with `dates` (additive field, 2026-07-22)."}},"required":["labels","values","dates","groupBy","typedValues"]},"EarningsTransaction":{"type":"object","properties":{"id":{"type":"string","description":"The order / tip / renewal id (source-specific — matches nothing on the buyer’s side)."},"productId":{"type":"string","description":"The sold product’s id — **drop transactions only**; absent on tips and subscriptions. Join it back to your own drop records."},"productName":{"type":"string","description":"Product name, or the literal \"Tip\" / \"Subscription\".","example":"Beach set — 6 photos"},"amountCents":{"type":"integer","description":"NET seller earnings in cents.","example":2000},"grossAmountCents":{"type":"integer","description":"Gross cents (buyer-paid minus tax).","example":2500},"buyerEmail":{"type":["string","null"],"description":"Buyer email when known.","format":"email"},"buyerName":{"type":["string","null"],"description":"Buyer display name — tips and subscriptions only; always null on drops."},"paidAt":{"type":"string","description":"When it was paid.","format":"date-time"},"type":{"type":"string","description":"Revenue source.","enum":["drop","tip","subscription"]}},"required":["id","productName","amountCents","grossAmountCents","paidAt","type"]},"EarningsResponse":{"type":"object","properties":{"stats":{"$ref":"#/components/schemas/EarningsStats"},"chart":{"$ref":"#/components/schemas/EarningsChart"},"transactions":{"type":"array","items":{"$ref":"#/components/schemas/EarningsTransaction"},"description":"The newest 50 transactions across all three sources — a fixed cap, not a page. For a complete ledger, narrow the date window until fewer than 50 come back."}},"required":["stats","chart","transactions"]},"Links":{"type":"object","properties":{"username":{"type":"string","description":"The creator’s @handle the links are built for.","example":"valeria"},"web":{"type":"object","properties":{"profile":{"type":"string","description":"The creator’s public profile.","format":"uri","example":"https://www.dropfans.io/u/valeria"},"tip":{"type":"string","description":"Opens the profile with the tip sheet open.","format":"uri","example":"https://www.dropfans.io/u/valeria?tip=1"},"tipTemplate":{"type":"string","description":"Tip link with the amount prefilled — replace `{usd}` with whole US dollars. Amounts outside the allowed range open the sheet unfilled.","example":"https://www.dropfans.io/u/valeria?tip={usd}"},"subscribe":{"type":"string","description":"Opens the profile with the subscribe sheet open.","format":"uri","example":"https://www.dropfans.io/u/valeria?subscribe=1"},"buyTemplate":{"type":"string","description":"Checkout link for any drop — replace `{productId}` with the id from create-drop.","example":"https://www.dropfans.io/buy/{productId}"}},"required":["profile","tip","tipTemplate","subscribe","buyTemplate"],"description":"Canonical web links. Templates contain literal placeholders — substitute before use."},"telegram":{"type":["object","null"],"properties":{"bot":{"type":"string","description":"The shared bot username, without @.","example":"DropfansBot"},"profile":{"type":"string","description":"Opens the creator’s Mini App profile.","format":"uri","example":"https://t.me/DropfansBot/app?startapp=p_valeria"},"tip":{"type":"string","description":"Opens the Mini App tip sheet.","format":"uri","example":"https://t.me/DropfansBot/app?startapp=t_valeria"},"tipTemplate":{"type":"string","description":"Tip prefilled — replace `{cents}` with the amount in **cents** (the web template takes dollars; this one takes cents).","example":"https://t.me/DropfansBot/app?startapp=pt_{cents}_valeria"},"subscribe":{"type":"string","description":"Opens the Mini App subscribe flow.","format":"uri","example":"https://t.me/DropfansBot/app?startapp=s_valeria"},"spin":{"type":"string","description":"Opens the Lucky Wheel (when enabled for the creator).","format":"uri","example":"https://t.me/DropfansBot/app?startapp=w_valeria"},"buyTemplate":{"type":"string","description":"Mini App checkout for any drop — replace `{productId}`.","example":"https://t.me/DropfansBot/app?startapp=b_{productId}"}},"required":["bot","profile","tip","tipTemplate","subscribe","spin","buyTemplate"],"description":"Telegram Mini App deep links. **Null when the shared Dropfans bot is not configured** — fall back to the web links."}},"required":["username","web"]},"NotificationsStatus":{"type":"object","properties":{"telegramHandle":{"type":["string","null"],"description":"The creator’s saved Telegram @handle (without @), or null."},"personalConnected":{"type":"boolean","description":"True when a personal chat id is registered — sale notifications DM the creator."},"groupConnected":{"type":"boolean","description":"True when a group/channel is registered for notifications."},"groupChatId":{"type":["string","null"],"description":"The registered group chat id (e.g. \"-100…\"), or null."},"groupName":{"type":["string","null"],"description":"The registered group’s title, or null."}},"required":["personalConnected","groupConnected"]},"Success":{"type":"object","properties":{"success":{"type":"boolean","description":"Always true.","example":true}},"required":["success"]},"Ok":{"type":"object","properties":{"ok":{"type":"boolean","description":"Always true. (This endpoint family returns {ok} where the vault family returns {success} — historical, kept for compatibility.)","example":true}},"required":["ok"]},"Error":{"type":"object","properties":{"error":{"type":"string","description":"Human-readable message describing what went wrong.","example":"Vault item not found"}},"required":["error"]},"ErrorWithCode":{"type":"object","properties":{"error":{"type":"string","description":"Human-readable message.","example":"Rate limit exceeded"},"code":{"type":"string","description":"Machine-readable code: unauthorized, app_suspended, first_party_only, rate_limited, username_required.","example":"rate_limited"}},"required":["error","code"]},"ErrorWithMatchedWord":{"type":"object","properties":{"error":{"type":"string","description":"Human-readable message.","example":"Your post contains a word that isn’t allowed."},"matchedWord":{"type":"string","description":"The prohibited word that tripped the filter — rewrite and retry."}},"required":["error","matchedWord"]},"ErrorWithField":{"type":"object","properties":{"error":{"type":"string","description":"Human-readable message naming the offending field and word.","example":"Field \"name\" contains a prohibited word: \"…\""},"field":{"type":"string","description":"Which request field tripped the filter (\"name\" or \"description\")."},"matchedWord":{"type":"string","description":"The prohibited word."}},"required":["error","field","matchedWord"]}}}}