Errors
The SteadyLink error body, every status code and error code, rate limits and their headers, request IDs, and when it is safe to retry.
On this page
Every SteadyLink API error has the same JSON shape, a standard HTTP status, and a request ID. Use this page to decide whether a failed request should be fixed, retried, or reported, and to find the exact meaning of an error code your integration received.
The S3-compatible API uses S3's XML errors instead; see S3 errors.
Error body#
{
"code": 403,
"message": {
"code": "insufficient_scope",
"required": "assets:write"
},
"requestId": "7d3c1f2e-9a4b-4c6d-8e1f-0a2b3c4d5e6f"
}Fields
codeinteger- The HTTP status code, repeated in the body.
messagestring | object- A sentence such as
"Upload session expired", or an object with a machine-readablecodeand extra fields. Branch onmessage.codewhenmessageis an object, and fall back to the HTTP status when it is a string. requestIdstring- The request ID, also returned in the
X-Request-Idheader.
Structured messages always have a code, and often a human-readable message inside them:
{
"code": 429,
"message": {
"code": "api_write_limit_exceeded",
"message": "Rate limit exceeded",
"retryAfter": 12
},
"requestId": "0b9e4d2a-6f1c-4a3e-8d7b-2c5f9e1a4b60"
}When the request body or query string does not match the route's schema (a missing field, a wrong type, a value out of range), the response is a 400 with the plain message "Validation error" and no field details. Check the request against the route's reference page.
Status codes#
| Status | Meaning | What to do |
|---|---|---|
400 | The request is malformed: schema validation failed, both Authorization and X-API-Key were sent, or the operation is not allowed in this state (for example deleting the current revision without force=true, or a malware-blocked replacement). | Fix the request. Do not retry it unchanged. |
401 | No credential, or the credential is invalid, expired, or revoked (Unauthorized, Invalid API key, API key expired or revoked). | Check the key or refresh the session. |
403 | The caller is authenticated but not allowed: missing scope, a workspace role without the permission, an inactive API entitlement, a plan feature that is not included, or delivery of a file blocked as infected. | Check the code in message. Retrying will not help. |
404 | The resource does not exist in the active workspace, or the caller cannot see it. | Check the ID and the workspace. |
409 | The resource is in a conflicting state: it already exists, is already processing, an idempotency key does not match, or a plan count limit was reached (feature_limit_reached, seat_limit_reached). | Read the resource again, then decide. |
410 | The thing you referenced expired: an upload session, a conversion, an invitation, a replacement request, a widget submission, or a collection link. | Create a new one. |
413 | Too large: the file exceeds the plan's upload size, or storing it would exceed the workspace storage allowance. | Upload a smaller file, free space, or upgrade. |
415 | Unsupported media: the declared type contradicts the file's bytes (MIME mismatch), the type is not accepted, or a transform was requested for a non-raster file. | Fix the content type or the request. |
422 | The request is well-formed but a value is not acceptable, such as an invalid webhook URL, delivery domain, or country code. | Fix the value named in message. |
423 | Delivery only: the requested revision has not finished its malware scan. | Retry after a few seconds. |
429 | A rate limit, usage limit, or concurrency limit was reached. | Wait for Retry-After, then retry. |
451 | Delivery only: the workspace's country policy blocks the requester's region. | Nothing to retry. |
500 | Unexpected server error. The body is "Internal Server Error". | Retry reads and idempotent writes with backoff; report it with the request ID if it persists. |
502 | An upstream service failed: storage could not save the bytes, an SSO provider could not be reached, or a webhook test endpoint did not answer with a 2xx. | Retry with backoff. |
503 | A feature is not configured on the server, or the service is temporarily unavailable. | Retry with backoff. |
504 | A gateway timed out before the API answered. | Retry reads and idempotent writes with backoff. |
Error codes#
The code inside a structured message, grouped by what caused it.
Access and entitlement#
| Code | Status | Meaning |
|---|---|---|
insufficient_scope | 403 | The API key lacks the scope this route needs. required names it, for example assets:write. |
insufficient_workspace_permission | 403 | The signed-in user's role does not allow the action. Includes permission and role. |
subscription_inactive | 403 | The workspace's paid plan is not active or its billing period has ended. Existing keys are kept and can still be listed and revoked in the dashboard, but they cannot authenticate until billing is active again. |
api_access_disabled | 403 | API access was turned off for this workspace by an administrative override. |
paid_plan_required | 403 | The workspace's plan does not include API access. |
business_plan_required | 403 | The feature, such as SSO, needs a Business or Enterprise plan. |
workspace_required | 400 | A user who belongs to several workspaces did not send X-Workspace-Id. |
Entitlement errors also carry message and plan. While a key is rejected for its entitlement, repeated attempts are themselves rate limited (see Rate limits).
Limits and quotas#
These share one shape:
{
"code": 413,
"message": {
"code": "upload_size_limit",
"resource": "upload_bytes",
"usage": 262144000,
"reserved": 0,
"requested": 262144000,
"limit": 104857600,
"resetAt": null,
"behavior": "hard_stop",
"upgrade": { "recommendedPlan": "personal", "url": "/dashboard/settings?section=plans" }
},
"requestId": "3a8f1c6e-2d4b-4e9a-b7c0-5f1e8d2a6c93"
}| Code | Status | Meaning |
|---|---|---|
upload_size_limit | 413 | The file is larger than the plan allows for one upload. |
storage_quota_exceeded | 413 | Storing the file would exceed the workspace storage allowance. Includes usedBytes, requestedBytes, and limitBytes. |
temporary_intake_storage_limit | 413 | Pending request and widget uploads would exceed the remaining storage. |
usage_limit_exceeded | 413 or 429 | A metered allowance for the billing period is used up (413 for storage, 429 for others). resetAt is when it renews. |
usage_throttled | 429 | Usage over the allowance is being slowed rather than stopped. |
delivery_quota_exceeded | 429 | The monthly delivery bandwidth allowance is used up. Sent with Retry-After: 3600. |
feature_limit_reached | 409 | A count limit for the plan, such as webhook endpoints, collections, or webhook deliveries. resource names it. |
seat_limit_reached | 409 | Inviting another member would exceed the plan's seats. |
api_key_limit_reached | 409 | The workspace has as many active API keys as its plan allows. |
s3_key_limit_reached | 409 | The user already has 20 active S3 app keys in this workspace. |
Plan limits are listed in Usage and limits.
Rate-limit codes#
| Code | Meaning |
|---|---|
api_burst_limit_exceeded | Too many API-key requests in 10 seconds, across the workspace. |
api_read_limit_exceeded | Too many API-key reads (GET, HEAD, OPTIONS) in one minute. |
api_write_limit_exceeded | Too many API-key writes in one minute. |
api_monthly_quota_exceeded | The workspace used its monthly API request allowance. |
api_expensive_operation_rate_exceeded | Too many expensive writes in one minute. |
api_expensive_operation_limit_exceeded | Too many expensive writes today. |
invalid_api_key_attempt_limit_exceeded | More than 30 requests with an invalid or revoked key from one IP address in a minute. |
disabled_api_key_attempt_limit_exceeded, disabled_api_key_workspace_limit_exceeded | Too many requests with a key whose workspace has no API entitlement (60 per minute per IP, 300 per minute per workspace). |
rate_limit_exceeded | A per-user limit for signed-in sessions (see below). |
conversion_archive_rate_limit_exceeded | More than 6 conversion archive requests in a minute. |
Every rate-limit body includes retryAfter in seconds, matching the Retry-After header.
Rate limits#
API-key limits are pooled per workspace, not per key: creating more keys does not raise them. A request counts against the burst window, then the per-minute read or write window, then the monthly allowance, and, for expensive writes, the per-minute and per-day expensive limits.
| Plan | Reads per minute | Writes per minute | Burst per 10 s | Expensive per minute | Expensive per day | Requests per month |
|---|---|---|---|---|---|---|
| Free | 120 | 30 | 30 | 10 | 250 | 50,000 |
| Personal | 300 | 75 | 75 | 25 | 2,500 | 250,000 |
| Pro | 1,200 | 300 | 300 | 120 | 25,000 | 2,000,000 |
| Business | 6,000 | 1,500 | 1,250 | 600 | 250,000 | 25,000,000 |
| Enterprise | 60,000 | 15,000 | 10,000 | 6,000 | 5,000,000 | 2,000,000,000 |
A write is expensive when its path contains /upload, /commit, /replace, /purge, /signed-url, or /retry, or when it is any write under /api/conversions. These are the operations that start scans, workers, or storage operations.
Signed-in sessions (the dashboard, or an accessToken) are not subject to the plan table. Their writes are limited per credential to 60 per minute, and delivery and download routes such as /a/{asset_id} allow 600 requests per minute per client when no API key is sent. Sign-in and other authentication routes allow 10 requests per minute per IP address. The S3-compatible API has its own per-key limits.
Response headers#
| Header | When | Value |
|---|---|---|
X-Request-Id | Every response | Your own X-Request-Id if you sent one that is ASCII and at most 128 characters, otherwise a generated UUID. |
RateLimit | API-key requests | limit=300, remaining=297, reset=41: the per-minute read or write window, with reset in seconds. |
RateLimit-Policy | API-key requests | "steadylink";q=300;w=60: the window's quota and length in seconds. |
X-RateLimit-Limit | API-key requests, and every 429 | Requests allowed in the window. |
X-RateLimit-Remaining | API-key requests, and every 429 | Requests left in the window. 0 on a 429. |
X-RateLimit-Reset | API-key requests, and every 429 | Unix time, in seconds, when the window resets. Note this is a timestamp, while reset in RateLimit is a number of seconds. |
Retry-After | 429 responses, and some quota errors | Seconds to wait before retrying. |
On a successful request the rate-limit headers describe the per-minute read or write window. On a 429 they describe whichever window was exceeded. Browsers can read all of these headers from cross-origin requests.
Send your own X-Request-Id (for example your trace ID) to find a request in both your logs and SteadyLink's. When you report a problem, include the request ID, the route, the status, and the time. Never include the API key or a signed token.
Retry policy#
| Situation | Retry? |
|---|---|
Network error or timeout, safe method (GET, HEAD, OPTIONS) | Yes, with exponential backoff and jitter. |
| Network error or timeout on a write | Only if the write is idempotent (it carries an Idempotency-Key), or after reading the resource to check whether it happened. |
429 | Yes, after Retry-After seconds. Monthly and daily quota errors will keep failing until the window resets. |
423 on delivery | Yes, after a short wait for the scan to finish. |
500, 502, 503, 504 | Yes for reads and idempotent writes, with backoff. Stop after a few attempts and report persistent 500s. |
409 | Read the resource first; retry only if the state now allows it. |
Other 4xx | No. Fix the request. |
Start backoff around 250 ms and double it on each attempt, with random jitter so many clients do not retry at once. Both SDKs do this for you on 429, 502, 503, and 504 (see TypeScript SDK retries and Python SDK retries).
Idempotency keys#
Upload completion (POST /api/upload-sessions/{id}/complete) has an explicit idempotency contract. Send an Idempotency-Key header and reuse it on every retry of the same completion:
- The first completion stores the key. A retry with the same key, or a completion of a session that is already finished, returns the session without doing the work twice.
- A completion with a different key while the session already has one returns
409 Idempotency key mismatch.
For any API-key request, an Idempotency-Key also identifies retries for metering, so a retried request with the same key counts once against the monthly management-operation allowance. It still counts against the per-minute rate limits. Other writes do not deduplicate on the key, so check the resource before retrying them.