REST endpoints

The key REST endpoints — agents, web/trial calls, numbers, calls, messages, and wallet/usage.

No MCP client? Every capability is also a plain JSON REST endpoint under /v1. Authenticate with the same scoped sky_ key as a Bearer token. The OpenAPI spec (openapi.json) is the machine-readable source of truth; this is the human map. To try requests, use the Skysay API Postman workspace. For copy-pasteable requests and responses from a new key to a placed call, follow the Quickstart with curl. Failed requests are described in Errors.

A workspace key never needs organization_id: leave it out of query strings and request bodies and the key's own workspace is used. See Authentication & keys.

Building a customer application?

Start with Customer applications, then connect live data through Integrations. The Generated API reference lists the current customer SDK operation set from the canonical OpenAPI snapshot.

Bounded list pages

The following growing lists return an object, not a bare array. Read the named item array and the pagination object together:

{
  "agents": [{ "id": "agent_example" }],
  "pagination": {
    "limit": 50,
    "returned": 1,
    "has_more": false,
    "next_cursor": null
  }
}

returned counts this page, not the entire collection. A page is complete only when has_more is false. Otherwise send next_cursor unchanged as the next request's cursor, keeping the same organization, project and other filters. limit must be a whole number between 1 and the route's maximum; out-of-range and malformed limits or cursors return 400, not a silently shortened page. All pages are newest first, with a stable creation-order tie break. A walk does not repeat rows or skip older rows that remain present throughout it; it is not a cross-request database snapshot and does not include newly added rows ahead of its cursor.

GET pathItem keyDefault limitMaximum
/v1/agentsagents50200
/v1/numbersnumbers50100
/v1/agent-endpointsagent_endpointsRequired200
/v1/number-requestsnumber_requests50100
/v1/phone-serversphone_servers50200
/v1/trial-callstrial_calls50100
/v1/api-keysapi_keys50200
/v1/customer-onboardingcustomer_onboarding50200
/v1/gateway-deploymentsgateway_deployments50200
/v1/end-usersend_users50200
/v1/conversationsconversations50200
/v1/messagesmessages100500
/v1/usage-recordsusage_records100500
/v1/wallet/ledgerentries100500
/v1/billing/invoicesinvoices100500
/v1/audit-eventsaudit_events100500
/v1/webhook-deliveriesdeliveries50500

The ledger retains its currency and totals, but totals cover this page's entries only; use GET /v1/wallet for the wallet balance. Invoice pages keep their currency. Delivery pages keep event_catalog and retry_policy. Number-request list items do not embed a provider mutation's audit history. GET /v1/calls retains its existing array and paging-header contract; call event responses are not changed by these list envelopes.

Migrating a list consumer

This replaces the previous bare-array contract on the listed routes. Do not interpret an unexpected response shape or a failed request as an empty list. In particular, a find-or-create client must finish a strict page walk before deciding a matching object does not exist; otherwise it can create duplicates. GET /v1/agent-endpoints requires an explicit limit so an unupdated client receives 400 instead of misreading the new envelope as no endpoints.

For each successful response, require an item array, positive integer limit, integer returned equal to the array length, boolean has_more, and a nonempty string next_cursor while more pages remain (otherwise null). Refuse repeated cursors and stop with an error if your own page budget is exhausted; never act on a partial collection as if it were complete. Read a known resource by its ID instead of searching only the first page. Keep project checks when replacing a project-filtered list with an ID lookup.

Agents

MethodPathPurpose
POST/v1/agentsCreate an agent (external_audio, external_text, hosted).
GET/v1/workspace-contextDiscover the key's own organization and a bounded project page.
GET PATCH/v1/agents/{id}/voice-stackRead or set the hosted agent's STT/LLM/TTS stack.
GET/v1/agents/{id}/workflowRead a draft/revision and bounded published-version summary before editing or publishing.
GET PUT/v1/agents/{id}/variablesRead or replace the agent's call variables, SMS templates and caller lookup.
POST/v1/agents/{id}/messagesSend one of the agent's published SMS templates (project-scoped).

Bootstrap and configure a hosted agent

Start a machine client with GET /v1/workspace-context and account:read. The bearer key decides the organization; this route does not accept an organization_id selector. It returns only the safe organization id/name plus a stable, explicitly bounded page of project id/name/environment values. Use the returned page metadata to continue discovery. It also works for a new workspace with no agents.

For a hosted agent, GET /v1/agents/{id}/voice-stack needs agents:read and PATCH /v1/agents/{id}/voice-stack needs agents:write. Select platform-managed STT, model, and TTS components by their catalog selectors. For BYOK, first create the credential through the separate write-only hosted-AI credential API, then supply only that existing credential reference in component_credential_bindings. Voice-stack reads never return a secret, ciphertext, credential-binding ID, or provider credential; unsupported component, language, and credential combinations are rejected on update.

Voice settings belong to the voice-stack route

PATCH /v1/agents/{id} edits the agent's own fields: its name, status, webhook and audio-stream URLs, prompt, first message, mode, tools, metadata and recording flag. It does not save voice settings.

