Skip to content

Sessions and messages

A session is one conversation between an end user and one agent. Your server starts it with the API key and receives a session token (avatarToken) that is valid for that session only. Sending a message returns true at once; the reply is generated asynchronously and stored as a message on the session, so a server-to-server integration polls the session’s messages (or subscribes to sessionEvents from a browser, see Events and Subscriptions).

Every operation is a POST of { "query", "variables" } to https://api.wetel.dev/graphql (sandbox) with the x-huat-platform: customer header and the credential shown. New here? Start with the Quickstart and the Authentication cheat sheet. Errors are listed in Errors; Postman and OpenAPI files are on Downloads.

OperationKindCredentialWhat it does
sdkStartmutationAPI keyStart a session against an agent and receive its session token.
sdkStartVoiceCallmutationAPI keyStart a live voice call with an agent and receive the LiveKit URL and join token for the caller.
sdkSendMessagemutationSession token (avatarToken)Send one user message into the session. Returns true at once; the reply follows asynchronously.
sessionsByExternalCustomerIdqueryAPI keyList the sessions started with one clientId, with their messages. The API-key way to poll for a reply.
sessionqueryDashboard JWTRead one session and its full message history by id (dashboard credential).
sdkEndSessionmutationSession token (avatarToken)End the session. The session token stops working for new messages.
evaluationqueryDashboard JWTRead the scored evaluation of an ended session (score, recommendation, summary, strengths, weaknesses).
refreshAvatarTokenmutationNo credential headerExchange a session token that is about to expire (or expired in the last 10 minutes) for a fresh one.

Start a session against an agent and receive its session token.

  • Kind: mutation
  • Credential: API key: X-Api-Key: sk_..., kept on your server only
  • Available in: sandbox and production
  • Call it from your server only. The API key must never reach a browser or a mobile app.
  • clientId is your own identifier for the end user or conversation. Give each conversation its own value (for example the ticket number) if you plan to poll with sessionsByExternalCustomerId.
  • context is an optional flat JSON object (at most 32 keys, scalar values, 8 KB) that every workflow turn can read as session.context. Never put secrets in it.
  • Starting a session can send the agent’s opening greeting, which is a metered model call.
  • Limited to 30 calls per minute per client IP.

Request

mutation StartSession($input: SdkStartInput!) {
sdkStart(input: $input) {
sessionId
avatarToken
agentId
graphqlEndpoint
}
}
variables
{
"input": {
"agentId": 7,
"clientId": "ticket-1001",
"context": {
"ticketId": "T-1001",
"product": "laptop"
}
}
}

Input fields

SdkStartInput

FieldTypeRequiredDescription
agentIdIntnoId of the agent to talk to (an integer from the agents query); NotFound when it is not in the API key’s workspace. Omit to use the workspace’s oldest ACTIVE agent; when the workspace has no active agent at all, a basic agent is created from agentName, position, personaPrompt, voiceTier and useCase.
agentNameStringnoName for the agent created when the workspace has no active agent (at most 100 characters, default “AI Assistant”). Ignored whenever an existing agent is used.
clientIdStringnoYour own identifier for the end user or conversation (at most 255 characters), stored on the session as clientExternalId so you can find it again with sessionsByExternalCustomerId. When omitted the server generates “sdk-” followed by a random UUID.
contextJSONnoOptional per-session data as a flat JSON object (pass it through variables, not an inline literal), for example {accountId, contactId, channelName, company} (channel itself is a reserved name and is refused). Same rules as turnContext: at most 32 keys matching [A-Za-z][A-Za-z0-9_]*, scalar values (strings up to 4096 characters), 8 KB in total, reserved runner names refused, never secrets. Stored on the session (readable through the session metadata the dashboard and API-key session queries return) and exposed to every workflow turn as ctx.session.context (for example {{session.context.company}}).
personaPromptStringnoSystem prompt for the agent created when the workspace has no active agent (at most 2000 characters; a generic friendly-assistant prompt when omitted). Ignored whenever an existing agent is used: it never overrides an agent’s own persona.
positionStringnoRole title for the agent created when the workspace has no active agent (at most 100 characters, default “Software Engineer”). Ignored whenever an existing agent is used.
useCaseAgentUseCasenoUse case for the agent created when the workspace has no active agent (default INTERVIEW). Ignored whenever an existing agent is used.
voiceTierVoiceTiernoVoice tier for the agent created when the workspace has no active agent (default STANDARD). Ignored whenever an existing agent is used.

