Every error the API returns has one body, at every status code:
All three fields are always present. message is one sentence you can show a person. errors lists the individual problems, and is [] (never absent or null) when there is only the message. Parse detail.message and detail.errors and your handler works for every failure. code is a stable identifier for an error your code is expected to branch on, and null on every other. Branch on code, never on the wording of message. The codes today all come from posting into a conversation: whatsapp_window_closed, voice_session_live, chat_ended, chat_open_elsewhere, idempotency_conflict, channel_unsupported, no_agent and messages_off (an agent was sent for a WhatsApp number or Telegram bot whose messages trigger is off; WhatsApp batches answer it too).

Status codes

422: schema validation

Each line is field.path: problem, with the body/query/path prefix stripped, so the path matches the key you sent. When there is exactly one problem, that line is also the message; with several, message is invalid request.
A limit above an endpoint’s ceiling is a 422, not a silent clamp.

401 versus 403

402: out of credits

Returned only by the four operations that start a billable session: POST /v1/calls/token, POST /v1/calls/outbound, POST /v1/calls/{session_id}/resume and POST /v1/chats. Sessions already running are unaffected.
An inbound phone call or media stream refused for credit never reaches an HTTP caller: it reads back with close_reason: "insufficient_credits". A batch pauses instead of failing. See pricing and credits.

Validation results are not errors

POST /v1/agents/{agent_id}/validate, POST /v1/tools/{tool_id}/validate and POST /v1/tasks/{task_id}/validate answer 200 with {"errors": [...], "warnings": [...]}, both always present. Errors block a publish; warnings do not, and a successful publish returns them in warnings. Publishing with errors present is a 400 carrying the same lines in detail.errors. See validation.

Retrying

Retry 500, 502 and 503 with exponential backoff. Every other code is a verdict on the request; sending it again changes nothing. Neither SDK retries for you.
Only two endpoints take an idempotency key: POST /v1/task-runs and POST /v1/conversations/items. Resending the same idempotency_key returns the original with Idempotent-Replayed: true. Any other retried POST is a second request: a second call placed, email sent.
Elsewhere:
  • Chat messages dedupe. POST /v1/chats/{chat_id}/messages requires a client_message_id UUID you generate. Re-sending the same id returns the original turn instead of posting twice.
  • Names are unique. A retried create of an agent, tool or task answers 409 (an agent with that name already exists). Treat it as success and read the resource.
  • Read before re-dialling. After a timeout on POST /v1/calls/outbound, list calls by contact_key or batch_id before placing it again.
  • Let batches retry. Call batches carry their own max_attempts and retry_after_minutes.

How the SDKs raise

Both SDKs raise one error type for every non-2xx response, carrying the envelope’s fields.
A request that never got a response (DNS, abort, offline) is not a TalqingApiError in TypeScript: the transport’s own error is rethrown. instanceof TalqingApiError holds across the ESM and CommonJS builds. TalqingClient also takes onUnauthorized, called once on any 401 before the error is thrown.

Limits

Sizes, counts and timeouts the API enforces.

Authentication

Tokens, roles, and where 401 and 403 come from.