Rate limits
Fixed per-minute and per-day windows per key, by tier — read the X-RateLimit-* headers rather than hardcoding.
Tiers#
| Tier | Per minute | Per day | Who |
|---|---|---|---|
| Personal | 60 | 5,000 | Personal keys. The budget is shared across all personal keys of the same creator — minting more keys does not multiply it. |
| App | 300 | 50,000 | Keys bound to an approved app, per key. Higher per-app overrides on request. |
| First party | unlimited | unlimited | Dropfans-operated integrations are exempt. |
Windows are fixed, not rolling: the minute window is the current clock minute, the day window is the current UTC day. Both reset on the boundary.
Always read X-RateLimit-Limit off the response instead of assuming a number — env-level and per-app overrides mean the live limit can differ from the defaults printed here.
Headers on every response#
| Header | Meaning |
|---|---|
X-RateLimit-Tier | personal, app or first_party. |
X-RateLimit-Limit | Requests allowed in the current minute window. |
X-RateLimit-Remaining | Requests left in the current minute window. |
X-RateLimit-Reset | Unix seconds when the minute window resets. |
X-RateLimit-Limit-Day | Requests allowed in the current UTC-day window. |
X-RateLimit-Remaining-Day | Requests left in the current UTC-day window. |
X-RateLimit-Reset-Day | Unix seconds when the day window resets. |
Retry-After | On 429 only — seconds to wait before retrying. |
First-party responses carry only the tier header; limited tiers carry the full set.
When you hit the limit#
{ "error": "Rate limit exceeded", "code": "rate_limited" }Status 429, with Retry-After in seconds. Wait that long, then resume — do not hammer the boundary.
The separate posting cap#
Creating feed posts has its own cap, independent of the request budget: 5 posts per creator per rolling 24 hours — the same cap the dashboard composer enforces. Exceeding it also returns 429 with Retry-After, but the body is prose without a code:
{ "error": "You’ve reached the posting limit (5 per day). Try again in 3 hours." }Tell the two apart by the code field: rate_limited means request volume; no code on a posts 429 means the daily posting cap.
Retry wrapper#
async function callApi(url, options = {}, attempt = 0) {
const res = await fetch(url, {
...options,
headers: {
Authorization: 'Bearer ' + process.env.DROPFANS_API_KEY,
...(options.headers || {}),
},
});
if (res.status === 429 && attempt < 3) {
const wait = parseInt(res.headers.get('Retry-After') || '60', 10);
await new Promise((r) => setTimeout(r, wait * 1000));
return callApi(url, options, attempt + 1);
}
return res;
}import os, time, requests
def call_api(method, url, attempt=0, **kwargs):
headers = kwargs.pop("headers", {})
headers["Authorization"] = f"Bearer {os.environ['DROPFANS_API_KEY']}"
res = requests.request(method, url, headers=headers, **kwargs)
if res.status_code == 429 and attempt < 3:
wait = int(res.headers.get("Retry-After", "60"))
time.sleep(wait)
return call_api(method, url, attempt + 1, **kwargs)
return resCapacity planning#
Most throttling comes from polling too much, not from real traffic. Budget it: polling check-status once a minute costs 1,440 requests a day — well inside the app tier, but over a quarter of a personal day budget. Use the batch endpoints (one call covers up to 200 drops), follow the polling patterns, and watch that page for webhooks, which will replace most polling.
Next: Errors or Webhooks & polling.
Questions? [email protected]
