Errors

The JSON body every failed request returns, what each HTTP status means, and which errors are safe to retry.

Every failed REST request answers with a JSON object that has an error field. Branch on the HTTP status first, then on error. The message is written for a person reading logs. Its wording can change between releases, so do not match on it.

The body shapes

Most errors use one shape: error names the kind of failure and message says what was wrong with this request.

{ "error": "BadRequestError", "message": "request body must be valid JSON" }

A few errors carry a code of their own and extra fields you can act on:

errorStatusExtra fields
not_found404path: the route does not exist. Check the method and path.
method_not_allowed405allow, plus an Allow header listing the methods the path accepts.
BadRequestError from PATCH /v1/agents/{id}/voice-stack400Sometimes language, reason and blockers ([{code, scope, detail}]): a language of the voice stack cannot be published. reason is the first blocker's code, for example voice_id_missing when no voice is chosen for that language; see A voice selection must name an exact voice.
realtime_config_not_runnable400reason and blockers ([{code, scope, detail}]): the agent could not run a call; see Customer applications.
outbound_call_queue_full429retry_after_seconds, plus a Retry-After header.
platform_maintenance503retry_after_seconds, plus a Retry-After header. See Maintenance windows.
WorkflowRevisionConflictError409current: the workflow draft that won; reload it before retrying.
internal_error5xxrequest_id. Quote it to support.

A server error that Skysay did not anticipate never shows its internal cause. You get a fixed message and an id instead:

{
  "error": "internal_error",
  "message": "The server could not complete this request. Quote request_id when contacting support.",
  "request_id": "a1b2c3d4e5f60718"
}

The request_id is unique to that one request. Keep it in your own logs beside the request that failed, and quote it to support so the failure can be found.

Some 5xx answers are designed rather than unexpected, and they are not this shape: a 503 webhook_retry_requested asking a provider to redeliver, a 503 platform_maintenance during a scheduled maintenance window, or a 502 kyc_provider_error naming a verification provider's own status. Those keep their own error code and carry no request_id.

What each status means

StatuserrorWhat it meansWhat to do
400BadRequestErrorThe request is malformed or breaks a rule: body is not a JSON object, a required field is missing, a value is out of range. The message names the field.Fix the request. Retrying it unchanged fails the same way.
401UnauthorizedErrorNo key, a key that is not a Bearer token, or a key that is invalid or revoked.Check the Authorization: Bearer sky_… header, or create a new key.
402InsufficientBalanceErrorThe wallet cannot cover what the request would reserve.Top up; see Billing.
403ForbiddenErrorThe key lacks a scope (missing required scope: calls:read), or the request names a workspace other than the key's own.Add the scope to the key, or remove the foreign organization_id.
404NotFoundErrorThe id does not exist, or it belongs to another workspace. The two are deliberately indistinguishable.Check the id and the key's workspace.
404not_foundNo such route.Check the method and path.
405method_not_allowedThe path exists but not with this method.Use a method from allow.
409ConflictError and othersThe request conflicts with current state: an Idempotency-Key reused for a different request, a stale revision, a draft that already exists.Read the current state, then decide. Do not blindly retry.
413PayloadTooLargeErrorThe request body, or an uploaded verification document, is over its size limit or quota.Send less.
415UnsupportedMediaTypeErrorThe request has a Content-Encoding other than identity.Send the body uncompressed.
422variesA simulation-suite request is well formed but cannot run as asked.Read message and the extra fields.
423SuspensionErrorA workspace control refused the action: a spend or velocity cap, the live-call concurrency limit, a suppression, or a suspension.Wait for the cap window, or contact support for a suspension.
429outbound_call_queue_fullThe outbound call queue is full.Retry after Retry-After seconds.
503platform_maintenanceSkysay is in a scheduled maintenance window and is not accepting new work.Retry after Retry-After seconds, with the same Idempotency-Key. See Maintenance windows.
500–504internal_error or a named errorSkysay could not complete the request.Retry with backoff, using an Idempotency-Key on POST /v1/calls. If it persists, send the request_id to support.

403 and 404 on a by-id read follow a fixed rule so a status can never reveal which ids exist in another workspace. See A resource outside your workspace is absent, not forbidden.

Maintenance windows

Skysay occasionally runs a short, scheduled maintenance window. While one is open, requests that would start new work answer 503 with "error": "platform_maintenance", a retry_after_seconds field and a Retry-After header:

{
  "error": "platform_maintenance",
  "message": "Skysay is undergoing scheduled maintenance. Please retry in a few minutes.",
  "retry_after_seconds": 300
}

Refused during a window:

  • placing a call (POST /v1/calls) or sending an SMS (POST /v1/messages);
  • incoming calls to your numbers. The caller is not answered, as when a number is suspended. Each call is recorded as failed with the reason platform_maintenance, so it appears in your call history, and nothing is charged;
  • starting a browser test or Talk session, a Chat turn, or a simulation run;
  • creating an agent (including through POST /v1/agent-endpoints), deleting an agent endpoint, and changing an agent's configuration, voice stack, knowledge bases, post-call extraction or workflow, including publishing. Setting an agent to inactive, with no other change in the same request, still works;
  • launching or resuming a campaign.

Not affected:

  • calls already in progress keep going, including their transfers and an agent's SMS sent during the call;
  • queued calls and running campaigns wait. A queued call is not failed and a campaign contact is not used up; they continue once the window closes. A window does not extend a campaign's end time: if ends_at passes during a window, the campaign ends then, like any campaign whose end time arrives;
  • wallet top-ups and payments, delivery reports and incoming SMS;
  • reading anything, and pausing or cancelling a campaign.

The same actions taken through MCP tools are refused too: the tool result is marked as failed and carries this error.

A refused request changed nothing, so retrying it after the window is safe. Keep the same Idempotency-Key on POST /v1/calls.

Examples

A missing key:

curl -s "$SKYSAY_BASE_URL/v1/agents"
{ "error": "UnauthorizedError", "message": "missing Authorization Bearer token" }

A body that is not JSON:

curl -s -X POST "$SKYSAY_BASE_URL/v1/agents" \
  -H "Authorization: Bearer $SKYSAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{not json'
{ "error": "BadRequestError", "message": "request body must be valid JSON" }

An id that is not in your workspace:

curl -s "$SKYSAY_BASE_URL/v1/agents/agent_doesnotexist" \
  -H "Authorization: Bearer $SKYSAY_API_KEY"
{ "error": "NotFoundError", "message": "agent not found" }

Retrying safely

Retry only 429 and 5xx, with exponential backoff. A call that outbound policy refuses is not an error: POST /v1/calls answers 201 with status: "policy_blocked" or "needs_review", as described in the curl quickstart. 4xx answers describe the request, and the same request gets the same answer. For POST /v1/calls, always send an Idempotency-Key header: a retry with the same key and body returns the original call instead of dialling twice. See Customer applications.

MCP clients see the same failures as JSON-RPC tool errors, for example missing MCP scopes: calls:write; see Scopes & permissions.

On this page