Error Codes
All API errors follow the same format. Here is how to handle each one.
Response Envelope
Every v1 response is wrapped in a consistent envelope. On success, the body is { "data": <resource>, "meta"?: { ... } }. On error, the body is { "error": { "code": "...", "message": "...", "details"?: { ... } } }. Paginated lists include meta.pagination = { total, limit, offset, has_more }. Generation endpoints return job details with data.poll_url.
Error Response Format
Every error response contains an error object:
code: machine-readable, stable. Switch on this.message: human-readable explanation.param: the request field that caused the error, when there is one.details: extra context, for exampleissuesfor validation errors orrequiredandbalancefor credits.doc_url: link to this page, at the section for the code.request_id: the same value as theRequest-Idresponse header. 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"
}
}Error Reference
| HTTP | Error Code | Meaning | What To Do |
|---|---|---|---|
| 400 | validation_failed | Invalid request body or missing required fields. | Check the message for which field failed validation. Fix the request and retry. |
| 401 | unauthorized | Missing or invalid API key. | Verify your Authorization: Bearer masko_... header is correct and the key has not been revoked. |
| 402 | insufficient_credits | Not enough credits for this operation. Response includes required and balance. | Top up credits at masko.ai/billing or reduce the request scope. |
| 403 | forbidden | You do not have access to this resource. | Verify the resource belongs to your account. Check that your API key has the necessary permissions. |
| 404 | not_found | The resource does not exist or is outside the credential’s workspace. | Check the resource ID in the URL. Use the list endpoints to find valid IDs. |
| 409 | conflict or an operation-specific code | Revision, idempotency, readiness, or active-run conflict. A talking animation for a mascot without a voice returns details.reason: "voice_required". | Read the current resource and error details before retrying. Give the mascot a voice with PUT /v1/mascots/{id}/voice. |
| 403 | forbidden with details.reason: "voice_locked" | Adding a voice unlocks once the account has spent $50 on Masko. details has paid_cents and required_cents. | Buy credits at masko.ai/billing, then try again. |
| 422 | Operation-specific code | A proposed graph is invalid. | Correct the graph or proposal; do not retry generation blindly. |
| 502 | upstream_error or suggestion_failed | An upstream operation failed. | Inspect the error; recover an accepted job or replay a supported idempotent operation. |
| 429 | rate_limited | Too many requests. | Back off before retrying. |
| 500 | internal | Something went wrong on our side; a write may have committed. | Reuse the original idempotency key. A stored error replays unchanged; check the operation outcome or contact support before starting a new request. |
| 503 | idempotency_persistence_failed | The execution outcome could not be saved for replay. | Keep the same key and inspect the saved job; contact support if the reservation stays blocked. |
Error Codes
Validation failed
validation_failed (400). The request body or query is invalid. Read param and details.issues, fix the request, and send it again.
Unauthorized
unauthorized (401). The API key is missing, invalid or revoked. Send Authorization: Bearer masko_... with an active key.
Insufficient credits
insufficient_credits (402). The account does not have enough credits. details.required and details.balance give the numbers. Top up at masko.ai/billing.
Forbidden
forbidden (403). The key cannot do this, 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 belongs to another workspace. Check the ID with a list endpoint.
Conflict
conflict (409). The resource changed or is busy. Read it again and retry with fresh values.
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 for a retry. 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. Unconfirmed operations stay blocked after 24 hours; check the saved job or contact support if this persists. A new key could duplicate a committed write.
Idempotency persistence failed
idempotency_persistence_failed (503). Execution finished, but its replay receipt could not be confirmed. The operation may have committed. Retry only with the same key; inspect the saved job or contact support before attempting 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.
Upstream error
upstream_error (502, 503, 504). A provider Masko depends on failed. Retry later; accepted jobs keep running.
Internal
internal (500). Something went wrong on our side. Retry after a short delay and include request_id if you contact support.
Handling Insufficient Credits
The 402 response includes required and balance fields under error.details so you can show users exactly how many credits they need:
// HTTP 402
{
"error": {
"code": "insufficient_credits",
"message": "Not enough credits. Required: 140, balance: 50.",
"details": {
"required": 140,
"balance": 50
}
}
}Rate Limit Recovery
When you hit a 429, use exponential backoff for robustness. If a Retry-After header is present, honor it.
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',
}),
}
);Retrying a rejected 429 is different from retrying a timed-out paid write. After an uncertain write response, check the saved job or run first. Only endpoints that document idempotency support can safely replay the same persisted operation key.