API
Rate limits
Per-endpoint-class rate limits, how to read 429 responses, and how to back off correctly.
ProductOS rate-limits per user (API tokens count against the user who issued them) per endpoint class. Limits are independent, exhausting writes doesn't affect reads.
Limits by class
| Class | Limit | Endpoints |
|---|---|---|
| List reads | 120 / minute | GET /api/v1/{entity} |
| Single reads | 240 / minute | GET /api/v1/{entity}/{id}, GET .../links/{junction} |
| Writes | 60 / minute | POST, PATCH, DELETE on entity routes, POST .../restore, link and unlink calls |
| Bulk | 30 / minute | POST /api/v1/{entity}/bulk (each call is one hit, whatever its size) |
| Export | 6 / minute | GET /api/v1/{entity}/export |
| AI | Per route, 4 to 60 / minute (chat 20, ProductSense 10, embedding 10) | POST /api/ai/*, in-app session only |
| Public widget submission | 10 / minute per IP, 1,000 / day per key | POST /api/v1/public/widget/feedback |
| Feedback API, secret key | 120 / minute and 10,000 / day per key; read-backs and lists 300 / minute per key | POST /api/v1/public/feedback, GET /api/v1/public/feedback, GET /api/v1/public/feedback/{id} |
| Feedback API, publishable key | 10 / minute per IP, 1,000 / day per key | POST /api/v1/public/feedback |
| MCP sign-in | Registration 30 / hour per IP; token exchange and refresh 120 / minute per IP; revocation 60 / minute per IP | POST /api/oauth/register, POST /api/oauth/token, POST /api/oauth/revoke |
The same user calling from multiple devices/tokens shares the bucket, limits are per user, not per token. Different users in the same workspace have independent buckets.
How limits work
Each class is a fixed-window bucket. The window opens on your first request and resets one minute later (one hour for MCP client registration, one day for the daily Feedback API caps); it is not aligned to wall-clock minutes. So if your first write lands at 12:00:30 and you make 60 writes in the next second, the 61st waits until 12:01:30.
The 429 response
{
"error": {
"code": "rate_limited",
"message": "Too many requests",
"details": {
"retryAfterSeconds": 18
}
}
}details.retryAfterSeconds is the number of seconds until the bucket resets; read it from the body. The Feedback API and the MCP sign-in endpoints also send it as a Retry-After header. There are no X-RateLimit-* headers.
Backoff strategy
async function withRetry<T>(fn: () => Promise<T>, attempts = 3): Promise<T> {
for (let i = 0; i < attempts; i++) {
try {
return await fn();
} catch (err) {
if (err instanceof ApiError && err.code === "rate_limited" && i < attempts - 1) {
const wait = ((err.details as { retryAfterSeconds?: number } | undefined)?.retryAfterSeconds ?? 1) * 1000;
await new Promise((r) => setTimeout(r, wait));
continue;
}
throw err;
}
}
throw new Error("unreachable");
}For bulk operations, stagger your requests instead of relying on retries:
- Writing 1000 features? Send 50/min for 20 minutes, not 1000 in one burst.
- Reading 1000 rows? Use
limit=200per request and 5 requests, not 1000 individualGET /api/v1/.../{id}calls.
Bulk endpoints
For large updates and deletes, prefer the bulk endpoint:
POST /api/v1/feedback/bulk
{ "ids": ["FDBK-000042", "FDBK-000043"], "action": "update", "patch": { "status": "planned" } }action is update or delete (a trash, where the entity has one), and ids takes up to 200 existing rows per call. Each call is one hit against its own 30 / minute bucket, regardless of payload size. Bulk does not create rows: for bulk creates, stagger your POSTs under the writes limit.
What's not rate-limited
GET /api/v1/health: uncapped, intended for liveness probes.GET /api/v1/openapi: no per-user limit.- Webhook inbound (e.g.
/api/integrations/github/webhook), provider-side rate-limited by GitHub/Linear/etc., we don't double-cap.
Upgrading limits
Enterprise customers can request higher limits per class via support. Self-serve limit adjustment isn't possible, limits exist to protect the platform and we want a conversation about the use case before lifting them.
Next
- Errors: full error code reference including
rate_limited. - Pagination: how to read large datasets without hitting list-read limits.