Skip to content

Sending & receiving DMs

FluidTalk generates the character's messages — it never touches the platform. Your connector reads the inbound DM on Instagram (or X, Reddit, Fanvue, TikTok, anywhere), you POST /chat with the lead's message, and the API hands back the character's reply as a list of chat bubbles for you to send. How you connect to the platform is your concern; staying in character, remembering the lead, and deciding what to say next is ours.

This guide covers the full inbound loop for a character named Ava talking to a lead @mark on Instagram:

  • Receive → reply — your connector gets a DM, you POST /chat, you deliver the returned bubbles[] on the platform.
  • Resuming — how a conversation auto-resumes per (character, platform, handle), and the rare case where you pass session_id.
  • Images — handing the character a photo the lead sent (image_url), described via vision.
  • Memory — how the character stays one consistent, cross-surface-aware persona.
  • Dedup — what an ignored response looks like when a character runs several accounts.

For the exact field-by-field shapes see the Chat reference. For the wallet rules behind a 402, see Billing.

Just want it done? Use an SDK. The fluidtalk package (same name on PyPI and npm) collapses the call, the envelope unwrapping, and typed errors into a single chat(...) call. Jump to The SDK one-liner.


Before you start

  • Your token is the character. You authenticate with a per-character connector token in the X-Connector-Token header — ftc_live_.... There is no account-level key and no character_id field; you address Ava by her token and name the platform in every request body. Get the token from the character's platform settings on the dashboard; it's shown once, so treat it like a password.
  • One token, many platforms. A character can run on several platforms; the same token works for all of them. The platform you send selects the bound workflow.
  • A lead is (platform, handle). handle is the lead's @username. Re-using the same (platform, handle) always resumes that lead's conversation — you rarely think about sessions at all.
  • Every response is enveloped. A success is { "data": <result>, "request_id": "req_..." }; the reply you care about is always under data. The same id is on the X-Request-Id header — quote it in any support request.
  • Billing is gated up front. When billing is enabled and the owner's wallet can't cover the turn, /chat returns 402 payment_required before any model call, so you're never charged for a refused turn. See Billing.

The inbound-message loop

Receiving and replying is always the same three beats:

1. Lead DMs your bot on the platform   ->  your connector receives it
2. POST /chat (platform, handle, message)   ->  the character generates a reply
3. Deliver bubbles[] in order, pausing delay_ms   ->  you send them on the platform

The character generates; the connector delivers. FluidTalk does not post anything itself.

Send the inbound

POST /chat with the platform, the lead's handle, and what they said.

FieldTypeRequiredDescription
platformstringyesThe platform the DM arrived on, e.g. "instagram".
handlestringyesThe lead's @username, e.g. "mark".
messagestringWhat the lead said.
image_urlstringnoA photo the lead sent; the character "sees" it via vision. See Sending images.
session_idstringnoResume a specific conversation. Omit to auto-resume/create. See Resuming.
own_usernamestringnoThe bot account that received this DM, for multi-account dedup.
bash
curl -X POST https://api-talk.fluidvip.com/api/v1/characters/chat \
  -H "X-Connector-Token: ftc_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "platform": "instagram",
    "handle": "mark",
    "message": "hey, saw your story 😍"
  }'

The reply comes back as a list of bubbles — the character may send several short chat messages with human-like delays, not one wall of text:

json
{
  "data": {
    "session_id": "ses_3f9a...",
    "bubbles": [
      { "text": "heyy you 🙈", "delay_ms": 1200, "image_url": null },
      { "text": "that story was just me being bored at home lol", "delay_ms": 2600, "image_url": null }
    ]
  },
  "request_id": "req_7c2e..."
}

When billing is enabled, the response also carries a billing object (credits, spend_usd, balance_after, collected) — see Billing.

Deliver the bubbles

Send each bubble in order, on the platform. Each delay_ms is the suggested pause before that bubble — it's the character "typing", and honoring it is what makes the conversation read like a person rather than a bot dumping a paragraph. A bubble's image_url is null for plain text; when the character sends a photo it carries the URL of the image to attach.

Bubbles are the unit of a reply. Don't concatenate them into one message — send them as separate DMs, with the pauses, the way a person texts.


Resuming a conversation

You almost never manage sessions. A session is auto-created and auto-resumed per (character, platform, handle): the next /chat for @mark on Instagram lands in the same conversation Ava already has with him, with all of its history.

  • Omit session_id (the common case) and the API resolves the live conversation for that lead, or starts one.
  • Pass session_id (returned on every /chat) only when you need to pin a turn to one specific conversation thread — for example if a single handle could have parallel threads in your system.

The session_id in the response is stable for the life of that conversation; persist it if you want, but (platform, handle) alone is enough to keep a lead's chat coherent.


Sending images

When the lead sends a photo, pass its URL as image_url. The character "sees" it through vision and replies in context — there is no separate upload step and no field for raw bytes; you give the API a URL it can fetch.

bash
curl -X POST https://api-talk.fluidvip.com/api/v1/characters/chat \
  -H "X-Connector-Token: ftc_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "platform": "instagram",
    "handle": "mark",
    "message": "what do you think? 😏",
    "image_url": "https://your-cdn.example.com/inbound/mark-42.jpg"
  }'

The URL must be publicly fetchable. Host it wherever you like — your own CDN, the platform's media URL, or any link that resolves to the image bytes.

When you only have bytes

Some platforms never give you a link you can share: Telegram hands you a file_id you download with your own bot token, and Instagram's media URLs are signed and expire. For those, upload the bytes first and use the URL you get back — see POST /inbound-media.

