Limits, quotas, and retention
This page lists the rate limits, request size limits, storage quota, and data retention periods that apply to every account.
Requests per minute
Rate limits use a sliding 60-second window and are counted in three independent scopes:
| Scope | Production default | Sandbox |
|---|---|---|
| Origin (IP address), before authentication | 600 per minute | 600 per minute |
| Account | 1,000 per minute | 500 per minute |
| Credential | 300 per minute | 150 per minute |
Every authenticated response admitted by the limiter includes RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset headers for the most restricted of the three scopes. When a request exceeds a limit, the API returns 429 with the same headers plus Retry-After, and the response body reports the state of each scope:
{
"error": {
"code": "rate_limit_exceeded",
"message": "Too many requests.",
"retry_after_seconds": 12,
"blocked_by": ["key"],
"limits": [
{ "scope": "instance", "limit": 1000, "window_seconds": 60, "remaining": 640, "retry_after_seconds": 0 },
{ "scope": "key", "limit": 300, "window_seconds": 60, "remaining": 0, "retry_after_seconds": 12 }
]
}
}In limits, the instance scope is the account limit and the key scope is the credential limit. blocked_by lists the scopes that rejected the request.
A 429 response executes nothing and does not count against the window. Wait retry_after_seconds before you retry, and spread your load over time instead of retrying in bursts. If the rate limiter is unavailable, the API returns 503 rate_limiter_unavailable: it fails closed and never admits a request without counting it.
Higher quotas are arranged with Cademí and apply to the account. A credential can have its own limit, which is always less than or equal to the account limit. Requests made with an administrator's OAuth access token count against the account limit and against a per-administrator limit with the credential default; an administrator cannot have a limit of its own. GET /capabilities reports the limit that applies to the caller. GET /account/usage reports consumption per window (1h, 24h, 7d, 30d), per outcome, and per credential, along with the limits in effect.
Request size
| Limit | Value |
|---|---|
| JSON request body | 1 MiB (413 payload_too_large), measured on the bytes read even when the request is sent chunked without a Content-Length header |
| JSON nesting depth | 16 levels (400 json_too_deep) |
| Items in an array | 1,000 (422 too_many_items) |
| Plain text | 64 KiB per field |
| Rich text | 512 KiB per field |
| Operation batch | Depends on the operation type, up to 1,000 items (422 too_many_items) |
| Open operations per account | 100 in queued or running, counted per environment (operation_backlog_exceeded) |
| File | Always through an upload session. The maximum size depends on purpose, up to 100 MiB, and the account can set a lower limit for editor and document. See Files and uploads |
| IDs in a policy selector | 1,000 |
| Event streams | 5 per credential, 20 per account (429 too_many_streams) |
Storage quota
Each account has a storage quota of 50 GiB, unless a different quota was agreed with Cademí. The quota is shared with the accounts billed together with it, such as its replicas. Files attached to student questions do not count toward it.
The API checks the quota when you open an upload session, using the declared size, and again when you complete it, using the actual size. An upload that would take the storage in use above the quota returns 409 quota_exceeded. See Files and uploads.
GET /account/usage includes a storage object with the storage in use and the quota, in bytes. It does not depend on period:
{
"storage": { "used_bytes": 18253611008, "quota_bytes": 53687091200 }
}used_bytes comes from a stored total that is updated on each upload and deletion through the API and recalculated daily. Files added or removed in the dashboard can take up to a day to show in it.
Retention
What the API stores, and for how long:
| Data | Retention |
|---|---|
Events (GET /events) | 30 days |
| Webhook deliveries and attempts | At least 60 days after the delivery is created; a delivery that has not reached a final status is kept |
| Operations and items | 30 days from creation (expires_at) |
Audit trail (/requests, /audit-entries) | 30 days |
| Idempotency receipt | 48 hours |
| Previous policy revisions | 365 days |
| Declarative configuration plans | Until the plan expires |
After the retention period, the record no longer exists: a GET for a pruned event returns 404, and a cursor that points to it returns 410 cursor_expired. If your integration needs a longer history, consume events through webhooks or GET /events and store them on your side.