跳转到内容

sdkStartVoiceCall Reference

此内容尚不支持你的语言。

mutation StartVoiceCall($input: SdkStartVoiceCallInput!) {
sdkStartVoiceCall(input: $input) {
sessionId
agentId
livekitUrl
livekitToken
roomName
expiresAt
}
}

Headers: x-api-key: <YOUR_API_KEY>, x-huat-platform: customer (see Authentication). Call it from your server. The API key must never reach a browser or app.

It starts a session exactly as sdkStart does (workspace scoping, the template gate and usage metering all apply) and mints a LiveKit join token for one fresh room. Variables go in variables:

{
"input": {
"agentId": 7,
"clientId": "call-2026-10-19-0042",
"context": { "source": "web-call", "plan": "trial" }
}
}
FieldTypeRequiredNotes
agentIdInt!YesAn agent in the API key’s workspace; NOT_FOUND otherwise. Its published workflow answers every turn, so make it voice-safe.
clientIdStringNoYour own identifier for the caller or the call, shown on the session. At most 255 characters. Generated when omitted.
contextJSONNoA flat per-call object, exposed to the workflow as ctx.session.context. Same rules as the context of sdkStart. Pass it through variables, not as an inline literal.
engineVoiceEngineNoWORKFLOW (default): the agent’s published workflow answers every turn. LIVE: a speech-to-speech model answers from the agent’s instructions only (no workflow, no tools); the transcript is recorded after the call, and it is refused unless the deployment has the live engine enabled. Pilot, staging only.
FieldTypeNotes
sessionIdInt!The session created for this call (the conversation record).
agentIdInt!The agent that will answer.
livekitUrlString!The LiveKit server URL (wss://...) to connect the LiveKit client to.
livekitTokenString!The join token for the one room of this call. See security.
roomNameString!The name of the room created for this call.
expiresAtDateTime!When the token stops being accepted for joining, about two minutes after the request. A call already joined continues.
  • It can join exactly one room, the one for this call, and no other.
  • The caller may publish audio and subscribe. There is no data channel and the caller cannot edit metadata.
  • It asks LiveKit to dispatch Wetel’s voice worker into the room as soon as the caller joins.
  • It is valid to join for about two minutes. Join immediately; mint a new call for a later attempt.
SituationCodeMessage
No API key headerUNAUTHENTICATEDMissing X-Api-Key header
Unknown or revoked keyUNAUTHENTICATEDInvalid API key
Voice not enabled on the environmentFORBIDDENVoice calls are not enabled on this environment
Platform header missingFORBIDDENPlatform is not specified!
Agent not in the key’s workspaceNOT_FOUNDthe agent was not found
The API key’s daily call cap reachedINTERNAL_SERVER_ERROR with extensions.status 429Daily voice call limit reached for this API key; try again tomorrow (UTC)
  • Per client address: 60 a minute by default, shared by every caller behind one server (that is, all of your calls from one server count together).
  • Per workspace: 30 call starts a minute. The ceiling is counted per API process.
  • Per API key per day: 1,000 call requests per UTC day by default (set per environment), counted across all servers. It resets at 00:00 UTC. Every request that passes the checks above counts, including one whose call then fails to start. Once reached, the mutation answers HTTP-429-style (extensions.status 429) with the message Daily voice call limit reached for this API key; try again tomorrow (UTC): do not retry that one until the next UTC day. This is a ceiling against a runaway script, not a plan limit.
  • The over-limit response is an error with a message and may not carry a specific extensions.code. Do not branch on a code: treat any error from this mutation as retryable after a short wait, with backoff, unless it is one of the codes above.
  • Usage is metered with the session (speech and model usage). A per-minute voice price will be published later.

The join token’s contents are readable by the caller’s browser. It carries the credential the voice worker uses for that one session, the same kind of session credential sdkStart hands to the chat SDK. So:

  • Treat a token as belonging to one caller. Never hand one caller’s token to another.
  • Never log it, share it or paste it into a support ticket.
  • Never put your API key in a page. Only your server calls this mutation.
  • Authenticate your own caller before you mint a call, and rate-limit your own start endpoint.

A planned change replaces the readable credential with a one-time handle (see the roadmap).

See the quickstart for a complete Node example and the browser example.

A curl equivalent:

Terminal window
curl -sS "$WETEL_API_URL" \
-H 'content-type: application/json' \
-H "x-api-key: $WETEL_API_KEY" \
-H 'x-huat-platform: customer' \
--data '{
"query": "mutation($input: SdkStartVoiceCallInput!){ sdkStartVoiceCall(input:$input){ sessionId livekitUrl roomName expiresAt } }",
"variables": { "input": { "agentId": 7 } }
}'