Allowed values

  • AgentUseCase: CUSTOM, EDUCATION, INTERVIEW, ONBOARDING, SALES, SUPPORT
  • VoiceTier: CHIRP3_HD, NEURAL2, STANDARD, STUDIO, WAVENET

Code samples

sdkStart.sh
#!/usr/bin/env bash
# sdkStart: Start a session against an agent and receive its session token.
# Credential: API key (WETEL_API_KEY).
API_URL="${WETEL_API_URL:-https://api.wetel.dev/graphql}"
curl -sS "$API_URL" \
-H "Content-Type: application/json" \
-H "x-huat-platform: customer" \
-H "X-Api-Key: $WETEL_API_KEY" \
--data-binary @- <<'JSON'
{
"query": "mutation StartSession($input:SdkStartInput!){sdkStart(input:$input){sessionId avatarToken agentId graphqlEndpoint}}",
"variables": {
"input": {
"agentId": 7,
"clientId": "ticket-1001",
"context": {
"ticketId": "T-1001",
"product": "laptop"
}
}
}
}
JSON

Responses

Success (HTTP 200)

{
"data": {
"sdkStart": {
"sessionId": 4211,
"avatarToken": "example-avatar-token-not-real",
"agentId": 7,
"graphqlEndpoint": "https://api.wetel.dev/graphql"
}
}
}
UNAUTHENTICATED: Missing API key (HTTP 200)
{
"errors": [
{
"message": "Missing X-Api-Key header",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"sdkStart"
],
"extensions": {
"code": "UNAUTHENTICATED",
"originalError": {
"message": "Missing X-Api-Key header",
"error": "Unauthorized",
"statusCode": 401
}
}
}
],
"data": null
}
UNAUTHENTICATED: Unknown or revoked API key (HTTP 200)
{
"errors": [
{
"message": "Invalid API key",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"sdkStart"
],
"extensions": {
"code": "UNAUTHENTICATED",
"originalError": {
"message": "Invalid API key",
"error": "Unauthorized",
"statusCode": 401
}
}
}
],
"data": null
}
NOT_FOUND: Agent not found in your workspace (HTTP 200)
{
"errors": [
{
"message": "Agent #7 not found",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"sdkStart"
],
"extensions": {
"code": "NOT_FOUND",
"status": 404,
"originalError": {
"message": "Agent #7 not found",
"error": "Not Found",
"statusCode": 404
}
}
}
],
"data": null
}
FORBIDDEN: Platform header missing (HTTP 200)
{
"errors": [
{
"message": "Platform is not specified!",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"sdkStart"
],
"extensions": {
"code": "FORBIDDEN",
"originalError": {
"message": "Platform is not specified!",
"error": "Forbidden",
"statusCode": 403
}
}
}
],
"data": null
}
BAD_USER_INPUT: Unknown field in the input (HTTP 400)

Sent with variables:

{
"input": {
"agentId": 7,
"customerId": "ticket-1001"
}
}
{
"errors": [
{
"message": "Variable \"$input\" got invalid value { agentId: 7, customerId: \"ticket-1001\" }; Field \"customerId\" is not defined by type \"SdkStartInput\".",
"locations": [
{
"line": 1,
"column": 23
}
],
"extensions": {
"code": "BAD_USER_INPUT"
}
}
]
}

