Quickstart: Hello ticket
This content is not available in your language yet.
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.