Skip to content

Authentication

Wetel uses three distinct credentials, each for a different actor in your integration. They are not interchangeable, and each unlocks a different, non-overlapping part of the API.

TierCredentialLifetimeWho holds itUnlocks
1. Dashboard / builderJWT (Authorization: Bearer <token>)Short-lived (about a day)A human logging into the Wetel dashboard, or a script bootstrapping onceFull account access: create/update agents, workflows, knowledge bases, connectors, and generating API keys
2. API keyOpaque string (x-api-key: sk_...)Long-lived, no expiryYour backend, in server-side .env — never a browserStarting sessions (sdkStart) only
3. Session tokenavatarToken (Authorization: Bearer <token>)Short-lived (about an hour), one per sessionYour frontend, passed down from your backendSending messages, ending sessions, and subscribing to session events for that one session

Each tier exists for a different actor, not as extra ceremony:

  • The JWT is for a human operating the dashboard, or a one-time bootstrap script. It’s deliberately not meant to sit in a running server process long-term.
  • The API key is your integration’s actual long-lived credential. It belongs in your backend’s environment, used for every server-to-server call that starts a session.
  • The session token isn’t something you manage as a secret — it’s minted automatically by sdkStart and handed to your frontend, closer to a session cookie than a credential you rotate or store. It’s short-lived specifically so a token leaked via browser devtools has a small blast radius. A conversation that outlives that hour renews the token in place with refreshAvatarToken — no new session, no second sdkStart.
mutation Login {
login(input: { email: "[email protected]", password: "<YOUR_PASSWORD>" }) {
accessToken
}
}

Headers: x-huat-platform: customer

Use the returned accessToken as Authorization: Bearer <accessToken> on subsequent dashboard calls.

Requires a JWT:

mutation GetKey {
generateApiKey {
key
warning
}
}

Headers: Authorization: Bearer <accessToken>, x-huat-platform: customer

key (formatted sk_...) is returned exactly once. Wetel stores only a SHA-256 hash of it server-side — the raw key is never persisted, and it cannot be retrieved again later. If you lose it, generate a new one.

generateApiKey above gives you one shared key for your whole account — rotating it for one integration or environment invalidates it everywhere else that reuses the same key. If you run more than one consumer against the same Wetel account (a staging environment and production, or two separate apps), create a named key per consumer instead — each is independently revocable without affecting the others:

mutation CreateNamedKey {
createNamedApiKey(name: "production-web-app") {
key
warning
}
}

Headers: Authorization: Bearer <accessToken>, x-huat-platform: customer

Same one-time-reveal behavior as generateApiKeykey is only ever shown in this response. Authenticate with it exactly like the legacy key (x-api-key: <key>); both forms are checked by the same guard, so existing integrations using generateApiKey are unaffected either way.

Revoke a specific named key without touching any other key on the account:

mutation RevokeNamedKey {
revokeNamedApiKey(id: 12)
}

Revocation is immediate and permanent — there’s no “un-revoke,” create a new named key instead.

The API key unlocks exactly one thing: starting a session.

mutation StartSession {
sdkStart(input: { agentId: 1 }) {
sessionId
avatarToken
}
}

Headers: x-api-key: <YOUR_API_KEY>, x-huat-platform: customer

sdkStart returns the avatarToken your frontend uses for everything else in that session.

Everything after sdkStart — sending messages, ending the session, subscribing to session events — uses the avatarToken, never the API key:

mutation SendMessage {
sdkSendMessage(input: { sessionId: 42, text: "Hello" })
}

Headers: Authorization: Bearer <avatarToken>, x-huat-platform: customer

Never use the API key in client-side code, and never use your own app’s user JWT (if you have one) as a Wetel credential. The avatarToken is the only credential that belongs on the client.

An avatarToken is valid for about an hour. A conversation that runs longer than that — a long-running embed left open on a tab, a channel-bridged chat that a user comes back to after lunch, any headless integration holding one session for hours — used to simply start failing with Invalid or expired avatar token, with no recovery path short of calling sdkStart again and losing the conversation’s session. refreshAvatarToken closes that gap: it exchanges the current token for a fresh, full-TTL one on the same session.