Start a live voice call with an agent and receive the LiveKit URL and join token for the caller.

  • Kind: mutation
  • Credential: API key: X-Api-Key: sk_..., kept on your server only
  • Available in: sandbox and production
  • Staging only, on request. Answers FORBIDDEN where voice calls are not enabled; not enabled in production.
  • Call it from your server only. The API key must never reach a browser or a mobile app.
  • The join token is valid to join for about two minutes and carries a session credential that the caller’s browser can read. Treat it as belonging to one caller; never log or share it.
  • context is an optional flat JSON object exposed to the workflow as ctx.session.context, with the same rules as sdkStart.
  • Limited to 60 calls per minute per client address and 30 call starts per minute per workspace. The over-limit error may carry no specific code.
  • Capped at 1,000 call requests per API key per UTC day by default. Past it the call is refused with extensions.status 429 and the message Daily voice call limit reached for this API key; try again tomorrow (UTC). A call nobody joins is closed as EXPIRED within about 15 minutes.

Request

mutation StartVoiceCall($input: SdkStartVoiceCallInput!) {
sdkStartVoiceCall(input: $input) {
sessionId
agentId
livekitUrl
livekitToken
roomName
expiresAt
}
}
variables
{
"input": {
"agentId": 7,
"clientId": "call-0042"
}
}

Input fields

SdkStartVoiceCallInput

FieldTypeRequiredDescription
agentIdInt!yesId of the agent that answers the call (an integer from the agents query); NotFound when it is not in the API key’s workspace. The agent’s published workflow answers every turn, so it should be a voice-safe workflow: short spoken sentences, no lists or markdown.
clientIdStringnoYour own identifier for the caller or the call, shown on the session (at most 255 characters). A fresh one is generated when omitted.
contextJSONnoOptional per-call data as a flat JSON object (pass it through variables, not an inline literal), exposed to the workflow as ctx.session.context. Same rules as the context of sdkStart.
engineVoiceEnginenoWho answers the call (default WORKFLOW: the agent’s published workflow). LIVE uses a speech-to-speech model from the agent’s instructions only (no workflow, no tools, transcript recorded after the call) and is refused unless the deployment has it enabled.

Allowed values

  • VoiceEngine: LIVE, WORKFLOW

Code samples

sdkStartVoiceCall.sh
#!/usr/bin/env bash
# sdkStartVoiceCall: Start a live voice call with an agent and receive the LiveKit URL and join token for the caller.
# Credential: API key (WETEL_API_KEY).
API_URL="${WETEL_API_URL:-https://api.wetel.dev/graphql}"
curl -sS "$API_URL" \
-H "Content-Type: application/json" \
-H "x-huat-platform: customer" \
-H "X-Api-Key: $WETEL_API_KEY" \
--data-binary @- <<'JSON'
{
"query": "mutation StartVoiceCall($input:SdkStartVoiceCallInput!){sdkStartVoiceCall(input:$input){sessionId agentId livekitUrl livekitToken roomName expiresAt}}",
"variables": {
"input": {
"agentId": 7,
"clientId": "call-0042"
}
}
}
JSON

Responses

Success (HTTP 200)

