Appearance
Core concepts
This page is your mental model for the FluidTalk Characters API. It defines every concept your connector token touches — Character, Platform + handle, Session, the Person (cross-surface awareness), Bubbles, Triggers, Follow-ups, and Comment threads — plus the cross-cutting behaviors that tie them together: the request/response envelope, idempotency, and multi-account dedup.
Each concept maps to one or more endpoints: see Chat, Events, Triggers, Follow-ups, Comments, and Comment replies. New here? Start with the Quickstart.
What the API does — and what it does not. The Characters API generates a character's messages and tracks the relationship. It never touches a platform itself: reading and sending DMs, posting comments, and watching for new followers is your connector's job. How you connect to the platform is your concern. The API is the brain; you are the hands.
How the pieces fit
A character is the top of the tree. Everything else hangs off the (platform, handle) pair that identifies whom the character is talking to:
Character (your connector token — one AI persona)
└── Platform (a workflow bound per platform: instagram, twitter, reddit, …)
└── Lead = (platform, handle) (someone the character talks to)
├── Session (one DM conversation thread → bubbles)
└── Comment thread (a public comment + its threaded replies)And cutting across that tree is the headline feature — the Person:
Person (one human, one consistent persona)
├── their DMs (Sessions)
└── their comments (Comment threads)
└── … across every post, thread, and surface- A Character is your token. You authenticate as exactly one character.
- A Platform is a workflow bound to that character in the dashboard. One character can run on many platforms with the same token — you name the platform in each request body.
- A Lead is whom you talk to, identified by
(platform, handle). Re-using the same pair resumes that relationship. - A Session is one DM conversation thread; a Comment thread is one public comment plus its replies.
- A Person unifies a lead's DM and comment activity into one coherent, aware persona.
You address leads by (platform, handle), not by ids — there is no character_id field (your token is the character) and you rarely pass a session_id. The ids you do see (session_id, a follow-up id, the request_id) are opaque strings. All timestamps are ISO-8601 UTC (e.g. 2026-06-23T14:05:00Z).
Character
The AI persona — and your credential. A character is one consistent personality (say, a character named Ava) with its own voice, photos, lorebook, and per-platform funnels. Your connector token authenticates as that character: the token is the character, so there is no separate account key and no character_id to pass.
You get a character's token from its platform settings in the dashboard. It is shown once — treat it like a password. A token looks like:
ftc_live_8f3c2a7b8e1d4056a1b2c3d4e5f60718Send it on every request in the X-Connector-Token header (see Authentication). The same token works for every platform the character is bound to; the platform field in each request body selects which bound workflow runs.
One character, many platforms, one token. A character can also run several accounts on one platform — see Multi-account dedup.
Platform + handle
How a lead is identified. Every conversational call carries a platform (the network, e.g. "instagram", "twitter", "reddit", "fanvue", "tiktok", or any platform you run) and a handle (the lead's @username, e.g. "mark"). Together, (platform, handle) is the lead's identity.
- The platform selects which of the character's bound workflows runs and scopes the lead —
markon Instagram andmarkon Twitter are two different leads. - Re-using the same
(platform, handle)resumes that lead's relationship automatically: their session, rapport, and history come back.
Handles are normalized (a leading @ and casing are smoothed out), so you don't have to canonicalize before you send them.
Session
One DM conversation thread. A session is the running thread between the character and one lead. It is auto-created and auto-resumed per (character, platform, handle) — you almost never manage it yourself. The reply from POST /chat returns the session_id it used:
json
{
"data": {
"session_id": "sess_9d1e0c4a7b6f2358e9a0b1c2d3e4f506",
"bubbles": [
{ "text": "heyy 👀", "delay_ms": 0, "image_url": null },
{ "text": "what made you message me", "delay_ms": 1400, "image_url": null }
]
},
"request_id": "req_2b44d8fbb0e7c5a1"
}- Omit
session_idand the API resumes (or creates) the lead's natural conversation — the normal case. - Pass
session_idonly to pin a turn to one specific thread (for example, to keep a re-delivered inbound in the same conversation — see Idempotency). - If the conversation has been closed,
bubblesis[](the character has nothing more to say).
Person — cross-surface awareness
The headline feature: one human is one persona, everywhere. A lead who DMs the character and comments on its posts is treated as the same Person. The character stays consistent and remembers across surfaces (DM ↔ comments) and across multiple posts and threads — it won't reintroduce itself, contradict an earlier chat, or forget what the two of you already covered.
You don't construct a Person or pass a person id; the API knits one together from the leads it sees. Two response fields reflect it:
| Field | Where | Meaning |
|---|---|---|
recognized_lead | /comments | true when the commenter is someone the character already knows (e.g. has DMed before). |
aware | /comments, /comments/reply | A cross-surface awareness summary — what the character knows about this Person from other surfaces and threads — that shaped the generated text. |
json
{
"data": {
"ok": true,
"comment": "ok the lighting in this is unreal ✨ how was the trip??",
"recognized_lead": true,
"aware": { "surfaces": ["dm", "comments"], "known_facts": ["planning a trip to Lisbon"] },
"thread_id": "thr_7c5a1d9e3f04"
},
"request_id": "req_0f3a6b8e1d22"
}This is why you pass the same (platform, handle) everywhere it applies: it lets the character behave like one coherent persona in DMs and in public, not a fresh stranger on each surface.
Bubbles
A reply is a list of short messages, not one blob. Real people send a few quick chat bubbles in a row, with pauses between them — so POST /chat (and a cold trigger) returns an ordered bubbles array. Send them in order, honoring each delay, to mimic human typing cadence.
| Field | Type | Description |
|---|---|---|
text | string | The bubble's message text. |
delay_ms | integer | How long to wait before sending this bubble, in milliseconds. |
image_url | string or null | An image the character chose to send with this bubble, or null for text only. |
json
[
{ "text": "omg wait", "delay_ms": 0, "image_url": null },
{ "text": "you're in Lisbon too??", "delay_ms": 1200, "image_url": null }
]An empty bubbles array means the character is intentionally staying silent (closed conversation, or an ignored inbound) — send nothing.
Triggers / entry points
Where a conversation can START. Beyond replying to an inbound DM, a character can be kicked off by an entry-point event — an outreach, a story reaction, a new follower, or a custom event you define. Entry points are configured per platform in the dashboard and fired with POST /triggers, addressed by their event_id.
How a trigger behaves depends on whether a chat already exists:
- Cold (no live conversation) → the character opens the conversation, reacting to the event. The response carries
opened: trueand abubblesarray to send. - Warm (a chat is already running) → the event becomes the next turn's stimulus, folded into the ongoing thread.
json
{
"data": {
"ok": true,
"deduped": false,
"opened": true,
"session_id": "sess_9d1e0c4a7b6f2358e9a0b1c2d3e4f506",
"bubbles": [ { "text": "thanks for the follow 🙈", "delay_ms": 0, "image_url": null } ]
},
"request_id": "req_a1b2c3d4e5f6"
}An unknown event_id is a safe no-op ({ "ok": true, "ignored": "event not configured" }), so you can wire up a webhook before the entry point exists in the dashboard without breaking anything.
Follow-ups
Proactive re-engagement, delivered as a pull-queue. A character periodically queues "hey stranger" rekindle messages for dormant leads. You don't get these pushed at you — you pull them, send each one on the platform, then ack them. This makes the queue safe to poll on a schedule and idempotent to retry.
The two-step cycle:
GET /followups— pull this connector's pending follow-ups (oldest first).POST /followups/{id}/ack— mark one delivered once you've sent it (terminal, idempotent).
Each follow-up looks like:
| Field | Type | Description |
|---|---|---|
id | string | The follow-up id — pass it to ack. |
handle | string | The lead's @username to send to. |
platform | string | The platform to send on. |
kind | string | The follow-up type (e.g. a rekindle). |
message | string | The text to send. |
created_at | ISO-8601 UTC | When the follow-up was queued. |
json
{
"data": {
"followups": [
{
"id": "fu_3e51b9a8d771c6e5",
"handle": "mark",
"platform": "instagram",
"kind": "rekindle",
"message": "hey stranger, you disappeared on me 🥲",
"created_at": "2026-06-22T08:05:00Z"
}
]
},
"request_id": "req_b2f9a40f0a4d2c8"
}Pull, deliver, then ack. A follow-up stays
pendinguntil you ack it, so an interrupted run simply re-pulls it next time — you never silently drop a rekindle.
Comment threads
The character's public comment plus the back-and-forth under it. A character can engage in public, not just in DMs. You hand the API a post and it generates a top-level comment to post; when someone replies, you hand it the reply and it generates a threaded reply — to the original poster or to other commenters, and it can fire multiple times in one thread.
POST /comments→ a top-level comment for a post. Returns thecommenttext, plusrecognized_leadandaware(see Person) and athread_id.POST /comments/reply→ a reply within a thread. Returns adecision("engage","drive_to_dm", or"skip") and thereplytext — which isnullwhen the character chooses not to reply.
json
{
"data": {
"ok": true,
"decision": "drive_to_dm",
"reply": "haha you're trouble — dm me and i'll tell you 👀",
"aware": { "surfaces": ["comments"], "thread_turns": 2 }
},
"request_id": "req_5b2f9a400f3a6b8e"
}Threads are cross-thread aware — the character remembers a Person across different posts and threads, and across the DM ↔ comment boundary. If comments aren't enabled for this character on this platform, the call is a safe no-op: { "ok": true, "ignored": "comments not configured" }.
The request/response envelope
Every endpoint returns the same envelope, so you parse responses one way across the whole API.
On success (HTTP 2xx) the real payload is always under data, alongside a request_id:
json
{
"data": { "...": "the endpoint's result" },
"request_id": "req_2b44d8fbb0e7c5a1"
}On error (HTTP 4xx/5xx) you get a structured error object instead:
json
{
"error": {
"code": "invalid_token",
"message": "The connector token is missing or not recognized.",
"type": "client_error",
"request_id": "req_2b44d8fbb0e7c5a1"
}
}Every response — success or error — also carries an X-Request-Id header with the same value as request_id. Quote it when you contact support or grep your logs. The full status → code mapping is on the Errors page; the SDKs unwrap data for you and raise a typed error per code.
Idempotency
Webhooks get re-delivered — so the event endpoints are idempotent, and chat is not.
POST /eventsandPOST /triggersrequire anexternal_event_id(unique per owner + platform). A re-delivered webhook with the same id applies exactly once; repeats returndeduped: trueand change nothing. Use your platform's native event id as the key.POST /followups/{id}/ackis idempotent on the follow-up id — acking an already-delivered follow-up is a safe no-op.POST /chathas no message id, so a re-sent inbound creates a new turn. Your connector should ensure at-most-once delivery of inbound DMs — or pass a stablesession_idto keep a retried inbound in the same conversation.
json
{
"data": { "ok": true, "deduped": true },
"request_id": "req_9a0b1c2d3e4f5061"
}Multi-account dedup
One character, several accounts on the same platform, but only one ever replies. A character can run multiple accounts on one platform (e.g. two Instagram accounts). With dedup enabled on the character, the first account a lead messages claims that lead; a later inbound from a different account is ignored, so a single lead never gets answered twice.
To participate, tell the API which account received each event:
- Pass
own_username(the bot account that received the DM) on/chatand/triggers. - Pass
?own_username=on/followups— a claimed follow-up is delivered only to the account that owns it.
When a call is deduped away, /chat tells you to send nothing:
json
{
"data": {
"session_id": null,
"bubbles": [],
"ignored": true,
"ignore_reason": "lead_claimed_by_other_account"
},
"request_id": "req_8d771c6e5b2f9a40"
}Dedup is off by default — omit own_username and every account behaves normally. Full setup, the "mother/slave" model, and edge cases are in Multi-account dedup.
Where to go next
- Make your first call: Quickstart
- How auth works: Authentication
- Reply to DMs: Chat reference
- Open conversations: Triggers reference · Re-engage: Follow-ups reference
- Go public: Comments · Comment replies
- Report sales: Events reference
- Run multiple accounts: Multi-account dedup
- What it costs: Billing
- Use a client library: Python SDK · TypeScript SDK
- Full endpoint catalog: API reference
- When things go wrong: Errors · Rate limits