Skip to content
Masko logomasko
Docs
Documentation

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 example issues for validation errors or required and balance for credits.
  • doc_url: link to this page, at the section for the code.
  • request_id: the same value as the Request-Id response 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

HTTPError CodeMeaningWhat To Do
400validation_failedInvalid request body or missing required fields.Check the message for which field failed validation. Fix the request and retry.
401unauthorizedMissing or invalid API key.Verify your Authorization: Bearer masko_... header is correct and the key has not been revoked.
402insufficient_creditsNot enough credits for this operation. Response includes required and balance.Top up credits at masko.ai/billing or reduce the request scope.
403forbiddenYou do not have access to this resource.Verify the resource belongs to your account. Check that your API key has the necessary permissions.
404not_foundThe 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.
409conflict or an operation-specific codeRevision, 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.
403forbidden 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.
422Operation-specific codeA proposed graph is invalid.Correct the graph or proposal; do not retry generation blindly.
502upstream_error or suggestion_failedAn upstream operation failed.Inspect the error; recover an accepted job or replay a supported idempotent operation.
429rate_limitedToo many requests.Back off before retrying.
500internalSomething 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.
503idempotency_persistence_failedThe 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.