Skip to content
DonutStatsAPI
Sign up
Getting started

Rate limits and plans

Every account has a plan. Headers on each response tell you where you stand.

Free

$0forever

For trying the API and small personal projects.

  • 20 requests per hour
  • Every endpoint, including all history
  • Up to 5 API keys
  • Live player lookups
Create a free account

Pro

Unlimited

$8per month

For Discord apps, websites and dashboards that run all day.

  • Unlimited requests
  • Bursts up to 300 requests per minute
  • Every endpoint, including all history
  • Up to 5 API keys
  • Cancel any time from your dashboard
Upgrade to Pro

Both plans can call every endpoint and read all history. The only difference is how often. Upgrade, change your card or cancel from your dashboard. Payments are handled by Stripe.

Limits belong to the account, so every key on an account shares them. Every request made with a valid key counts, including 304 Not Modified answers and errors such as 400 and 404.

PlanLimitResets
Free20 requests per hourAt the start of every hour, UTC. Unused requests do not carry over.
ProNo hourly or daily cap. Bursts of up to 300 requests per minute.The burst allowance refills continuously.

The burst limit on Pro keeps the API fast for everyone. A client that spreads its calls out never notices it.

HeaderMeaning
x-ratelimit-limitFree: requests per hour. Pro: requests you can make in a burst.
x-ratelimit-remainingRequests left right now.
x-ratelimit-resetFree only: seconds until the hour resets.
retry-afterOn 429 responses: seconds to wait before trying again.

Responses look like this. The values are an example.

HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
cache-control: private, max-age=20, stale-while-revalidate=120
x-ratelimit-limit: 20
x-ratelimit-remaining: 17
x-ratelimit-reset: 1840

Past the limit the API answers 429 Too Many Requests with a retry-after header and the same number in the body as retryAfter:

HTTP/1.1 429 Too Many Requests
retry-after: 1840
x-ratelimit-limit: 20
x-ratelimit-remaining: 0
x-ratelimit-reset: 1840

{
  "error": "Hourly limit reached. The Free plan allows 20 requests per hour",
  "retryAfter": 1840
}

On Pro the wait is a second or two, so retrying is the right move. On Free it can be most of an hour, so give up and show a message instead. This helper does both:

async function donutstats(path, attempts = 3) {
  for (let attempt = 1; ; attempt++) {
    const response = await fetch(`https://api.donutstats.io${path}`, {
      headers: { "x-api-key": process.env.DONUTSTATS_API_KEY },
    });
    const wait = Number(response.headers.get("retry-after") ?? 1);
    if (response.status !== 429 || attempt === attempts || wait > 60) {
      if (!response.ok) throw new Error(`DonutStats API returned ${response.status}`);
      return response.json();
    }
    await new Promise((resolve) => setTimeout(resolve, wait * 1000));
  }
}

const orders = await donutstats("/v1/orders?sort=value&limit=50");

Live player refreshes are expensive, so they have their own per minute budget on top of the request limit. Past it, the player endpoint returns stored data with lookup.status set to rate_limited, or a 429 if there is nothing stored. Read live lookups for the details.

The image endpoints need no key and do not count towards your plan. They are limited to 60 images per minute per IP address instead.

  • Cache responses for as long as their cache-control header allows. Data does not change faster than that. See caching and freshness.
  • Prefer list endpoints over many single calls. GET /v1/items returns prices, changes and a 30 day sparkline for 200 products in one request.
  • Use big pages for history. History endpoints return up to 5,000 points per request.
  • Use lookup=none when you read players in bulk.
  • Spread scheduled jobs out instead of firing them all at the top of the hour.

Poll summaries, not lists

GET /v1/auction/summary and GET /v1/orders/summary are tiny and carry updatedAt. Poll those and only fetch the big lists when they change.