{
"data": {
"sdkStartVoiceCall": {
"sessionId": 4212,
"agentId": 7,
"livekitUrl": "wss://voice.example.com",
"livekitToken": "example-join-token-not-real",
"roomName": "call-4212",
"expiresAt": "2026-10-19T09:02:00.000Z"
}
}
}
UNAUTHENTICATED: Missing API key (HTTP 200)
{
"errors": [
{
"message": "Missing X-Api-Key header",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"sdkStartVoiceCall"
],
"extensions": {
"code": "UNAUTHENTICATED",
"originalError": {
"message": "Missing X-Api-Key header",
"error": "Unauthorized",
"statusCode": 401
}
}
}
],
"data": null
}
UNAUTHENTICATED: Unknown or revoked API key (HTTP 200)
{
"errors": [
{
"message": "Invalid API key",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"sdkStartVoiceCall"
],
"extensions": {
"code": "UNAUTHENTICATED",
"originalError": {
"message": "Invalid API key",
"error": "Unauthorized",
"statusCode": 401
}
}
}
],
"data": null
}
FORBIDDEN: Voice calls not enabled on this environment (HTTP 200)
{
"errors": [
{
"message": "Voice calls are not enabled on this environment",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"sdkStartVoiceCall"
],
"extensions": {
"code": "FORBIDDEN",
"originalError": {
"message": "Voice calls are not enabled on this environment",
"error": "Forbidden",
"statusCode": 403
}
}
}
],
"data": null
}
NOT_FOUND: Agent not found in your workspace (HTTP 200)
{
"errors": [
{
"message": "Agent #7 not found",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"sdkStartVoiceCall"
],
"extensions": {
"code": "NOT_FOUND",
"status": 404,
"originalError": {
"message": "Agent #7 not found",
"error": "Not Found",
"statusCode": 404
}
}
}
],
"data": null
}

Send one user message into the session. Returns true at once; the reply follows asynchronously.

  • Kind: mutation
  • Credential: Session token (avatarToken): Authorization: Bearer <avatarToken>, returned by sdkStart for one session
  • Available in: sandbox and production
  • true is an acknowledgement, not the reply. Read the reply by polling the session’s messages or from the sessionEvents subscription.
  • text holds up to 100,000 characters. It may be empty only when attachments has at least one entry.
  • turnContext is an optional flat JSON object for this turn only (workflows read it as turn.context).
  • The token must belong to the same session as input.sessionId.

Request

mutation SendMessage($input: SdkSendMessageInput!) {
sdkSendMessage(input: $input)
}
variables
{
"input": {
"sessionId": 4211,
"text": "My laptop cannot connect to the office VPN since this morning."
}
}

Input fields

SdkSendMessageInput

FieldTypeRequiredDescription
attachments[MessageAttachmentInput!]noOptional files sent with this message (at most 10). Validated here (absolute http(s) URL, no embedded credentials, sanitised filename, MIME and kind inferred when omitted) but NOT fetched: a workflow node downloads them when it needs them. In workflow context they appear as ctx.attachments (list of {url, mime, filename, kind}), ctx.attachmentImageUrls (the IMAGE urls, usable as imageUrlsVar), and the first entry fills ctx.attachment, ctx.attachmentUrl, ctx.attachmentMime and ctx.attachmentFilename. When present they take precedence over any attachment marker written inside the text. text may be empty when at least one attachment is sent.
clientTurnIdStringnoOptional opaque correlation token of your own (at most 255 characters). Not stored or interpreted; echoed back verbatim on every AiResponseEvent of this turn’s reply so you can match a reply to the call that caused it.
sessionIdInt!yesId of the session (sdkStart.sessionId). Must be the session the avatarToken was issued for, otherwise the call is refused.
textString!yesThe user message, up to 100,000 characters. May be an empty string only when attachments has at least one entry.
turnContextJSONnoOptional per-turn data as a flat JSON object (pass it through variables, not an inline literal). At most 32 keys matching [A-Za-z][A-Za-z0-9_]*, values string (up to 4096 characters), number, boolean or null, 8 KB in total; runner-reserved names such as session, channel or userMessage are refused. Never put secrets here. Appears in workflow context as ctx.turn.context (for example {{turn.context.orderId}}); a string trigger key also sets ctx.channel.trigger. Applies to this turn only.

MessageAttachmentInput

