Appearance
Errors
Every FluidTalk Characters API error is an HTTP status code paired with a JSON body. The body always carries a top-level error object — with a stable machine-readable code, a human-readable message, a type (client_error or server_error), and the request_id — so you can branch on the status code and the error.code value, and quote the request_id to support.
This page is the catalog of statuses a connector token sees, the shape of the error body, the request_id you use for support, and how both SDKs map each status to a typed error class.
- Base URL:
https://api-talk.fluidvip.com/api/v1/characters - See also: authentication.md · billing.md · rate-limits.md · reference/index.md
The error body
Every non-2xx response has this envelope:
json
{
"error": {
"code": "invalid_token",
"message": "connector token is missing, invalid, or revoked",
"type": "client_error",
"request_id": "req_7f3a9c2e1b"
}
}| Field | Type | Description |
|---|---|---|
code | string | Stable, machine-readable error code (e.g. invalid_token). Branch on this — it does not change. |
message | string | Human-readable explanation, for logs and operators. Do not pattern-match its exact text; it may change. |
type | string | client_error for every 4xx, server_error for every 5xx. |
request_id | string | The id of this request (req_...). Same value as the X-Request-Id response header. Quote it to support. |
Always read the HTTP status first, then error.code for the precise case. Both are stable. Every response — success or error — also carries an X-Request-Id header with the same value as request_id; see The request_id.
This envelope is the same on every endpoint. On success the payload is wrapped instead as { "data": <result>, "request_id": "req_..." } — see reference/index.md.
Status catalog
| Status | error.code | type | Meaning | What to do |
|---|---|---|---|---|
| 400 | invalid_request | client_error | Malformed or missing field the API could parse but not accept (e.g. a missing required platform or handle). | Fix the request and retry. |
| 401 | invalid_token | client_error | Missing, invalid, or revoked connector token. | Check the X-Connector-Token header. See authentication.md. |
| 402 | payment_required | client_error | The character owner's wallet can't cover this turn — returned before any model call. | Top up the wallet, then retry. See billing.md. |
| 403 | forbidden | client_error | We know which character you are, but this action isn't allowed for it. | Don't retry unchanged — check the platform binding / plan. |
| 404 | not_found | client_error | Not found — or not yours (a resource that belongs to another character). | The id or path does not exist for your character. |
| 409 | conflict | client_error | The request conflicts with the current state of the resource. | Resolve the conflicting state, then retry. |
| 413 | payload_too_large | client_error | The uploaded body is over the limit — only POST /inbound-media (10 MB) returns this. | Downscale the image and retry. |
| 422 | validation_error | client_error | The body is well-formed JSON but a field failed validation. | Correct the indicated field and retry. |
| 429 | rate_limited | client_error | Too many requests. | Honor the Retry-After header, then retry. See rate-limits.md. |
| 500 | internal_error | server_error | Something went wrong on our side. | Retry with backoff; if it persists, contact support with the request_id. |
| 502 | upstream_error | server_error | A service we depend on failed — only POST /inbound-media (file storage) returns this. | Retry with backoff. |
| 503 | service_unavailable | server_error | A dependency is unavailable or unconfigured — only POST /inbound-media returns this. | Retry later, not in a tight loop. |
Notes on individual statuses
- 401 vs 403.
401means we could not authenticate your token (no token, a malformed one, or a revoked one).403means we authenticated your token — we know which character you are — but this action is not allowed (for example the platform binding is disabled, or the action isn't available to this character or plan). - 404, never 403, across characters. Your token is one character. FluidTalk never reveals whether an id exists for a different character. A follow-up id at
POST /followups/{id}/ackthat belongs to another connector returns404exactly as if it did not exist. Do not read a404as proof a resource was deleted — it may simply not be addressable by your token. - 402 before any model call. When billing is enabled and the wallet can't cover a generated turn, the API returns
402 payment_requiredbefore calling the model — so you are never charged for a refused turn. Top up the wallet and retry. See billing.md. - "Ignored" and "deduped" are not errors. Several endpoints answer with HTTP
200and a soft outcome instead of an error — handle these in your normal success path, not your error path:- multi-account dedup ignored an inbound →
{ "ignored": true, "ignore_reason": "lead_claimed_by_other_account" }(send nothing). See guides/multi-account-dedup.md. - an unconfigured trigger →
{ "ignored": "event not configured" }. - comments not enabled for this character/platform →
{ "ignored": "comments not configured" }. - a re-delivered
external_event_idon/eventsor/triggers→{ "deduped": true }(applied exactly once).
- multi-account dedup ignored an inbound →
- Some actions require the dashboard. Creating characters, binding platforms, and issuing connector tokens are owner-only, interactive actions that are not exposed to a connector token — perform those in the FluidTalk dashboard (
https://talk.fluidvip.com).
The request_id
Every response — success or error — carries the same id two ways: the X-Request-Id response header, and a request_id field (under data's envelope on success, inside the error object on failure). It is the single value to quote when you contact support, so we can find the exact call in our logs.
http
HTTP/1.1 402 Payment Required
Content-Type: application/json
X-Request-Id: req_91b2d4a07c
{"error":{"code":"payment_required","message":"wallet balance is insufficient for this turn","type":"client_error","request_id":"req_91b2d4a07c"}}Both SDKs surface it on every typed error as request_id (Python) / requestId (TypeScript). Log it next to your own request so a support ticket can correlate the two.
SDK error-class mapping
Both SDKs parse the response and raise a typed error. Every typed error derives from ApiError, so you can catch all SDK/API failures in one place (except ApiError / catch (e instanceof ApiError)) or branch on a specific subclass. Each error exposes the HTTP status, the code, the message, and the request_id (requestId in TypeScript).
| HTTP status | error.code | Python class | TypeScript class | Covers |
|---|---|---|---|---|
| 401 | invalid_token | AuthError | AuthError | Missing / invalid / revoked connector token |
| 402 | payment_required | PaymentRequiredError | PaymentRequiredError | Wallet can't cover the turn |
| 403 | forbidden | PermissionError | PermissionError | Authenticated, but the action isn't allowed |
| 404 | not_found | NotFoundError | NotFoundError | Not found / not yours |
| 409 | conflict | ConflictError | ConflictError | Request conflicts with current state |
| 422 | validation_error | ValidationError | ValidationError | A field failed validation |
| 429 | rate_limited | RateLimitError | RateLimitError | Too many requests |
| 400, 500, and any other non-2xx | invalid_request, internal_error, … | ApiError | ApiError | Everything else (read .status / .code) |
ApiErroris both the base class and the catch-all. Statuses without a dedicated subclass — including400(invalid_request) and500(internal_error) — surface asApiError; branch onerror.status/error.codeto tell them apart. In Python, the 403 class is imported asPermissionError(it is defined internally asPermissionError_to avoid shadowing the builtin, but exported under thePermissionErrorname).
Python — try / except
python
from fluidtalk import (
FluidTalk,
AuthError,
PaymentRequiredError,
PermissionError,
NotFoundError,
ConflictError,
ValidationError,
RateLimitError,
ApiError,
)
ft = FluidTalk(token="ftc_live_8f3c...")
try:
reply = ft.chat(platform="instagram", handle="mark", message="hey ava!")
except AuthError:
# 401 — connector token missing / invalid / revoked
raise
except PaymentRequiredError:
# 402 — the character's wallet can't cover this turn; top up, then retry
...
except PermissionError:
# 403 — token is valid, but this character can't perform the action
...
except NotFoundError:
# 404 — the addressed resource does not exist for your character
...
except ConflictError:
# 409 — the request conflicts with the current state; resolve, then retry
...
except ValidationError as e:
# 422 — a field failed validation; fix it, then retry
...
except RateLimitError:
# 429 — honor the Retry-After header, then retry (see rate-limits.md)
...
except ApiError as e:
# 400 / 500 / other — branch on e.status and e.code
print(e.status, e.code, e.request_id)
raiseTypeScript — try / catch
typescript
import {
FluidTalk,
AuthError,
PaymentRequiredError,
PermissionError,
NotFoundError,
ConflictError,
ValidationError,
RateLimitError,
ApiError,
} from "fluidtalk";
const ft = new FluidTalk({ token: "ftc_live_8f3c..." });
try {
const reply = await ft.chat({ platform: "instagram", handle: "mark", message: "hey ava!" });
} catch (e) {
if (e instanceof AuthError) {
// 401 — connector token missing / invalid / revoked
} else if (e instanceof PaymentRequiredError) {
// 402 — wallet can't cover this turn; top up, then retry
} else if (e instanceof PermissionError) {
// 403 — token is valid, but this character can't perform the action
} else if (e instanceof NotFoundError) {
// 404 — the addressed resource does not exist for your character
} else if (e instanceof ConflictError) {
// 409 — conflicts with the current state; resolve, then retry
} else if (e instanceof ValidationError) {
// 422 — a field failed validation; fix it, then retry
} else if (e instanceof RateLimitError) {
// 429 — honor the Retry-After header, then retry (see rate-limits.md)
} else if (e instanceof ApiError) {
// 400 / 500 / other — branch on e.status and e.code
console.error(e.status, e.code, e.requestId);
} else {
throw e;
}
}When you make raw HTTP calls instead of using an SDK, replicate the same logic: switch on the HTTP status, and read error.code for the precise case.
Raw HTTP — inspecting the body with curl
bash
curl -i -X POST "https://api-talk.fluidvip.com/api/v1/characters/chat" \
-H "X-Connector-Token: ftc_live_BAD" \
-H "Content-Type: application/json" \
-d '{"platform": "instagram", "handle": "mark", "message": "hey ava!"}'A bad token responds:
http
HTTP/1.1 401 Unauthorized
Content-Type: application/json
X-Request-Id: req_7f3a9c2e1b
{"error":{"code":"invalid_token","message":"connector token is missing, invalid, or revoked","type":"client_error","request_id":"req_7f3a9c2e1b"}}Retrying
- Retry
429(after theRetry-Afterdelay) and transient5xx(internal_error) responses with backoff. - Retry after a fix
402once the wallet is topped up, and409once the conflicting state is cleared. - Do not retry
400,401,403,404, and422unchanged — they signal a request, token, or configuration problem that retrying will not resolve. Fix the input, token, id, or field first.
Note that /chat has no message id, so a re-sent inbound creates a new turn — ensure at-most-once delivery of inbound DMs (or send a stable session_id to keep them in one conversation). /events and /triggers are idempotent on external_event_id, so re-delivering those is safe (deduped: true). See rate-limits.md for per-token and per-action limits and the Retry-After contract.