These six names belong to PATCH /v1/agents/{id}/voice-stack (MCP tool set_agent_voice_stack):

SettingWhat it configures
turn_takinginterruption sensitivity, endpointing, and the automatic call limits
voice_polishend-of-call phrase, delivery profile, acknowledgements, tool-wait
audio_environmentthe pinned ambience preset
follow_callerthe automatic multilingual policy
languagesthe selected language set
language_profilesper-locale voice, transcriber and greeting overrides

Sending any of them to PATCH /v1/agents/{id} returns 400 naming the route that does save them. Until this release the same request returned 200 and silently discarded the setting, so a limit you believed was saved was not, and the call then ran on the platform default. If you have code that sets turn_taking through the plain agent route and appears to succeed, it has never been taking effect; move it to the voice-stack route.

The MCP tool update_agent is the same route and behaves the same way: it is documented as applying "the same generic PATCH fields and validation as PATCH /v1/agents/{agent_id}", and from this release that includes the refusal. An agent script that sets turn_taking through update_agent now gets an error where it used to get a success it should never have had.

Unrelated fields your client happens to send are still accepted and ignored, as before. Only these six recognised-but-misplaced names are refused — they are not "unknown fields", so REST's no-op compatibility for genuinely unknown keys does not cover them.

A voice selection must name an exact voice

PATCH /v1/agents/{id}/voice-stack returns 400 if you send a TTS provider and model without a voice_id, whenever that provider has voices to choose from. The body names the language and says why:

{
  "error": "BadRequestError",
  "message": "Choose a voice for German.",
  "language": "de",
  "reason": "voice_id_missing",
  "blockers": [{ "code": "voice_id_missing", "scope": "agent", "detail": "" }]
}

Branch on reason or the blockers codes, not on the message: the codes are the same ones call admission and readiness report, and the message can change between releases.

Until this release such a save could return 200 while storing a stack with no voice. Nothing reported a problem until the agent was called, and the call then failed before the caller heard anything. Refusing the save is how you find out at the moment you make the change rather than on the next live call.

Two things are unchanged. Saving only a transcriber, with no voice selection in the request, is unaffected. And editing an older agent whose stored voice predates this rule still works: that repair path resolves the provider's own default voice rather than refusing your edit.

Before a workflow PATCH or publish, use GET /v1/agents/{id}/workflow?version_limit=20&version_offset=0 with agents:read. It gives you the editable draft/revision, validation, safe canvas layout, current published-version metadata, and a newest-first bounded version page to compare. An existing agent with no draft returns workflow_state: "not_created"; the read never creates a blank workflow, compiles, repairs, calls AI/providers, or writes state.

The Generated API reference and live OpenAPI document carry the exact request and response contracts.

Audio environments

MethodPathPurpose
GET/v1/audio-environmentsRead the immutable, versioned licensed ambience catalogue for the workspace.

GET /v1/audio-environments needs account:read. It is a local catalogue read: it does not contact a voice provider, call a carrier, change a selected environment, or return audio bytes. See Audio environments for the outgoing-ambience and caller-input contracts. A 503 means the packaged catalogue failed its integrity check; retry rather than inventing a preset or substituting another version.

Calls

MethodPathPurpose
POST/v1/trial-callsCreate an offline fixture with synthetic_only: true; not a live phone trial.
POST/v1/callsPlace a policy-gated outbound call.
GET/v1/callsList calls.
GET/v1/calls/{id}Call status + recording state.
GET/v1/calls/statsCall volume, outcome, talk time and spend over one or more date windows.
POST/v1/calls/{id}/cancelWithdraw a call that is still queued, or one held for review, before anything dials.
POST/v1/calls/{id}/hangupAsk a live call to hang up.

What a call-stats window counts

GET /v1/calls/stats needs calls:read. Each window you ask for comes back with since and until plus five integers:

FieldWhat it counts
callsEvery call created in the window, including ones a policy blocked and ones that were never billable.
answeredCalls somebody was on: completed, or answered, in progress, or with media started right now. Never a call the platform merely accepted.
failedCalls that ended with nobody on them: failed, busy, no answer, canceled, refused by a policy or live-call gate, or held for review.
talk_secondsTotal talk time across the window's recorded calls.
spend_minorThe wallet reservation estimate made when each call was admitted, in the wallet's minor units. This is not settled billing. Calls that never cost anything are left out: refused, held for review, or canceled before dialling.

answered and failed do not have to add up to calls. A call that is still dialing or still queued is in neither, and a call waiting on operator review is in neither. Read the two figures the endpoint gives you; do not derive one from calls minus the other.

End a live call