FieldTypeRequiredDescription
filenameStringnoOriginal filename, at most 255 characters. Quotes, square brackets and control characters are removed.
kindAttachmentKindnoKind of file. When omitted it is inferred from the MIME type (image/*, audio/*, video/*, PDF and Office or text types are DOCUMENT, anything else OTHER).
mimeStringnoMIME type, at most 100 characters (for example image/png). When omitted it is inferred from the filename, then from the URL path extension.
urlString!yesAbsolute http(s) URL of the file, at most 2048 characters, without user:password credentials. Treat a signed URL as a credential: it is redacted wherever messages are read back.

Allowed values

  • AttachmentKind: AUDIO, DOCUMENT, IMAGE, OTHER, VIDEO

Code samples

sdkSendMessage.sh
#!/usr/bin/env bash
# sdkSendMessage: Send one user message into the session. Returns true at once; the reply follows asynchronously.
# Credential: Session token (avatarToken) (WETEL_AVATAR_TOKEN).
API_URL="${WETEL_API_URL:-https://api.wetel.dev/graphql}"
curl -sS "$API_URL" \
-H "Content-Type: application/json" \
-H "x-huat-platform: customer" \
-H "Authorization: Bearer $WETEL_AVATAR_TOKEN" \
--data-binary @- <<'JSON'
{
"query": "mutation SendMessage($input:SdkSendMessageInput!){sdkSendMessage(input:$input)}",
"variables": {
"input": {
"sessionId": 4211,
"text": "My laptop cannot connect to the office VPN since this morning."
}
}
}
JSON

Responses

Success (acknowledgement only) (HTTP 200)

{
"data": {
"sdkSendMessage": true
}
}
FORBIDDEN: Token for a different session (HTTP 200)
{
"errors": [
{
"message": "This avatar token is not valid for the requested session",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"sdkSendMessage"
],
"extensions": {
"code": "FORBIDDEN",
"originalError": {
"message": "This avatar token is not valid for the requested session",
"error": "Forbidden",
"statusCode": 403
}
}
}
],
"data": null
}
UNAUTHENTICATED: Expired or invalid session token (HTTP 200)
{
"errors": [
{
"message": "Invalid or expired avatar token",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"sdkSendMessage"
],
"extensions": {
"code": "UNAUTHENTICATED",
"originalError": {
"message": "Invalid or expired avatar token",
"error": "Unauthorized",
"statusCode": 401
}
}
}
],
"data": null
}
BAD_REQUEST: Session already ended (HTTP 200)
{
"errors": [
{
"message": "Session 4211 is not active (status: ended)",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"sdkSendMessage"
],
"extensions": {
"code": "BAD_REQUEST",
"originalError": {
"message": "Session 4211 is not active (status: ended)",
"error": "Bad Request",
"statusCode": 400
}
}
}
],
"data": null
}
UNAUTHENTICATED: No Authorization header (HTTP 200)
{
"errors": [
{
"message": "Missing or malformed Authorization header",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"sdkSendMessage"
],
"extensions": {
"code": "UNAUTHENTICATED",
"originalError": {
"message": "Missing or malformed Authorization header",
"error": "Unauthorized",
"statusCode": 401
}
}
}
],
"data": null
}

List the sessions started with one clientId, with their messages. The API-key way to poll for a reply.

  • Kind: query
  • Credential: API key: X-Api-Key: sk_..., kept on your server only
  • Available in: sandbox and production
  • externalCustomerId is the clientId you passed to sdkStart. Sessions come oldest first; first defaults to 20.
  • Messages come oldest first. A reply is a MODEL message that follows your USER message. Poll every one to two seconds with a timeout; a reply usually takes a few seconds and can take longer when the workflow calls tools.
  • Do not select every field of every message: the API refuses a query whose complexity is above 50 fields.

Request

query PollSession($externalCustomerId: String!, $first: Int) {
sessionsByExternalCustomerId(externalCustomerId: $externalCustomerId, first: $first) {
totalCount
edges {
node {
id
status
messages {
id
role
text
turnIndex
createdAt
}
}
}
}
}
variables
{
"externalCustomerId": "ticket-1001",
"first": 1
}

Allowed values

  • SessionStatus: ACTIVE, ENDED, EXPIRED, PENDING
  • MessageRole: MODEL, USER

Code samples

sessionsByExternalCustomerId.sh
#!/usr/bin/env bash
# sessionsByExternalCustomerId: List the sessions started with one clientId, with their messages. The API-key way to poll for a reply.
# Credential: API key (WETEL_API_KEY).
API_URL="${WETEL_API_URL:-https://api.wetel.dev/graphql}"
curl -sS "$API_URL" \
-H "Content-Type: application/json" \
-H "x-huat-platform: customer" \
-H "X-Api-Key: $WETEL_API_KEY" \
--data-binary @- <<'JSON'
{
"query": "query PollSession($externalCustomerId:String!$first:Int){sessionsByExternalCustomerId(externalCustomerId:$externalCustomerId first:$first){totalCount edges{node{id status messages{id role text turnIndex createdAt}}}}}",
"variables": {
"externalCustomerId": "ticket-1001",
"first": 1
}
}
JSON

Responses

Reply has arrived (HTTP 200)

{
"data": {
"sessionsByExternalCustomerId": {
"totalCount": 1,
"edges": [
{
"node": {
"id": 4211,
"status": "ACTIVE",
"messages": [
{
"id": 90311,
"role": "USER",
"text": "My laptop cannot connect to the office VPN since this morning.",
"turnIndex": 0,
"createdAt": "2026-10-12T03:15:02.114Z"
},
{
"id": 90312,
"role": "MODEL",
"text": "Sorry to hear that. Which operating system is the laptop running, and do you see an error message?",
"turnIndex": 1,
"createdAt": "2026-10-12T03:15:05.871Z"
}
]
}
}
]
}
}
}
UNAUTHENTICATED: API key missing (HTTP 200)
{
"errors": [
{
"message": "Missing X-Api-Key header",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"sessionsByExternalCustomerId"
],
"extensions": {
"code": "UNAUTHENTICATED",
"originalError": {
"message": "Missing X-Api-Key header",
"error": "Unauthorized",
"statusCode": 401
}
}
}
],
"data": null
}

Read one session and its full message history by id (dashboard credential).

  • Kind: query
  • Credential: Dashboard JWT: Authorization: Bearer <jwt>, from login (a person or a one-off script)
  • Available in: sandbox and production
  • Takes a dashboard JWT. An API key or a session token is refused with Unauthorized; with an API key, poll with sessionsByExternalCustomerId instead.
  • Returns null (not an error) for an id that does not exist in your workspace.

Request

query ReadSession($id: ID!) {
session(id: $id) {
id
status
startedAt
endedAt
messages {
id
role
text
turnIndex
}
}
}
variables
{
"id": "4211"
}

Allowed values

  • SessionStatus: ACTIVE, ENDED, EXPIRED, PENDING
  • MessageRole: MODEL, USER

Code samples

session.sh
#!/usr/bin/env bash
# session: Read one session and its full message history by id (dashboard credential).
# Credential: Dashboard JWT (WETEL_JWT).
API_URL="${WETEL_API_URL:-https://api.wetel.dev/graphql}"
curl -sS "$API_URL" \
-H "Content-Type: application/json" \
-H "x-huat-platform: customer" \
-H "Authorization: Bearer $WETEL_JWT" \
--data-binary @- <<'JSON'
{
"query": "query ReadSession($id:ID!){session(id:$id){id status startedAt endedAt messages{id role text turnIndex}}}",
"variables": {
"id": "4211"
}
}
JSON

Responses

Success (HTTP 200)

{
"data": {
"session": {
"id": 4211,
"status": "ENDED",
"startedAt": "2026-10-12T03:15:01.502Z",
"endedAt": "2026-10-12T03:18:40.020Z",
"messages": [
{
"id": 90311,
"role": "USER",
"text": "My laptop cannot connect to the office VPN since this morning.",
"turnIndex": 0
},
{
"id": 90312,
"role": "MODEL",
"text": "Sorry to hear that. Which operating system is the laptop running, and do you see an error message?",
"turnIndex": 1
}
]
}
}
}

Not in your workspace (HTTP 200)

{
"data": {
"session": null
}
}
UNAUTHENTICATED: Called with an API key instead of a JWT (HTTP 200)
{
"errors": [
{
"message": "Unauthorized",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"session"
],
"extensions": {
"code": "UNAUTHENTICATED",
"originalError": {
"message": "Unauthorized",
"statusCode": 401
}
}
}
],
"data": {
"session": null
}
}

End the session. The session token stops working for new messages.

  • Kind: mutation
  • Credential: Session token (avatarToken): Authorization: Bearer <avatarToken>, returned by sdkStart for one session
  • Available in: sandbox and production
  • evaluation is generated asynchronously and only for conversations with at least two turns, so it is often null in this response.

Request

mutation EndSession($input: SdkEndSessionInput!) {
sdkEndSession(input: $input) {
sessionId
durationSeconds
evaluation {
score
summary
}
}
}
variables
{
"input": {
"sessionId": 4211
}
}

Input fields

SdkEndSessionInput

FieldTypeRequiredDescription
sessionIdInt!yesId of the session to end (sdkStart.sessionId). Must be the session the avatarToken was issued for.

Code samples

sdkEndSession.sh
#!/usr/bin/env bash
# sdkEndSession: End the session. The session token stops working for new messages.
# Credential: Session token (avatarToken) (WETEL_AVATAR_TOKEN).
API_URL="${WETEL_API_URL:-https://api.wetel.dev/graphql}"
curl -sS "$API_URL" \
-H "Content-Type: application/json" \
-H "x-huat-platform: customer" \
-H "Authorization: Bearer $WETEL_AVATAR_TOKEN" \
--data-binary @- <<'JSON'
{
"query": "mutation EndSession($input:SdkEndSessionInput!){sdkEndSession(input:$input){sessionId durationSeconds evaluation{score summary}}}",
"variables": {
"input": {
"sessionId": 4211
}
}
}
JSON

Responses

Success (HTTP 200)

{
"data": {
"sdkEndSession": {
"sessionId": 4211,
"durationSeconds": 219,
"evaluation": null
}
}
}
FORBIDDEN: Token for a different session (HTTP 200)
{
"errors": [
{
"message": "This avatar token is not valid for the requested session",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"sdkEndSession"
],
"extensions": {
"code": "FORBIDDEN",
"originalError": {
"message": "This avatar token is not valid for the requested session",
"error": "Forbidden",
"statusCode": 403
}
}
}
],
"data": null
}

Read the scored evaluation of an ended session (score, recommendation, summary, strengths, weaknesses).

  • Kind: query
  • Credential: Dashboard JWT: Authorization: Bearer <jwt>, from login (a person or a one-off script)
  • Available in: sandbox and production
  • Returns null until the session has ended and the background evaluation has finished, and stays null for a session that was not evaluated (fewer than two turns, or evaluation not enabled for it). Poll with a back-off after sdkEndSession.
  • score is 0 to 100. For a support agent recommendation is one of resolved, escalate or follow-up (n/a when none fits); an agent with its own evaluation labels returns those.
  • transcript is also available on this type; it holds the whole conversation, so select it only if you need it.

Request

query SessionEvaluation($sessionId: Int!) {
evaluation(sessionId: $sessionId) {
id
sessionId
score
recommendation
summary
strengths
weaknesses
generatedAt
}
}
variables
{
"sessionId": 4211
}

Code samples

evaluation.sh
#!/usr/bin/env bash
# evaluation: Read the scored evaluation of an ended session (score, recommendation, summary, strengths, weaknesses).
# Credential: Dashboard JWT (WETEL_JWT).
API_URL="${WETEL_API_URL:-https://api.wetel.dev/graphql}"
curl -sS "$API_URL" \
-H "Content-Type: application/json" \
-H "x-huat-platform: customer" \
-H "Authorization: Bearer $WETEL_JWT" \
--data-binary @- <<'JSON'
{
"query": "query SessionEvaluation($sessionId:Int!){evaluation(sessionId:$sessionId){id sessionId score recommendation summary strengths weaknesses generatedAt}}",
"variables": {
"sessionId": 4211
}
}
JSON

Responses

Success (HTTP 200)

{
"data": {
"evaluation": {
"id": 1532,
"sessionId": 4211,
"score": 82,
"recommendation": "resolved",
"summary": "The user could not connect to the VPN. The agent identified an expired client certificate and walked the user through renewing it; the user confirmed it works.",
"strengths": [
"Asked for the operating system and error before suggesting a fix"
],
"weaknesses": [
"Did not offer to open a ticket in case the fix failed"
],
"generatedAt": "2026-10-12T03:18:52.114Z"
}
}
}

Not evaluated yet (or never) (HTTP 200)

{
"data": {
"evaluation": null
}
}
UNAUTHENTICATED: Called with an API key instead of a JWT (HTTP 200)
{
"errors": [
{
"message": "Unauthorized",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"evaluation"
],
"extensions": {
"code": "UNAUTHENTICATED",
"originalError": {
"message": "Unauthorized",
"statusCode": 401
}
}
}
],
"data": {
"evaluation": null
}
}

Exchange a session token that is about to expire (or expired in the last 10 minutes) for a fresh one.

  • Kind: mutation
  • Credential: No credential header: The credential travels inside the input (see the operation)
  • Available in: sandbox and production
  • A session token lives about one hour. The token goes in input.avatarToken, not in an Authorization header, because an expired token must be accepted here.
  • The session must still be active. Limited to 10 calls per minute per client IP.
  • An open sessionEvents WebSocket keeps the old token; close it and reopen it with the new one.

Request

mutation RefreshToken($input: RefreshAvatarTokenInput!) {
refreshAvatarToken(input: $input) {
sessionId
avatarToken
expiresInMs
}
}
variables
{
"input": {
"avatarToken": "example-avatar-token-not-real"
}
}

Input fields

RefreshAvatarTokenInput

FieldTypeRequiredDescription
avatarTokenString!yesThe current avatarToken, still valid or expired for at most the grace window (10 minutes by default). At most 4096 characters.

Code samples

refreshAvatarToken.sh
#!/usr/bin/env bash
# refreshAvatarToken: Exchange a session token that is about to expire (or expired in the last 10 minutes) for a fresh one.
# Credential: No credential header.
API_URL="${WETEL_API_URL:-https://api.wetel.dev/graphql}"
curl -sS "$API_URL" \
-H "Content-Type: application/json" \
-H "x-huat-platform: customer" \
--data-binary @- <<'JSON'
{
"query": "mutation RefreshToken($input:RefreshAvatarTokenInput!){refreshAvatarToken(input:$input){sessionId avatarToken expiresInMs}}",
"variables": {
"input": {
"avatarToken": "example-avatar-token-not-real"
}
}
}
JSON

Responses

Success (HTTP 200)

{
"data": {
"refreshAvatarToken": {
"sessionId": 4211,
"avatarToken": "example-refreshed-token-not-real",
"expiresInMs": 3600000
}
}
}
UNAUTHENTICATED: Expired beyond the grace window, or invalid (HTTP 200)
{
"errors": [
{
"message": "Invalid or expired avatar token",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"refreshAvatarToken"
],
"extensions": {
"code": "UNAUTHENTICATED",
"originalError": {
"message": "Invalid or expired avatar token",
"error": "Unauthorized",
"statusCode": 401
}
}
}
],
"data": null
}
BAD_REQUEST: Session already ended (HTTP 200)
{
"errors": [
{
"message": "Session 4211 is not active (status: ended)",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"refreshAvatarToken"
],
"extensions": {
"code": "BAD_REQUEST",
"originalError": {
"message": "Session 4211 is not active (status: ended)",
"error": "Bad Request",
"statusCode": 400
}
}
}
],
"data": null
}