Skip to content

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.


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"
  }
}
FieldTypeDescription
codestringStable, machine-readable error code (e.g. invalid_token). Branch on this — it does not change.
messagestringHuman-readable explanation, for logs and operators. Do not pattern-match its exact text; it may change.
typestringclient_error for every 4xx, server_error for every 5xx.
request_idstringThe 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

Statuserror.codetypeMeaningWhat to do
400invalid_requestclient_errorMalformed or missing field the API could parse but not accept (e.g. a missing required platform or handle).Fix the request and retry.
401invalid_tokenclient_errorMissing, invalid, or revoked connector token.Check the X-Connector-Token header. See authentication.md.
402payment_requiredclient_errorThe character owner's wallet can't cover this turn — returned before any model call.Top up the wallet, then retry. See billing.md.
403forbiddenclient_errorWe know which character you are, but this action isn't allowed for it.Don't retry unchanged — check the platform binding / plan.
404not_foundclient_errorNot found — or not yours (a resource that belongs to another character).The id or path does not exist for your character.
409conflictclient_errorThe request conflicts with the current state of the resource.Resolve the conflicting state, then retry.
413payload_too_largeclient_errorThe uploaded body is over the limit — only POST /inbound-media (10 MB) returns this.Downscale the image and retry.
422validation_errorclient_errorThe body is well-formed JSON but a field failed validation.Correct the indicated field and retry.
429rate_limitedclient_errorToo many requests.Honor the Retry-After header, then retry. See rate-limits.md.
500internal_errorserver_errorSomething went wrong on our side.Retry with backoff; if it persists, contact support with the request_id.
502upstream_errorserver_errorA service we depend on failed — only POST /inbound-media (file storage) returns this.Retry with backoff.
503service_unavailableserver_errorA 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. 401 means we could not authenticate your token (no token, a malformed one, or a revoked one). 403 means 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}/ack that belongs to another connector returns 404 exactly as if it did not exist. Do not read a 404 as 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_required before 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 200 and 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_id on /events or /triggers{ "deduped": true } (applied exactly once).
  • 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 statuserror.codePython classTypeScript classCovers
401invalid_tokenAuthErrorAuthErrorMissing / invalid / revoked connector token
402payment_requiredPaymentRequiredErrorPaymentRequiredErrorWallet can't cover the turn
403forbiddenPermissionErrorPermissionErrorAuthenticated, but the action isn't allowed
404not_foundNotFoundErrorNotFoundErrorNot found / not yours
409conflictConflictErrorConflictErrorRequest conflicts with current state
422validation_errorValidationErrorValidationErrorA field failed validation
429rate_limitedRateLimitErrorRateLimitErrorToo many requests
400, 500, and any other non-2xxinvalid_request, internal_error, …ApiErrorApiErrorEverything else (read .status / .code)

ApiError is both the base class and the catch-all. Statuses without a dedicated subclass — including 400 (invalid_request) and 500 (internal_error) — surface as ApiError; branch on error.status / error.code to tell them apart. In Python, the 403 class is imported as PermissionError (it is defined internally as PermissionError_ to avoid shadowing the builtin, but exported under the PermissionError name).

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)
    raise

TypeScript — 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 the Retry-After delay) and transient 5xx (internal_error) responses with backoff.
  • Retry after a fix 402 once the wallet is topped up, and 409 once the conflicting state is cleared.
  • Do not retry 400, 401, 403, 404, and 422 unchanged — 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.

FluidTalk Characters API — part of the Fluidvip ecosystem.