mutation RefreshAvatarToken($input: RefreshAvatarTokenInput!) {
refreshAvatarToken(input: $input) {
sessionId
avatarToken
expiresInMs
}
}
{ "input": { "avatarToken": "<your-current-or-just-expired-avatarToken>" } }

Headers: x-huat-platform: customer only.

The token goes in input.avatarToken, not in an Authorization: Bearer header — this is the one avatarToken-authenticated operation that has to accept an already-expired token, and the normal header-based guard rejects one outright. No API key and no JWT are involved, so this is safe to call from the browser, from your backend, or from wherever the token currently lives.

The response gives you the replacement token plus expiresInMs, so you can schedule the next refresh off the server’s own TTL rather than hardcoding an hour.

You do not have to refresh before the token expires — a client whose token is still valid usually has no reason to know it’s about to. The mutation accepts a token that expired within the last 10 minutes (AVATAR_TOKEN_REFRESH_GRACE_MS, configurable per deployment), so the natural trigger works: make the call, get Invalid or expired avatar token back, refresh, retry. Past the grace window the token is gone for good and you need a new sdkStart.

Only the expiry bound is relaxed, and only that far. The token’s signature is still verified in full, and the session it is bound to must still exist and be ACTIVE. The replacement carries the same sessionId, the same tenant, and the same avatar backend config as the token you presented — it can never be used to move a session, widen a token’s scope, or reach another tenant’s data.

Reconnect the WebSocket — you cannot swap the token into a live one

Section titled “Reconnect the WebSocket — you cannot swap the token into a live one”

Plain HTTP mutations need no special handling — sdkSendMessage and sdkEndSession each carry their own Authorization header, so they pick the new token up on the very next call.

Rate-limited to 10 calls per minute per client IP. A single session legitimately needs one refresh per hour, so this is generous for real use while still capping brute-force attempts against an endpoint that by design accepts an expired credential.

ResponseMeaning
Invalid or expired avatar tokenBad signature, malformed token, expired beyond the grace window, or a session that no longer exists. Deliberately indistinguishable — start a new session.
Session <id> is not active (status: <...>)The token is fine but the session has already ended. Start a new one; there is nothing to refresh into.

Full field-by-field reference: refreshAvatarToken.

The legacy single API key (generateApiKey): rotating is revoking, and it’s instant. There’s no separate “revoke” mutation for this one — calling generateApiKey again overwrites the stored hash, so the old key stops matching on the very next request. There’s no cache layer in front of the check, so there’s no propagation delay to worry about. If it leaks, generate a new one and update your backend’s environment; the old one is dead immediately. This still rotates your only key, though — if multiple integrations share it, they all break at once.

Named API keys have a real, targeted revoke (revokeNamedApiKey(id), see above) — also instant, same no-cache-delay behavior, but scoped to just that one key. Use named keys instead of the legacy single key whenever more than one consumer shares an account, specifically so a leak or rotation in one doesn’t take down the others.

JWT: cannot be revoked before it expires. JWTs are stateless and are never persisted anywhere, so there’s nothing to invalidate server-side if one leaks — it remains valid until its expiry, which is roughly a day. This is a known current constraint, not a bug: keep JWTs out of long-running server processes, use them only for the dashboard/bootstrap flows described above, and rely on the API key for anything that needs to sit in your backend long-term.

Session tokens (avatarToken) are not something you revoke directly — they’re short-lived and scoped to a single session. Calling sdkEndSession ends the session; the token has no further use after that regardless. Ending the session also closes off refreshAvatarToken, which only renews a token whose session is still ACTIVE — so a refreshable token can never outlive the session it belongs to.

  • Getting Started — a minimal end-to-end example using these credentials.
  • Core API Flow — the full sdkStartsdkSendMessage → subscription → sdkEndSession lifecycle.
  • Agents — creating and configuring agents (requires a JWT).
  • Multi-Tenancy — how your API key scopes every call to your own account’s data.
  • Troubleshooting — common errors, including what “Platform is not specified!” actually means.