message is one sentence you can show a person. errors lists the individual problems when a failure had more than one; when it did not, errors is an empty array — never absent, never null.
detail.message and detail.errors and it will work everywhere, including on a failure you have never seen.
Request validation
A request that does not match the operation’s schema comes back as 422, with one line per problem in the formfield.path: problem.
body, query and path prefixes are stripped, so config.llm.model is what to look for in your payload, not body.config.llm.model.
When there is exactly one problem, that line is also the message, so a single-issue failure reads well without opening errors:
Every status code
There is no
429. Nothing on the platform rate-limits inbound requests — see limits. A 429 you receive came from one of your own providers being relayed.402, in full
402 appears in exactly two places: POST /v1/calls/token (minting a web call token) and POST /v1/calls/outbound (placing a phone call). Nowhere else returns it.
close_reason: "insufficient_credits", and a running batch pauses rather than failing. The balance is per region, so this is the balance of whichever base URL you called. See pricing and credits.
401 versus 403 versus 503
All three come out of the same authorization step and they mean three different things.
Treating a
503 like a 401 — clearing credentials, forcing a re-login, alerting a human — is the wrong reaction to a network blip. They are opposite verdicts: one is final, one is transient.
502 means a provider, not us
A502 is Talqing having called something on your behalf and not got an answer. It shows up when a provider-key check cannot reach the provider, when a voice or avatar gallery is unavailable, when a carrier’s API does not respond, and when the code-execution service is unreachable.
It is specifically not a verdict on what you sent. A 502 from PUT /v1/byok/{provider} means we could not verify the key, so nothing was stored and your existing key is untouched — retrying costs one click. See provider keys.
Validation responses are not errors
POST /v1/agents/{agent_id}/validate and POST /v1/tools/{tool_id}/validate answer 200 whatever they find. The verdict is the body:
errors block a publish. warnings do not. A publish with warnings succeeds and hands the warnings back on the publish response. So an empty errors array means “this will publish”, not “this is good” — read the warnings anyway, because they are where the things that will surprise a caller live.
Trying to publish with errors present returns 400 with those same lines in detail.errors. See validation.
Retrying
Retry500, 502 and 503, with exponential backoff. Retry nothing else. Every other code is a verdict on the request itself, and sending it again changes nothing except your bill.
Neither SDK retries for you. A failed call raises immediately, once — there is no hidden backoff loop, so what you write is what happens.
There is no idempotency key
There is one exception and three things to do instead:- Text messages dedupe themselves.
POST /v1/conversations/messagesrequiresclient_message_id, a UUID you generate. Re-sending with the same one is ignored rather than duplicated, so retrying that call is safe by construction. - Names are unique per workspace. A retried create for an agent, tool or task answers
400 an agent with that name already existsrather than making a second copy. That400is evidence the first attempt worked — treat it as success and go and read the resource. - Read before you retry anything that spends money. After a timeout on
POST /v1/calls/outbound, list calls filtered bycontact_keyorbatch_idbefore dialling again. The call record is written before the carrier is contacted, so a call that got as far as our side is visible even if the response never reached you. - Let batches do the retrying. A call batch or email batch has
max_attemptsandretry_after_minutesbuilt in, and will not re-dial a recipient it already reached. Driving one call at a time from your own loop gives up all of that.
How the SDKs surface an error
Both SDKs raise one exception type for every non-2xx, carrying the same fields the envelope does.TalqingApiError is branded so instanceof holds even when a dependency graph pulls in both the ESM and the CommonJS build of the package. Without that, the check above would silently fall through and the error would keep propagating.
A failure that never reached the API — DNS, an aborted request, a proxy’s HTML 502 — has no envelope to parse. TypeScript rethrows the transport’s own error unchanged. Python raises TalqingAPIError with the raw body text as the message and an empty errors, so the type stays uniform even when the body is not ours.
The TypeScript client also takes an onUnauthorized callback, fired once when the API answers 401. The credentials are gone, so every other in-flight call will fail identically — handling it in one place beats every caller re-deriving it. The error is still thrown afterwards.
Limits
What you can cross, and the two limits that do not exist.
Authentication
Where 401 and 403 come from, and how a role change reaches a token.