Skip to content

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_8f3c2a7b8e1d4056a1b2c3d4e5f60718

Send 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 — mark on Instagram and mark on 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_id and the API resumes (or creates) the lead's natural conversation — the normal case.
  • Pass session_id only 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, bubbles is [] (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:

FieldWhereMeaning
recognized_lead/commentstrue when the commenter is someone the character already knows (e.g. has DMed before).
aware/comments, /comments/replyA 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.

FieldTypeDescription
textstringThe bubble's message text.
delay_msintegerHow long to wait before sending this bubble, in milliseconds.
image_urlstring or nullAn 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: true and a bubbles array 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:

  1. GET /followups — pull this connector's pending follow-ups (oldest first).
  2. POST /followups/{id}/ack — mark one delivered once you've sent it (terminal, idempotent).

Each follow-up looks like:

FieldTypeDescription
idstringThe follow-up id — pass it to ack.
handlestringThe lead's @username to send to.
platformstringThe platform to send on.
kindstringThe follow-up type (e.g. a rekindle).
messagestringThe text to send.
created_atISO-8601 UTCWhen 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 pending until 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 the comment text, plus recognized_lead and aware (see Person) and a thread_id.
  • POST /comments/reply → a reply within a thread. Returns a decision ("engage", "drive_to_dm", or "skip") and the reply text — which is null when 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 /events and POST /triggers require an external_event_id (unique per owner + platform). A re-delivered webhook with the same id applies exactly once; repeats return deduped: true and change nothing. Use your platform's native event id as the key.
  • POST /followups/{id}/ack is idempotent on the follow-up id — acking an already-delivered follow-up is a safe no-op.
  • POST /chat has 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 stable session_id to 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 /chat and /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

FluidTalk Characters API — part of the Fluidvip ecosystem.