Rate limits
Per-organization limits on concurrent jobs, submits per minute and requests per minute, the tiers that set them, the headers that report them, and how to handle 429.
Windpaint limits how fast an organization can submit work and how many jobs it can have running at once. Limits are set by your organization’s tier, which goes up as you buy credits.
Tiers
Your tier depends on the total you’ve paid for credit top-ups over the life of the organization. Free signup credits don’t count.
| Tier | Lifetime purchases | Concurrent jobs | Submits per minute |
|---|---|---|---|
| 1 | None (free credits only) | 5 | 60 |
| 2 | $50 or more | 10 | 120 |
| 3 | $500 or more | 25 | 300 |
| 4 | $2,000 or more | 50 | 600 |
Every tier also allows 1,200 requests per minute per API key across all authenticated endpoints. Requests from the dashboard count per user instead of per key.
A new tier applies from the first request after a top-up is paid. If you need higher limits than tier 4, contact support; Windpaint can set custom limits for your organization, and its tier then reads override.
What counts
| Limit | Counted per | What counts toward it |
|---|---|---|
| Concurrent jobs | Organization | Generation jobs in status queued or in_progress. |
| Submits per minute | Organization | POST /v1/generation/capabilities/{capability}, POST /v1/generation/models/{model} and POST /v1/workflows/{slug}/runs. |
| Requests per minute | API key (or dashboard user) | Every authenticated request, including submits, uploads and the calls on this page. |
- A submit counts toward both the submits limit and the requests limit. Uploads count only toward requests.
- A submit is refused while your organization is at its concurrency limit.
- Starting a workflow run counts as one submit and is refused at the concurrency limit. The jobs its steps start later aren’t limited, so a running workflow can take your organization above its concurrency limit for a while.
- Minute windows are fixed and aligned to the clock: they reset at the start of each minute, not 60 seconds after your first request.
Headers
Authenticated responses carry the state of the limit that applied to the request:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | The allowance of the limit that applied. On submit endpoints this is the submits-per-minute limit; elsewhere it’s requests per minute. |
X-RateLimit-Remaining | What’s left in the current window. |
X-RateLimit-Reset | Seconds until the window resets. A number of seconds, not a timestamp. |
Retry-After | Seconds to wait before retrying. Only on 429. |
On a concurrency 429, X-RateLimit-Limit is your concurrency limit, X-RateLimit-Remaining is 0, and X-RateLimit-Reset equals Retry-After.
Public routes (such as GET /v1/openapi.json and GET /v1/errors) aren’t rate-limited and send none of these headers. Browser clients can read them; they’re exposed through CORS.
429 responses
A request over a limit returns 429 in the standard error envelope:
{
"error": {
"code": "rate_limit.concurrency",
"message": "Too many jobs in flight for the organization's tier. Retry once some finish.",
"details": { "limit": 5, "current": 5, "retry_after": 5 },
"request_id": "59630881.7c2e4a"
}
}
| Code | Cause | details |
|---|---|---|
rate_limit.requests | The requests-per-minute or submits-per-minute allowance is spent. | limit, current (the count including this request), retry_after |
rate_limit.concurrency | Your organization has as many jobs queued or running as its tier allows. | limit, current (jobs queued or running, plus other submits being processed at that moment), retry_after |
Nothing is created or charged when a request is refused with 429.
To handle it, wait Retry-After seconds and send the request again. For rate_limit.requests, that’s when the window resets. For rate_limit.concurrency, Retry-After is a polling hint (5 seconds); what you’re waiting for is one of your jobs to finish, so watch your running jobs’ status or webhooks and submit when one completes. If you retry submits, send an Idempotency-Key so a retry never creates a second job.
GET /v1/billing/limits
Your organization’s tier, limits and current usage.
Permission: billing:read
curl https://api.windpaint.ai/v1/billing/limits \
-H "Authorization: Bearer $WINDPAINT_API_KEY"
{
"data": {
"organization_id": "0b7c5e9a-2f4d-4a61-9c3e-8d2f1a6b7c40",
"tier": 1,
"tier_source": "purchases",
"lifetime_purchases_cents": 0,
"limits": { "concurrent_jobs": 5, "submits_per_minute": 60, "requests_per_minute": 1200 },
"usage": { "concurrent_jobs": 0, "submits_this_minute": 0, "requests_this_minute": 1, "resets_in": 42 },
"next_tier": {
"tier": 2,
"min_purchases_cents": 5000,
"limits": { "concurrent_jobs": 10, "submits_per_minute": 120, "requests_per_minute": 1200 }
},
"overrides": { "tier": null, "concurrent_jobs": null, "submits_per_minute": null }
}
}
The full field list is in the Billing reference. A few things to know:
usage.requests_this_minuteis the calling key’s count (or your user’s, from the dashboard) and includes this call, which itself counts toward requests per minute.usage.concurrent_jobscounts jobs already queued or running. It doesn’t include submits still being processed, so it can read one lower thandetails.currenton a concurrency429.next_tierisnullat tier 4 or when your limits are set by Windpaint (tier_source: "override").
The dashboard shows the same information under Settings → Billing → Rate limits.
Staying under the limits
- Keep a client-side cap on jobs in flight equal to your tier’s concurrency limit, and submit the next job when one finishes.
- Use webhooks, the event stream or
Prefer: waitinstead of tight polling loops. Status polls count toward requests per minute. - Spread a large batch over several minutes rather than sending it in one burst.