bash
# 1. bytes in -> permanent url out (base64, max 10 MB)
curl -X POST https://api-talk.fluidvip.com/api/v1/characters/inbound-media \
  -H "X-Connector-Token: ftc_live_..." \
  -H "Content-Type: application/json" \
  -d '{"platform":"instagram","data_b64":"<base64>","filename":"photo.jpg"}'
# -> { "data": { "url": "https://...", "file_id": "..." }, "request_id": "req_..." }

# 2. pass that url to /chat as image_url (as above)

A URL we can't fetch isn't an error — the turn still goes through, the character just reacts without having seen the photo. So if an expiring link silently stops working, you get bland replies rather than a failure you can alert on. When in doubt, upload the bytes.


Staying in character & remembering

The whole point of a FluidTalk character is that it's one coherent persona, everywhere. You don't manage prompts, history, or memory — the character does:

  • It stays in voice across the entire conversation, turn after turn.
  • It remembers what @mark told it — facts, preferences, where the funnel left off.
  • The lead who DMs Ava and comments on her posts is the same Person. Ava is consistent and remembers across surfaces (DMs ↔ comments) and across multiple posts and threads.

You get all of this just by calling /chat with the same (platform, handle). For the model behind it, see Core concepts.


When an inbound is ignored (dedup)

A character can run several accounts on one platform (e.g. two Instagram accounts). With multi-account dedup enabled, the first account a lead messages claims that lead, and an inbound arriving on a different account is ignored so only one account ever replies.

To opt in, send own_username — the bot account that received the DM — on /chat. When the call is ignored, you get a clear signal and send nothing:

json
{
  "data": {
    "session_id": null,
    "bubbles": [],
    "ignored": true,
    "ignore_reason": "lead_claimed_by_other_account"
  },
  "request_id": "req_91af..."
}

Dedup is off by default — omit own_username and /chat behaves normally. See Multi-account dedup for the full "mother-slave" model.

Closed conversations. If the conversation is already closed, bubbles is [] (and ignored is absent). An empty bubbles list always means send nothing this turn.


The SDK one-liner

You rarely need to drive the envelope by hand. chat(...) makes the call, unwraps data, and raises typed errors for you.

Python (pip install fluidtalk):

python
from fluidtalk import FluidTalk

ft = FluidTalk(token="ftc_live_8f3c...")

reply = ft.chat(platform="instagram", handle="mark", message="hey, saw your story 😍")
for bubble in reply.bubbles:
    # deliver on the platform, pausing bubble.delay_ms first
    send_dm("mark", bubble.text, image_url=bubble.image_url)

# A photo the lead sent:
ft.chat(
    platform="instagram",
    handle="mark",
    message="what do you think? 😏",
    image_url="https://your-cdn.example.com/inbound/mark-42.jpg",
)

TypeScript (npm install fluidtalk):

typescript
import { FluidTalk } from "fluidtalk";

const ft = new FluidTalk({ token: "ftc_live_8f3c..." });

const reply = await ft.chat({
  platform: "instagram",
  handle: "mark",
  message: "hey, saw your story 😍",
});
for (const bubble of reply.bubbles) {
  // deliver on the platform, pausing bubble.delay_ms first
  await sendDm("mark", bubble.text, bubble.imageUrl);
}

// A photo the lead sent:
await ft.chat({
  platform: "instagram",
  handle: "mark",
  message: "what do you think? 😏",
  imageUrl: "https://your-cdn.example.com/inbound/mark-42.jpg",
});

When dedup ignores the inbound, the returned object carries ignored: true and an empty bubbles list — check it before you send.


Deliver each inbound at most once

Unlike /events and /triggers, /chat has no idempotency key — there's no message id to dedup on, so a re-sent inbound creates a new turn (and, with billing on, a new charge). Make sure your connector delivers each inbound DM to /chat at most once. If you might retry, pass a stable session_id so retries at least stay inside the same conversation rather than spawning parallel threads. See Core concepts → idempotency.

There is a backstop, but don't lean on it: if the same text arrives repeatedly on one conversation, only the first two are answered and the rest come back ignored: true with ignore_reason: "duplicate_message" (unbilled — no model call runs). Any different message resets it, so a real person who nudges twice is never cut off. Seeing duplicate_message in your logs is a signal that your connector is re-delivering DMs it has already handled. See Repeated messages are suppressed.


Errors you may hit

Statuserror.codeWhat to do
400invalid_requestFix the request body — missing platform/handle, malformed JSON.
401invalid_tokenCheck the X-Connector-Token header. See Authentication.
402payment_requiredThe owner's wallet can't cover the turn — top up in the dashboard. No model call ran. See Billing.
403forbiddenThe token isn't permitted for this platform/action.
404not_foundNo workflow is bound for this character + platform.
422validation_errorA field has the wrong type/shape.
429rate_limitedHonor the Retry-After header. See Rate limits.
500internal_errorTransient server error — retry with backoff, quoting the request_id.

4xx codes have type: "client_error"; 5xx are type: "server_error". The SDKs map these to typed errors — AuthError (401), PaymentRequiredError (402), PermissionError (403), NotFoundError (404), ValidationError (422), RateLimitError (429), and ApiError for the rest. See the full error model.


See also

  • Chat API reference — every field on /chat, request and response.
  • Triggers — start a conversation from a story reaction, new follower, or custom event.
  • Follow-ups — pull and deliver the character's proactive rekindles for dormant leads.
  • Multi-account dedup — run several accounts on one platform with one replying.
  • Billing — metered model spend, the wallet, and handling 402.
  • Core concepts — characters, people, sessions, bubbles, and cross-surface awareness.
  • SDKs: Python · TypeScript.

FluidTalk Characters API — part of the Fluidvip ecosystem.