POST /v1/calls/{call_id}/hangup needs calls:write and takes an empty body. It is the request the Hang up button on the Calls page sends.

  • 202 means the request was recorded, not that the call is over. The worker holding the media session tears the call down, and the call then completes through its ordinary path with ended_reason hangup_requested, unless it had already started ending another way, in which case that reason is reported. Watch the call (or call.ended) for the ending; do not treat the 202 as one.
  • Asking again returns the original request (already_requested: true, the same hangup_requested_at). Until a worker acknowledges, the request reads status pending. If the worker reports that the teardown failed, the call is still up and a further request starts a fresh attempt; after three failed attempts the endpoint refuses, so the call is escalated rather than retried.
  • 409 when the call is not live: it has already finished, it is still queued for origination (use POST /v1/calls/{call_id}/cancel), or its teardown has already failed three times.
  • 404 for a call id that does not exist, belongs to another workspace, or is a Simulations Lab run. The three answer identically, so the endpoint cannot be used to find out whether someone else's call exists.

Post-call results describes the response fields.

Numbers

MethodPathPurpose
GET/v1/coverageCountry coverage matrix.
POST/v1/available-numbers/searchLoad exact numbers the carrier can sell now, optionally by area code or region. Nothing is reserved or charged.
GET/v1/available-numbersList the exact numbers currently loaded, with where each one is.
POST/v1/number-requestsRequest a managed number (operator-approved).
GET/v1/numbersList numbers and registration status.
GET PUT/v1/numbers/{id}/rulesRead / update number rules.

How long a regulated number takes to activate

Some countries require the carrier to approve a registration before a number carries traffic. Two fields describe that wait, and both are deliberately silent rather than guessing.

activation_estimate, on the requirements for a country and number type, is not always a duration, and must be checked before it is displayed as one. It holds a duration only where the number needs a carrier registration and the carrier publishes an activation time for that market: that published figure, passed through unchanged, usually measured in weeks. Where registration is required and the carrier publishes nothing — Estonia among them — it is empty, because Skysay does not substitute a figure of its own.

The other two values are routing states, not times:

  • instant_after_payment_and_policy_checks — the number needs no carrier registration at all.
  • manual_review — whether it needs one is not yet established.

Either describes the market, never your particular request.

The headline on GET /v1/end-users/{end_user_profile_id}/registration-status says how many of that profile's number registrations Skysay has submitted to the carrier. It is empty before anything has been submitted, and empty while a refusal or a request for more information is outstanding, because that is what needs your attention instead. The per-number headline on GET /v1/numbers/{number_id}/activation reports the same submission for one number and adds the carrier's published activation time when that market has one.

An empty headline is not a missing value: it means nothing has been submitted that it could report. For the stage a request has actually reached, read regulatory_status on the number, and blocking_items on the activation endpoint for what it is waiting on.

next_action on the activation endpoint is held to the same rule. It repeats the first blocking item when there is one. With nothing blocking it tells you to wait for carrier approval only when the registration really is open at the carrier; where nothing has been sent it says so, and where a registration exists but is not showing as open it says that instead of guessing either way.

Messages

MethodPathPurpose
GET/v1/conversationsList SMS conversations.
POST/v1/messagesSend a policy-gated SMS.

Capabilities

Outbound calls and SMS authorize against the workspace's capabilities record. Machine clients can inspect that record, manage optional destination restrictions, or request higher limits with these commands. See Policy & outbound for what each field means.

MethodPathPurpose
GET/v1/organizations/{organization_id}/capabilitiesThe record, its calling rules, the selectable profiles, and live usage against its caps.
POST/v1/organizations/{organization_id}/capabilitiesRequest or replace the selection. profiles is required, and so is a destination choice: all_countries: true or a countries allowlist.
POST/v1/organizations/{organization_id}/capabilities/accept-policyAccept a policy version without changing what the record authorizes.
POST/v1/organizations/{organization_id}/capabilities/destination-restrictionsSave optional customer narrowing with the current destination_revision; countries: null removes only this extra restriction.
POST/v1/organizations/{organization_id}/capabilities/limit-requestsRequest higher limits with a reason; does not change caps, prices or wallet funds.

For an explicit profile-selection request, the destination choice is all_countries: true (every destination except sanctioned countries) or countries naming at least one ISO 3166-1 alpha-2 code. A request with neither, with an empty list, or with an entry that is not a two-letter code once normalized answers 400, names the entry, and writes nothing. The check is on shape, not on membership of the assigned ISO list, so a well-formed but unassigned code is stored as sent and then matches no destination — see Destination controls.

Wallet & usage

MethodPathPurpose
GET/v1/walletWallet balance, billing settings, top-up options, low-balance readiness.

The wallet's purchased availability and browser-only promotional credit are separate. See Billing and browser credit before treating a balance as available phone spending.

Activation checklist

MethodPathPurpose
GET/v1/activation-stateThe setup steps, each with complete and next_action, and the first incomplete step as next_step.

Pass project_id to read one project; leave it out for the whole workspace. metrics holds exact totals for that scope: agents, trial_calls, numbers, number_requests, calls (live calls; simulations are not counted) and sms_conversations, plus available_balance_minor. Every count is the full total, however many records the workspace holds.

Keys & scopes are identical to MCP

REST and MCP share the same scoped keys and the same fail-closed scope checks. See Authentication & keys.