(This example leaves out livekitToken on purpose, so it does not land in your shell history or logs.)

A call is a session. Read its transcript with the sessions API. A call nobody joins is closed by the platform: about 10 minutes after the request (checked every 5 minutes, so within about 15 minutes) its session is set to EXPIRED, with no evaluation and no events. A call counts as joined once the caller’s first turn reached the session.

For a LIVE call the voice worker stores the transcript afterwards with sdkRecordVoiceTranscript, using the call’s own token. It is not meant to be called from a browser or with an API key: it appends the spoken lines (at most 200 lines, 60,000 characters) to the call’s session once, without running the agent.

startOutboundCall makes one of your AI employees place a phone call. It uses the signed-in workspace owner’s login (a dashboard JWT), not an API key, and only the workspace owner may call it.

mutation StartOutboundCall($input: StartOutboundCallInput!) {
startOutboundCall(input: $input) {
callId
status
maxDurationSeconds
}
}
{
"input": {
"agentId": 7,
"toNumber": "+60123456789",
"selfTest": true,
"maxDurationSeconds": 120
}
}
FieldTypeRequiredNotes
agentIdInt!YesThe AI employee that places the call. NOT_FOUND when it is not in your workspace.
toNumberString!YesE.164: a plus and 8 to 15 digits. For a self-test it must equal the phone number on the owner’s own profile and start with an allowed prefix.
selfTestBoolean!YesMust be true. false is refused with “not available yet”.
maxDurationSecondsIntNoHard maximum call length, 15 to 300. Default 120. The call is ended at this limit.
engineVoiceEngineNoWORKFLOW (default) or LIVE, as for sdkStartVoiceCall. LIVE is refused unless the deployment has the live engine enabled.

Allowed destination prefixes are +1 and +60 unless the workspace sets its own list.

FieldTypeNotes
callIdInt!Id of the outbound call record.
statusString!Always queued: the dial happens asynchronously a moment later.
maxDurationSecondsInt!The maximum call length that applies.

The mutation returns at once. The phone rings a moment later and the AI says one short opening sentence that identifies it as an AI assistant. The result never contains a join token or the phone number.

SituationCode
Outbound calling is off for the environment or for the workspace, or the platform kill switch is onFORBIDDEN
The workspace’s daily call or minute limit is reachedFORBIDDEN
The destination prefix is not allowed, or toNumber is not your own profile numberFORBIDDEN
Caller is not the workspace ownerFORBIDDEN
toNumber is not valid E.164, selfTest is false, the duration is outside 15 to 300, or LIVE is not enabledBAD_REQUEST
agentId is not in your workspaceNOT_FOUND

Each call record moves through these states: dialing, ringing, answered, then an end state: completed, voicemail, no_answer, busy, declined or failed. A watcher records completed with the call’s duration and end reason once an answered call ends; the voice worker detects voicemail, and a voicemail call is hung up. The mutation itself only ever reports queued. To read a call’s later states, use outboundCall (see below), which is on staging and ships to production with the next release.

Reading a call: outboundCall and outboundAvailability

Section titled “Reading a call: outboundCall and outboundAvailability”

Both use the signed-in workspace owner’s login (a dashboard JWT) and are refused for anyone else. They are reads: they never place a call.

query OutboundAvailability {
outboundAvailability {
available
reason
phoneLast4
phoneCountry
defaultDurationSeconds
}
}
query OutboundCall($id: Int!) {
outboundCall(id: $id) {
id
status
toCountry
toNumberLast4
durationSeconds
endReason
sessionId
}
}

outboundAvailability tells a screen whether the “Call my phone” self-test could start now, so a button can be hidden or disabled without placing a call. available is false with a plain reason: NOT_ENABLED, NO_PHONE, COUNTRY_NOT_ALLOWED, DAILY_LIMIT or UNAVAILABLE. NOT_ENABLED covers every kind of “off” together on purpose. It does not check the AI employee or whether the voice worker is online, and it does not verify that the profile number belongs to the owner. The usage limits are hidden unless the workspace has calls enabled, and the full number is never returned (only phoneLast4 and phoneCountry).

outboundCall(id) returns the state of one call in your workspace, using the callId from startOutboundCall. An id from another workspace answers not found. Poll about every 2 seconds while the status is queued, dialing or ringing, every 5 seconds while answered, and stop at an end state (completed, voicemail, no_answer, busy, declined, failed, cancelled or blocked). It returns only the last four digits and the country of the called number, never the number, its hash or a join token. endReason is a short machine code, not text to show as it is.

The destination number is never stored in clear. Wetel keeps a per-workspace keyed hash of it, plus the last four digits and the country.