Errors and recovery
Every error returns an error object with a stable code. Find the code below to see how to fix the request or retry safely.
Error Response Format
code: machine-readable and stable. Switch on this.message: human-readable. It may change between versions, so use it for logs only.param: the request field that caused the error, when there is one.details: extra context, such asissuesfor validation orrequiredandbalancefor credits.doc_url: this page, at the section for the code.request_id: the same value as theRequest-Idheader. Include it when you contact support.
{
"error": {
"code": "validation_failed",
"message": "duration: Number must be less than or equal to 15",
"param": "duration",
"details": {
"issues": [{ "param": "duration", "message": "Number must be less than or equal to 15" }]
},
"doc_url": "https://masko.ai/docs/reference/errors#validation-failed",
"request_id": "req_3f9c2a71d0b84e6a9c1f5e22"
}
}Successful responses use { "data": ..., "meta"?: ... } instead; see requests and responses.
Error Codes
Some operations return their own codes: a 409 for an operation-specific conflict, and a 422 when a proposed graph is invalid. Correct the graph rather than retrying generation blindly.
Validation failed
validation_failed (400). The body or query is invalid. Fix the fields named in param and details.issues, then resend.
Unauthorized
unauthorized (401). The API key is missing, invalid or revoked. Send Authorization: Bearer masko_... with an active key.
Insufficient credits
insufficient_credits (402). details.required and details.balance give the numbers. Top up at masko.ai/billing or reduce the request.
Forbidden
forbidden (403). The key lacks permission, for example a read-only key sending a POST. Use a key with the right permission.
Not found
not_found (404). The resource does not exist or is outside the credential's workspace. Check the ID with a list endpoint.
Conflict
conflict (409). A revision, readiness or active-run conflict. Read the resource and details again, then retry with fresh values. With details.reason: "voice_required", the mascot needs a voice for a talking animation: set one with PUT /v1/mascots/{id}/voice.
Idempotency key reused
idempotency_key_reused (409). This account already used the key with a different workspace, API version, URL, content type or body. Restore the original inputs to retry, and use a new key only for an intentionally new operation.
Idempotency request in progress
idempotency_request_in_progress (409). A request with this key is running or its outcome could not be confirmed. Wait a few seconds and retry with the same key, since a new key could duplicate a committed write. Unconfirmed operations stay blocked after 24 hours; check the saved job or contact support.
Idempotency persistence failed
idempotency_persistence_failed (503). The operation ran and may have committed, but its replay receipt could not be saved. Retry only with the same key, and inspect the saved job or contact support before starting a new operation.
Invalid idempotency key
invalid_idempotency_key (400). The Idempotency-Key header must be 1 to 255 characters.
Rate limited
rate_limited (429). Too many requests. Back off before retrying, as shown in rate limit recovery.
Upstream error
upstream_error (502, 503, 504), or suggestion_failed (502). A provider Masko depends on failed. Accepted jobs keep running: recover the job, or retry later through a supported idempotent replay.
Internal
internal (500). Something failed on our side, and a write may have committed. Retry with the original idempotency key (a stored error replays unchanged) and check the outcome before starting a new request. Contact support with request_id if it persists.
Handling Insufficient Credits
Show the user how many credits are missing:
// HTTP 402
{
"error": {
"code": "insufficient_credits",
"message": "Not enough credits. Required: 140, balance: 50.",
"details": {
"required": 140,
"balance": 50
}
}
}Rate Limit Recovery
Back off exponentially, and honor the Retry-After header when present.
async function fetchWithRetry(url, options, maxRetries = 3) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const res = await fetch(url, options);
if (res.status !== 429) {
return res;
}
if (attempt === maxRetries) {
throw new Error('Rate limited after max retries');
}
// Use Retry-After header, or exponential backoff
const retryAfter = res.headers.get('Retry-After');
const seconds = retryAfter === null ? NaN : Number(retryAfter);
const dateDelay = retryAfter ? Date.parse(retryAfter) - Date.now() : NaN;
const delay = Number.isFinite(seconds) ? Math.max(0, seconds * 1000)
: Number.isFinite(dateDelay) ? Math.max(0, dateDelay)
: Math.pow(2, attempt) * 1000;
console.log(`Rate limited. Retrying in ${delay / 1000}s...`);
await new Promise((r) => setTimeout(r, delay));
}
}
// Usage:
const res = await fetchWithRetry(
'https://api.masko.ai/v1/mascots/MASCOT_ID/generate',
{
method: 'POST',
headers: {
'Authorization': 'Bearer masko_YOUR_API_KEY',
'Masko-API-Version': '2026-09-26',
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: 'image',
name: 'Hello',
image_prompt: 'waving hello',
}),
}
);A rejected 429 is safe to retry. A timed-out paid write is not: check the saved job or run first, and replay only through endpoints that support Idempotency-Key.
