Quickstart: Hello ticket
This page gets you from an API key to an agent’s reply in four HTTP calls, written for developers who usually work with REST. The example domain is a support ticket, but nothing here is ticket-specific: the same four calls run any agent.
GraphQL in four sentences
Section titled “GraphQL in four sentences”There is one URL, and every call is an HTTP POST to it with a JSON body { "query": "...", "variables": { ... } }. Each operation (for example sdkStart) plays the role of an endpoint, and the query lists the fields you want back, so the response holds exactly those. Errors come back in a JSON errors array, each with a machine-readable extensions.code (see Errors); most of them arrive with HTTP status 200, so always check errors before reading data. Every request also carries the header x-huat-platform: customer.
What you need
Section titled “What you need”- An API key (
sk_...) from the sandbox. A workspace member creates one withcreateNamedApiKey; keep it on your server only. - The id of an agent with a published workflow in that workspace (
WETEL_AGENT_ID). Building agents and workflows is covered in Agents and the workflow overview.
The four calls
Section titled “The four calls”| Step | Operation | Credential | What comes back |
|---|---|---|---|
| 1 | sdkStart | API key (X-Api-Key) | sessionId and a session token (avatarToken) |
| 2 | sdkSendMessage | session token (Authorization: Bearer <avatarToken>) | true: an acknowledgement, not the reply |
| 3 | sessionsByExternalCustomerId | API key | the session’s messages; repeat until a reply shows |
| 4 | sdkEndSession | session token | the session’s duration |
The reply is generated asynchronously (the agent may call tools first), so step 3 polls: every two seconds, for at most sixty, it reads the session’s messages and stops when a MODEL message follows your USER message.
The program
Section titled “The program”Each tab is a complete program. Set WETEL_API_KEY and WETEL_AGENT_ID and run it; the comment at the top of each file has the exact command. The same files are on Downloads.
#!/usr/bin/env bash# Hello ticket: start a session, send one message, poll for the reply, end the session.# Needs curl and jq. Run: WETEL_API_KEY=sk_... WETEL_AGENT_ID=7 bash hello-ticket.shset -euo pipefailAPI_URL="${WETEL_API_URL:-https://api.wetel.dev/graphql}"AGENT_ID="${WETEL_AGENT_ID:-1}"CLIENT_ID="ticket-$(date +%s)" # one clientId per conversationTEXT="My laptop cannot connect to the office VPN since this morning."
read -r -d '' START <<'GRAPHQL' || truemutation StartSession($input: SdkStartInput!) { sdkStart(input: $input) { sessionId avatarToken agentId graphqlEndpoint }}GRAPHQLread -r -d '' SEND <<'GRAPHQL' || truemutation SendMessage($input: SdkSendMessageInput!) { sdkSendMessage(input: $input)}GRAPHQLread -r -d '' POLL <<'GRAPHQL' || truequery PollSession($externalCustomerId: String!, $first: Int) { sessionsByExternalCustomerId(externalCustomerId: $externalCustomerId, first: $first) { totalCount edges { node { id status messages { id role text turnIndex createdAt } } } }}GRAPHQLread -r -d '' END <<'GRAPHQL' || truemutation EndSession($input: SdkEndSessionInput!) { sdkEndSession(input: $input) { sessionId durationSeconds evaluation { score summary } }}GRAPHQL
# gql QUERY VARIABLES_JSON AUTH_HEADER: prints .data, or the first error and exits.gql() { local response response=$(jq -n --arg query "$1" --argjson variables "$2" '{query: $query, variables: $variables}' | curl -sS "$API_URL" -H "Content-Type: application/json" -H "x-huat-platform: customer" -H "$3" --data-binary @-) if [ "$(jq '.errors | length' <<<"$response")" != "0" ]; then jq -r '.errors[0] | "\(.extensions.code): \(.message)"' <<<"$response" >&2 exit 1 fi jq '.data' <<<"$response"}
# 1. Start a session with the API key.STARTED=$(gql "$START" "$(jq -n --argjson agentId "$AGENT_ID" --arg clientId "$CLIENT_ID" '{input: {agentId: $agentId, clientId: $clientId}}')" "X-Api-Key: $WETEL_API_KEY")SESSION_ID=$(jq '.sdkStart.sessionId' <<<"$STARTED")AVATAR_TOKEN=$(jq -r '.sdkStart.avatarToken' <<<"$STARTED")
# 2. Send the message with the session token. It returns true at once.gql "$SEND" "$(jq -n --argjson sessionId "$SESSION_ID" --arg text "$TEXT" '{input: {sessionId: $sessionId, text: $text}}')" "Authorization: Bearer $AVATAR_TOKEN" >/dev/null
# 3. Poll the session's messages until the reply arrives# (the reply is every MODEL message after the last USER message).REPLY=""DEADLINE=$(($(date +%s) + 60))while [ -z "$REPLY" ] && [ "$(date +%s)" -lt "$DEADLINE" ]; do sleep 2 POLLED=$(gql "$POLL" "$(jq -n --arg clientId "$CLIENT_ID" '{externalCustomerId: $clientId, first: 1}')" "X-Api-Key: $WETEL_API_KEY") REPLY=$(jq -r --argjson id "$SESSION_ID" ' .sessionsByExternalCustomerId.edges[].node | select(.id == $id) | .messages | ((map(.role) | rindex("USER")) // -1) as $u | .[($u + 1):] | map(select(.role == "MODEL") | .text) | join("\n")' <<<"$POLLED")doneecho "${REPLY:-No reply within the timeout.}"
# 4. End the session with the session token.gql "$END" "$(jq -n --argjson sessionId "$SESSION_ID" '{input: {sessionId: $sessionId}}')" "Authorization: Bearer $AVATAR_TOKEN" >/dev/null// Hello ticket: start a session, send one message, poll for the reply, end the session.// Node 18+. Run: WETEL_API_KEY=sk_... WETEL_AGENT_ID=7 npx tsx hello-ticket.tsconst API_URL = process.env.WETEL_API_URL ?? "https://api.wetel.dev/graphql";const API_KEY = process.env.WETEL_API_KEY ?? "";const AGENT_ID = Number(process.env.WETEL_AGENT_ID ?? "1");const POLL_INTERVAL_MS = 2000;const TIMEOUT_MS = 60000;
const START = `mutation StartSession($input: SdkStartInput!) { sdkStart(input: $input) { sessionId avatarToken agentId graphqlEndpoint }}`;const SEND = `mutation SendMessage($input: SdkSendMessageInput!) { sdkSendMessage(input: $input)}`;const POLL = `query PollSession($externalCustomerId: String!, $first: Int) { sessionsByExternalCustomerId(externalCustomerId: $externalCustomerId, first: $first) { totalCount edges { node { id status messages { id role text turnIndex createdAt } } } }}`;const END = `mutation EndSession($input: SdkEndSessionInput!) { sdkEndSession(input: $input) { sessionId durationSeconds evaluation { score summary } }}`;
type Message = { role: string; text: string };type GraphQLResponse = { data?: any; errors?: { message: string; extensions?: { code?: string } }[];};type Session = { id: number; messages: Message[] };
async function gql( query: string, variables: Record<string, unknown>, auth: Record<string, string>): Promise<any> { const res = await fetch(API_URL, { method: "POST", headers: { "Content-Type": "application/json", "x-huat-platform": "customer", ...auth, }, body: JSON.stringify({ query, variables }), }); const body = (await res.json()) as GraphQLResponse; if (body.errors?.length) { const error = body.errors[0]; throw new Error(`${error.extensions?.code}: ${error.message}`); } return body.data;}
// The reply is every MODEL message after the last USER message.function replyAfterLastUserMessage(messages: Message[]): string | undefined { const lastUser = messages.map(m => m.role).lastIndexOf("USER"); const replies = messages.slice(lastUser + 1).filter(m => m.role === "MODEL"); return replies.length ? replies.map(m => m.text).join("\n") : undefined;}
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
async function main(): Promise<void> { const agentId = AGENT_ID; const clientId = `ticket-${Date.now()}`; // one clientId per conversation const text = "My laptop cannot connect to the office VPN since this morning."; const withKey = { "X-Api-Key": API_KEY };
// 1. Start a session with the API key. const started = await gql( START, { input: { agentId: agentId, clientId: clientId, }, }, withKey ); const { sessionId, avatarToken } = started.sdkStart; const withToken = { Authorization: `Bearer ${avatarToken}` };
// 2. Send the message with the session token. It returns true at once. await gql( SEND, { input: { sessionId: sessionId, text: text, }, }, withToken );
// 3. Poll the session's messages until the reply arrives. let reply: string | undefined; const deadline = Date.now() + TIMEOUT_MS; while (!reply && Date.now() < deadline) { await sleep(POLL_INTERVAL_MS); const polled = await gql( POLL, { externalCustomerId: clientId, first: 1, }, withKey ); const session = polled.sessionsByExternalCustomerId.edges .map((edge: { node: Session }) => edge.node) .find((node: Session) => node.id === sessionId); reply = replyAfterLastUserMessage(session?.messages ?? []); } console.log(reply ?? "No reply within the timeout.");
// 4. End the session with the session token. await gql( END, { input: { sessionId: sessionId, }, }, withToken );}
main().catch(err => { console.error(err); process.exit(1);});
export {};<?php// Hello ticket: start a session, send one message, poll for the reply, end the session.// PHP 7.3+ with curl. Run: WETEL_API_KEY=sk_... WETEL_AGENT_ID=7 php hello-ticket.php$apiUrl = getenv('WETEL_API_URL') ?: 'https://api.wetel.dev/graphql';$apiKey = getenv('WETEL_API_KEY');$pollIntervalSeconds = 2;$timeoutSeconds = 60;
$START = <<<'GRAPHQL'mutation StartSession($input: SdkStartInput!) { sdkStart(input: $input) { sessionId avatarToken agentId graphqlEndpoint }}GRAPHQL;$SEND = <<<'GRAPHQL'mutation SendMessage($input: SdkSendMessageInput!) { sdkSendMessage(input: $input)}GRAPHQL;$POLL = <<<'GRAPHQL'query PollSession($externalCustomerId: String!, $first: Int) { sessionsByExternalCustomerId(externalCustomerId: $externalCustomerId, first: $first) { totalCount edges { node { id status messages { id role text turnIndex createdAt } } } }}GRAPHQL;$END = <<<'GRAPHQL'mutation EndSession($input: SdkEndSessionInput!) { sdkEndSession(input: $input) { sessionId durationSeconds evaluation { score summary } }}GRAPHQL;
function gql(string $apiUrl, string $query, array $variables, array $auth): array{ $ch = curl_init($apiUrl); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => array_merge(['Content-Type: application/json', 'x-huat-platform: customer'], $auth), CURLOPT_POSTFIELDS => json_encode(['query' => $query, 'variables' => $variables]), ]); $body = json_decode(curl_exec($ch), true); curl_close($ch); if (!empty($body['errors'])) { $error = $body['errors'][0]; throw new RuntimeException(($error['extensions']['code'] ?? 'ERROR') . ': ' . $error['message']); } return $body['data'];}
// The reply is every MODEL message after the last USER message.function replyAfterLastUserMessage(array $messages): ?string{ $lastUser = -1; foreach ($messages as $i => $message) { if ($message['role'] === 'USER') { $lastUser = $i; } } $replies = []; foreach (array_slice($messages, $lastUser + 1) as $message) { if ($message['role'] === 'MODEL') { $replies[] = $message['text']; } } return $replies ? implode("\n", $replies) : null;}
$agentId = (int) (getenv('WETEL_AGENT_ID') ?: 1);$clientId = 'ticket-' . round(microtime(true) * 1000); // one clientId per conversation$text = 'My laptop cannot connect to the office VPN since this morning.';$withKey = ['X-Api-Key: ' . $apiKey];
// 1. Start a session with the API key.$started = gql($apiUrl, $START, [ 'input' => [ 'agentId' => $agentId, 'clientId' => $clientId, ],], $withKey)['sdkStart'];$sessionId = $started['sessionId'];$withToken = ['Authorization: Bearer ' . $started['avatarToken']];
// 2. Send the message with the session token. It returns true at once.gql($apiUrl, $SEND, [ 'input' => [ 'sessionId' => $sessionId, 'text' => $text, ],], $withToken);
// 3. Poll the session's messages until the reply arrives.$reply = null;$deadline = time() + $timeoutSeconds;while ($reply === null && time() < $deadline) { sleep($pollIntervalSeconds); $polled = gql($apiUrl, $POLL, [ 'externalCustomerId' => $clientId, 'first' => 1,], $withKey); foreach ($polled['sessionsByExternalCustomerId']['edges'] as $edge) { if ($edge['node']['id'] === $sessionId) { $reply = replyAfterLastUserMessage($edge['node']['messages']); } }}echo ($reply ?? 'No reply within the timeout.'), PHP_EOL;
// 4. End the session with the session token.gql($apiUrl, $END, [ 'input' => [ 'sessionId' => $sessionId, ],], $withToken);// Hello ticket: start a session, send one message, poll for the reply, end the session.// .NET 8: put this in Program.cs of a console app. Run: WETEL_API_KEY=sk_... WETEL_AGENT_ID=7 dotnet runusing System.Net.Http.Json;using System.Text.Json.Nodes;
var apiUrl = Environment.GetEnvironmentVariable("WETEL_API_URL") ?? "https://api.wetel.dev/graphql";var apiKey = Environment.GetEnvironmentVariable("WETEL_API_KEY") ?? "";var agentId = int.Parse(Environment.GetEnvironmentVariable("WETEL_AGENT_ID") ?? "1");var pollInterval = TimeSpan.FromSeconds(2);var timeout = TimeSpan.FromSeconds(60);
const string Start = """ mutation StartSession($input: SdkStartInput!) { sdkStart(input: $input) { sessionId avatarToken agentId graphqlEndpoint } } """;const string Send = """ mutation SendMessage($input: SdkSendMessageInput!) { sdkSendMessage(input: $input) } """;const string Poll = """ query PollSession($externalCustomerId: String!, $first: Int) { sessionsByExternalCustomerId(externalCustomerId: $externalCustomerId, first: $first) { totalCount edges { node { id status messages { id role text turnIndex createdAt } } } } } """;const string End = """ mutation EndSession($input: SdkEndSessionInput!) { sdkEndSession(input: $input) { sessionId durationSeconds evaluation { score summary } } } """;
using var http = new HttpClient();
async Task<JsonNode> Gql(string query, object variables, string authHeader, string authValue){ using var request = new HttpRequestMessage(HttpMethod.Post, apiUrl) { Content = JsonContent.Create(new { query, variables }), }; request.Headers.Add("x-huat-platform", "customer"); request.Headers.TryAddWithoutValidation(authHeader, authValue); using var response = await http.SendAsync(request); var body = JsonNode.Parse(await response.Content.ReadAsStringAsync())!; if (body["errors"] is JsonArray errors && errors.Count > 0) { throw new Exception($"{errors[0]?["extensions"]?["code"]}: {errors[0]?["message"]}"); } return body["data"]!;}
// The reply is every MODEL message after the last USER message.static string? ReplyAfterLastUserMessage(JsonArray messages){ var list = messages.Select(m => m!).ToList(); var lastUser = list.FindLastIndex(m => (string?)m["role"] == "USER"); var replies = list.Skip(lastUser + 1) .Where(m => (string?)m["role"] == "MODEL") .Select(m => (string?)m["text"]) .ToList(); return replies.Count > 0 ? string.Join("\n", replies) : null;}
var clientId = $"ticket-{DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()}"; // one clientId per conversationvar text = "My laptop cannot connect to the office VPN since this morning.";
// 1. Start a session with the API key.var started = (await Gql(Start, new { input = new { agentId = agentId, clientId = clientId } }, "X-Api-Key", apiKey))["sdkStart"]!;var sessionId = (int)started["sessionId"]!;var avatarToken = (string)started["avatarToken"]!;
// 2. Send the message with the session token. It returns true at once.await Gql(Send, new { input = new { sessionId = sessionId, text = text } }, "Authorization", $"Bearer {avatarToken}");
// 3. Poll the session's messages until the reply arrives.string? reply = null;var deadline = DateTime.UtcNow + timeout;while (reply is null && DateTime.UtcNow < deadline){ await Task.Delay(pollInterval); var polled = await Gql(Poll, new { externalCustomerId = clientId, first = 1 }, "X-Api-Key", apiKey); foreach (var edge in polled["sessionsByExternalCustomerId"]!["edges"]!.AsArray()) { if ((int)edge!["node"]!["id"]! == sessionId) { reply = ReplyAfterLastUserMessage(edge["node"]!["messages"]!.AsArray()); } }}Console.WriteLine(reply ?? "No reply within the timeout.");
// 4. End the session with the session token.await Gql(End, new { input = new { sessionId = sessionId } }, "Authorization", $"Bearer {avatarToken}");# Hello ticket: start a session, send one message, poll for the reply, end the session.# Python 3.8+ with "requests". Run: WETEL_API_KEY=sk_... WETEL_AGENT_ID=7 python hello_ticket.pyimport osimport time
import requests
API_URL = os.environ.get("WETEL_API_URL", "https://api.wetel.dev/graphql")API_KEY = os.environ["WETEL_API_KEY"]AGENT_ID = int(os.environ.get("WETEL_AGENT_ID", "1"))POLL_INTERVAL_SECONDS = 2TIMEOUT_SECONDS = 60
START = """mutation StartSession($input: SdkStartInput!) { sdkStart(input: $input) { sessionId avatarToken agentId graphqlEndpoint }}"""SEND = """mutation SendMessage($input: SdkSendMessageInput!) { sdkSendMessage(input: $input)}"""POLL = """query PollSession($externalCustomerId: String!, $first: Int) { sessionsByExternalCustomerId(externalCustomerId: $externalCustomerId, first: $first) { totalCount edges { node { id status messages { id role text turnIndex createdAt } } } }}"""END = """mutation EndSession($input: SdkEndSessionInput!) { sdkEndSession(input: $input) { sessionId durationSeconds evaluation { score summary } }}"""
def gql(query, variables, auth): response = requests.post( API_URL, json={"query": query, "variables": variables}, headers={"x-huat-platform": "customer", **auth}, timeout=30, ) body = response.json() if body.get("errors"): error = body["errors"][0] raise RuntimeError(f"{error.get('extensions', {}).get('code')}: {error['message']}") return body["data"]
def reply_after_last_user_message(messages): """The reply is every MODEL message after the last USER message.""" roles = [m["role"] for m in messages] last_user = len(roles) - 1 - roles[::-1].index("USER") if "USER" in roles else -1 replies = [m["text"] for m in messages[last_user + 1 :] if m["role"] == "MODEL"] return "\n".join(replies) if replies else None
def main(): agent_id = AGENT_ID client_id = f"ticket-{int(time.time() * 1000)}" # one clientId per conversation text = "My laptop cannot connect to the office VPN since this morning." with_key = {"X-Api-Key": API_KEY}
# 1. Start a session with the API key. started = gql(START, { "input": { "agentId": agent_id, "clientId": client_id, }, }, with_key)["sdkStart"] session_id, avatar_token = started["sessionId"], started["avatarToken"] with_token = {"Authorization": f"Bearer {avatar_token}"}
# 2. Send the message with the session token. It returns true at once. gql(SEND, { "input": { "sessionId": session_id, "text": text, }, }, with_token)
# 3. Poll the session's messages until the reply arrives. reply = None deadline = time.time() + TIMEOUT_SECONDS while reply is None and time.time() < deadline: time.sleep(POLL_INTERVAL_SECONDS) polled = gql(POLL, { "externalCustomerId": client_id, "first": 1, }, with_key) nodes = [edge["node"] for edge in polled["sessionsByExternalCustomerId"]["edges"]] session = next((n for n in nodes if n["id"] == session_id), None) reply = reply_after_last_user_message(session["messages"] if session else []) print(reply or "No reply within the timeout.")
# 4. End the session with the session token. gql(END, { "input": { "sessionId": session_id, }, }, with_token)
if __name__ == "__main__": main()Why it polls, and the alternatives
Section titled “Why it polls, and the alternatives”- One
clientIdper conversation. Step 3 finds the session through theclientIdyou passed tosdkStart, which is why the program builds a fresh one (ticket-<timestamp>). Use your own ticket or conversation id in a real integration. - Do not poll faster than once a second. A reply usually takes a few seconds and can take longer when the workflow calls tools. Keep a timeout and end the session either way.
- Only messages after yours count. The program reads the messages that follow your last
USERmessage, so anything earlier in the session (an earlier turn, a greeting) is never mistaken for the reply. - With a dashboard JWT you can read one session directly with
session(id) { messages }. That operation does not accept an API key. - Browsers receive replies as they stream over the
sessionEventssubscription instead of polling: see Core API Flow and Events and Subscriptions. - The session token lasts about an hour. Renew it on the same session with
refreshAvatarToken.
Next steps
Section titled “Next steps”- Authentication cheat sheet: the three credentials and the errors each one produces.
- Sessions and messages reference: every operation with samples in five languages, allowed values and example responses.
- Recipe: Route a ticket: the calls that build and run a ticket-routing agent.
- Interactive API reference: every operation on one page, with allowed values, example responses and request samples.
- Downloads: the Postman collection and the OpenAPI file.