Rate Limiting
This page lists every rate limit currently enforced by the Wetel API, sourced directly from the API’s throttling configuration (last reconciled against the deployed code on 2026-09-17). Anything not listed here has no enforced limit today — see No limit today below before relying on that in production.
How limits are keyed
Section titled “How limits are keyed”Every GraphQL rate limit below is keyed by the calling client’s IP address — not by API key,
tenant, or user, with one exception: generateText added a second, independent
per-tenant ceiling alongside its per-IP limit on 2026-09-24 — see that section below. Two
consequences worth designing for on every other operation:
- If your backend proxies many end users through one server, all of them share that server’s bucket. Apply your own per-user fairness limiter in front if that matters to you.
- Conversely, one abusive caller cannot exhaust a tenant’s budget for everyone else — which is
the reason
sdkStartis limited this way rather than per tenant.
“Client IP” is the address your request reaches Wetel from, read at the edge from Cloudflare’s
CF-Connecting-IP; a request that does not come through Cloudflare cannot set it. Before
2026-10-06 the platform counted the Cloudflare edge address instead, so callers who happened to share
an edge could also share a count. Each real address is now counted on its own, which is what this page
has always described. If your servers or your users sit behind one NAT, they share one bucket.
The window is fixed per operation (mostly 60 seconds). Exceeding a limit returns a GraphQL error
with HTTP 429 semantics; back off and retry after the window.
Since 2026-10-29 on staging (production rollout pending), that error carries extensions.code TOO_MANY_REQUESTS and extensions.retryAfterSeconds, with a message like Too many requests. Try again in about 2 minutes.; extensions.status stays 429 and the HTTP status of the response is unchanged. Before, the code was INTERNAL_SERVER_ERROR. See Error handling.
All throttled GraphQL operations require the standard x-huat-platform: customer header
alongside your normal authentication — see Authentication.
Sessions
Section titled “Sessions”| Operation | Limit | Window |
|---|---|---|
sdkStart | 30 requests | per 60 seconds, per client IP |
refreshAvatarToken | 10 requests | per 60 seconds, per client IP |
speechToken | 30 requests | per 60 seconds, per client IP |
agentActivitySummary | 30 requests | per 60 seconds, per client IP |
Role templates
Section titled “Role templates”| Operation | Limit | Window |
|---|---|---|
agentTemplates | 30 requests | per 60 seconds, per client IP |
hireAgentTemplate | 5 requests | per 60 seconds, per client IP |
provideAgentTemplateSlotCredential | 10 requests | per 60 seconds, per client IP |
bindAgentTemplateSlot | 20 requests | per 60 seconds, per client IP |
Hiring also has its own ceilings per workspace: 10 hired roles at once and 5 hires per 24 hours.
The partner operations (an API key with the templates:hire scope) have their own limits, also per client IP per 60 seconds: partnerHireAgentTemplate, partnerAgentTemplates, partnerAgentTemplateInstall and partnerAgentTemplateInstalls 60 requests, partnerBindAgentTemplateSlot 20 requests. Their per-key ceilings are 200 roles hired at once and 50 hires per 24 hours, unless Wetel set other limits for the key. Live on staging and in production since 2026-10-09.
The numbers above are the defaults. sdkStart and refreshAvatarToken are deployment settings (SDK_START_LIMIT_PER_MIN, AVATAR_TOKEN_REFRESH_LIMIT_PER_MIN), so an environment tuned for a higher-volume partner can differ from this page; the limit that applies to you is the one in the error you get back. Separately, every new session also asks the avatar service for its token, which is limited to 60 per minute per source IP for the whole backend, so the effective session-start ceiling is the lower of the two.
sdkStart: added 2026-09-08. Every sdkStart creates a session and fires a metered
opening-greeting LLM call, so an unthrottled endpoint would let anyone holding a leaked API key
inflate a tenant’s bill. Thirty per minute is far above any real embed’s start rate; if you
legitimately need more (e.g. a load test), run it from more than one source IP.
agentActivitySummary: added 2026-10-03. It is the most expensive read in the schema (grouped scans
over up to 92 days of runs), so a dashboard should fetch it on load and on focus rather than polling.
The limit is per client IP, so a team behind one office network shares it.
refreshAvatarToken: added 2026-09-14. This is the one operation that deliberately accepts an
already-expired credential (within its grace window — see
refreshAvatarToken), so the rate limit is the
only thing capping brute-force attempts against the token signature. One session legitimately needs
about one refresh per hour, so ten per minute leaves very large headroom. Keyed per client IP rather
than per tenant on purpose: a tenant-keyed bucket would let one abusive caller lock that tenant’s
real embeds out of refreshing at all.
Knowledge base / embeddings
Section titled “Knowledge base / embeddings”| Operation | Limit | Window |
|---|---|---|
embed (text embedding query) | 10 requests | per 60 seconds, per client IP |
requestKnowledgeDocumentUpload (step 1 of document upload) | 10 requests | per 60 seconds, per client IP |
confirmKnowledgeDocumentUpload (step 2 of document upload) | 10 requests | per 60 seconds, per client IP |
ingestDocumentFromUrl | 10 requests | per 60 seconds, per client IP |
embed also caps texts at 100 entries per call. The embeddings limit is documented in context
at Embeddings Integration.
Example request against a throttled operation:
curl -X POST https://api.wetel.dev/graphql \ -H "Content-Type: application/json" \ -H "X-Api-Key: <key>" \ -H "x-huat-platform: customer" \ -d '{"query":"query { embed(texts: [\"hello\"]) { vectors } }"}'Custom actions
Section titled “Custom actions”| Operation | Limit | Window |
|---|---|---|
createCustomAction | 10 requests | per 60 seconds, per client IP |
updateCustomAction | 10 requests | per 60 seconds, per client IP |
deleteCustomAction | 10 requests | per 60 seconds, per client IP |
testCustomAction | 10 requests | per 60 seconds, per client IP |
Workflow generation
Section titled “Workflow generation”| Operation | Limit | Window |
|---|---|---|
aiGenerateWorkflow (LLM-assisted workflow graph generation) | 5 requests | per 60 seconds, per client IP |
generateWorkflowDiff (LLM-assisted incremental edit) | 20 requests | per 60 seconds, per client IP |
aiGenerateWorkflow is tighter than the others because each call triggers a full-graph LLM
generation, not a simple CRUD write. generateWorkflowDiff is looser because it’s designed for an
interactive co-pilot loop.
Workflow tasks
Section titled “Workflow tasks”Added 2026-09-21.
| Operation | Limit | Window |
|---|---|---|
runWorkflowTask | 60 requests | per 60 seconds, per client IP |
workflowTaskOutcome | 60 requests | per 60 seconds, per client IP (the same bucket as runWorkflowTask) |
runWorkflowTask can fan out into LLM calls and
outbound partner requests, so an unthrottled endpoint would let anyone holding a leaked API key
inflate a tenant’s bill (and hammer a partner’s API) — the same reasoning as sdkStart. The ceiling
is deliberately looser than sdkStart’s 30/min because a machine orchestrator legitimately bursts (a
queue drain, a retry sweep) where a human-driven embed does not. Keyed per client IP, like everything
else here: a tenant-keyed bucket would let one abusive caller lock a tenant’s real orchestrator out
of its own endpoint.
Per-workspace cap on tasks in flight (in production since 2026-10-29). Separately from the per-IP limit, one workspace can have a limited number of headless tasks (calls to runWorkflowTask and scheduled fires) waiting or running at once: 10 by default. A call over that cap is refused with HTTP 429 and recorded with the rejection code TENANT_BUSY; retry when an earlier task finishes. Chat turns do not count towards it. The count is best effort: two simultaneous calls can both get in by one. Over GraphQL, read the message rather than extensions.code, which is INTERNAL_SERVER_ERROR for a 429.
LLM API
Section titled “LLM API”| Operation | Limit | Window |
|---|---|---|
generateText | 30 requests | per 60 seconds, per client IP |
generateText | 60 requests | per 60 seconds, per tenant |
startTextGeneration | 10 requests | per 60 seconds, per client IP |
Updated 2026-09-24: generateText’s per-IP floor was raised from 10/min to 30/min, and a second,
independent per-tenant ceiling of 60/min was added — both must pass. This is the one operation on
this whole page keyed partly by tenant rather than client IP alone, so a partner backend proxying
many end users through one server IP isn’t bottlenecked by the IP tier the way every other
operation here is by design (see How limits are keyed above). The tenant
is resolved from whichever credential authenticated the call — a dashboard JWT or an X-Api-Key
header, both now accepted (see LLM API: Auth).
startTextGeneration is unchanged — still 10/min per client IP only, X-Api-Key only.
See LLM API for the request shapes (including the 4,000-character
prompt cap).
Dashboard-only operations
Section titled “Dashboard-only operations”| Operation | Limit | Window |
|---|---|---|
testLlmProvider | 10 requests | per 60 seconds, per client IP |
llmModelCatalog | 60 requests | per 60 seconds, per client IP |
transcribeSpeech | 10 requests | per 60 seconds, per client IP |
Authentication
Section titled “Authentication”Added 2026-09-08 (see the changelog).
| Operation | Limit | Window |
|---|---|---|
login | 10 requests | per 60 seconds, per client IP |
loginWithWebbyxOne | 10 requests | per 60 seconds, per client IP |
register | 5 requests | per 60 seconds, per client IP |
isRegisterable | 5 requests | per 60 seconds, per client IP |
refreshToken | 20 requests | per 60 seconds, per client IP |
generateOTP | 3 requests | per 5 minutes, per client IP |
validateSecuredToken | 5 requests | per 60 seconds, per client IP |
forgotPasswordWithOTP | 5 requests | per 15 minutes, per client IP |
resetUserPasswordWithUrl | 5 requests | per 15 minutes, per client IP |
requestEmailVerification | 3 requests | per 15 minutes, per user (staging; production rollout pending) |
confirmEmailVerification | 5 requests | per 15 minutes, per user (staging; production rollout pending) |
Avatar service (TTS / STT / avatar sessions)
Section titled “Avatar service (TTS / STT / avatar sessions)”The avatar service’s REST endpoints (POST /tts, POST /tts/google, POST /stt,
POST /avatar-session) carry their own throttles, separate from the GraphQL API above. Exact
limits and request/response shapes are documented at
Avatar Service REST Reference — not repeated here to
avoid the two pages drifting out of sync.
Added 2026-09-17: each of those four routes is now governed by two independent limits that
must both pass — a per-client-IP ceiling (unchanged, same numbers as before) and a new,
higher per-tenant ceiling shared across every caller authenticated with that tenant’s avatar
tokens. An unauthenticated or invalid-token request is governed by the IP limit alone; the tenant
limit only ever applies once a valid avatarToken resolves a tenant. See the REST reference page
for the exact numbers per route.
Changed 2026-10-05: the per-address ceilings on the speech routes are 120 a minute for speech-to-text and 300 for text-to-speech (they were 10 and 30, and were in effect shared by every caller). The per-tenant ceilings (200 and 600) are unchanged and normally bind first.
Structural limits (not rate limits)
Section titled “Structural limits (not rate limits)”These apply to every request regardless of rate:
- Query depth ≤ 7 and query complexity ≤ 50 — see Request limits.
sendMessage/sdkSendMessagetext≤ 100,000 characters — rejected, not truncated.- Request body ≤ 10 MB on the GraphQL API (6 MB on the avatar service’s REST routes). An oversized request is refused with a
413that carries no CORS headers, so a browser reports it as a CORS error rather than a size error: check the size first when a large request fails that way. ingestDocumentinlinetext≤ 500,000 characters; uploaded files ≤ 20 MB, PDF, DOCX, Markdown, CSV, or XLSX (Markdown/CSV/XLSX support added 2026-09-17 — see Knowledge Base (RAG): File upload).
No limit today
Section titled “No limit today”The following core session operations have no enforced rate limit as of this writing:
sdkSendMessagesdkEndSessionsendMessage,startSession,endSession
Any other GraphQL operation not listed in the tables above is also currently unthrottled.
This is a deliberate current state, not a guarantee. Treat anything not explicitly listed on this
page as subject to change without notice, and design your integration (including any
headless / channel-connector integration) with your own
backoff and retry handling regardless of whether a limit is currently enforced server-side —
especially for any integration that fans a single external event (e.g. a busy group chat) into
many sdkSendMessage calls in a short window.