Skip to content

Public comments & threaded replies

A FluidTalk character doesn't only run DMs — it can also speak in public, commenting on a post and then holding a threaded back-and-forth underneath that comment. This guide shows the two comment operations: writing a top-level comment on a post (POST /comments), and replying to the replies that land under it (POST /comments/reply), to the original poster or to other commenters, as many times as the thread keeps going.

As everywhere in the Characters API, you own the platform I/O: the API generates the character's comment text and tracks the relationship; your connector posts it. The API never posts anything itself.

All calls go to the production base URL https://api-talk.fluidvip.com/api/v1/characters and authenticate with your per-character connector token in the X-Connector-Token header (see authentication.md). The token is the character — there is no character_id; you name the platform in each request body, and the same token works for every platform that character runs on.

For the Person model behind cross-surface awareness (the same lead who DMs and comments is one persona), see concepts.md. For the exact field-by-field request/response shapes, see reference/comments.md and reference/comment-reply.md. Every response is wrapped in the standard { "data": …, "request_id": "req_…" } envelope and carries an X-Request-Id header — the shapes below show the inner data.


How a comment thread works

post                       a platform post (post_ref + caption/images/author)
└── character's comment     POST /comments  → the text you post under the post
    ├── reply (the OP)      POST /comments/reply → the character replies back
    └── reply (someone else) POST /comments/reply → the character replies, thread-aware
  • A comment is a single top-level comment the character writes for a post. You identify the post by post_ref (the platform's unique id or URL) and pass what context you have about it.
  • A thread is the character's comment plus the replies under it. Each incoming reply is a separate POST /comments/reply call. The character can reply more than once per thread, and to different commenters in the same thread — it stays consistent across the whole thread, and across other threads and DMs via the shared Person.

Generating a comment or a reply is a metered turn — see Billing. If the character owner's wallet can't cover it you get 402 before any model call, so you are never charged for a comment you didn't get.


Comment on a post

POST /comments generates a top-level comment for a post. Post the returned comment text yourself.

NameTypeRequiredDescription
platformstringYesThe platform the post is on (e.g. "instagram"). Selects the character's bound comments workflow.
post_refstringYesThe platform's unique id or URL for the post. Identifies the comment thread on later replies.
postobjectNoContext about the post the character is reacting to — see below.

The post object:

NameTypeRequiredDescription
post.captionstringNoThe post's caption text.
post.image_urlsstring[]NoImage URLs on the post; described via vision so the comment can react to what's actually shown.
post.author_handlestringNoThe @username of whoever published the post — used for cross-surface recognition.
bash
curl -X POST https://api-talk.fluidvip.com/api/v1/characters/comments \
  -H "X-Connector-Token: ftc_live_..." \
  -H "Content-Type: application/json" \
  -d '{
        "platform": "instagram",
        "post_ref": "https://instagram.com/p/Cxy12Ab34Cd/",
        "post": {
          "caption": "golden hour on the rooftop ✨",
          "image_urls": ["https://cdn.example.com/post-1.jpg"],
          "author_handle": "mark"
        }
      }'
python
from fluidtalk import FluidTalk

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

result = ft.comment(
    platform="instagram",
    post_ref="https://instagram.com/p/Cxy12Ab34Cd/",
    caption="golden hour on the rooftop ✨",
    image_urls=["https://cdn.example.com/post-1.jpg"],
    author_handle="mark",
)
print(result.comment)          # post this text on the platform
print(result.recognized_lead)  # True if the author is already a known Person
typescript
import { FluidTalk } from "fluidtalk";

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

const result = await ft.comment({
  platform: "instagram",
  postRef: "https://instagram.com/p/Cxy12Ab34Cd/",
  caption: "golden hour on the rooftop ✨",
  imageUrls: ["https://cdn.example.com/post-1.jpg"],
  authorHandle: "mark",
});
console.log(result.comment);         // post this text on the platform
console.log(result.recognizedLead);  // true if the author is already a known Person

The SDKs take the post fields flat (caption, image_urls/imageUrls, author_handle/authorHandle) and assemble the nested post object for you. Over raw HTTP, send them nested under post as shown in the curl example.

The response:

json
{
  "data": {
    "ok": true,
    "comment": "obsessed with this fit 😍 where'd you get the jacket?",
    "recognized_lead": true,
    "aware": { "...": "cross-surface awareness summary" },
    "thread_id": "cth_7b21…"
  },
  "request_id": "req_2c9f…"
}
  • comment is the text to post. recognized_lead is true when the post's author is a Person the character already knows. aware is a compact summary of what the character knows about them across surfaces (see below). thread_id is present once a thread exists, so you can correlate the replies that follow.

Reply in a thread

POST /comments/reply generates the character's reply to a reply under its comment. It can fire multiple times in one thread — to the original poster or to other commenters who join in.

NameTypeRequiredDescription
platformstringYesThe platform the thread is on.
post_refstringYesThe post whose thread this reply belongs to.
replier_handlestringYesThe @username of whoever replied.
reply_textstringNoWhat they said — the text the character is responding to.
parent_comment_refstringNoThe platform ref of the comment being replied to, to thread the reply correctly.
postobjectNoPost context (same shape as above), if the thread is new to the character.
bash
curl -X POST https://api-talk.fluidvip.com/api/v1/characters/comments/reply \
  -H "X-Connector-Token: ftc_live_..." \
  -H "Content-Type: application/json" \
  -d '{
        "platform": "instagram",
        "post_ref": "https://instagram.com/p/Cxy12Ab34Cd/",
        "replier_handle": "mark",
        "reply_text": "you look unreal here 😍",
        "parent_comment_ref": "ig_comment_17985…"
      }'
python
result = ft.comment_reply(
    platform="instagram",
    post_ref="https://instagram.com/p/Cxy12Ab34Cd/",
    replier_handle="mark",
    reply_text="you look unreal here 😍",
    parent_comment_ref="ig_comment_17985…",
)

if result.reply is not None:        # None when the character chooses to skip
    print(result.decision, result.reply)
typescript
const result = await ft.commentReply({
  platform: "instagram",
  postRef: "https://instagram.com/p/Cxy12Ab34Cd/",
  replierHandle: "mark",
  replyText: "you look unreal here 😍",
  parentCommentRef: "ig_comment_17985…",
});

if (result.reply !== null) {        // null when the character chooses to skip
  console.log(result.decision, result.reply);
}

The response carries the character's decision and, when it decides to speak, the reply text:

json
{
  "data": {
    "ok": true,
    "decision": "drive_to_dm_nudge",
    "reply": "aw you're sweet 🙈 come dm me, way easier to chat there",
    "aware": { "...": "cross-surface awareness summary" }
  },
  "request_id": "req_4f10…"
}

The decision tells you what to do:

decisionMeaning
comment_replyReply in-thread. reply holds the text to post under the thread.
drive_to_dm_nudgeReply, but nudge the conversation toward DMs. Post reply, then continue in DMs via POST /chat.
skipThe character chose not to reply. reply is null — post nothing.
bow_outThe replier turned hostile or called the character a bot. reply is null, and the thread is marked BOWED_OUT so it stays quiet.

When reply is null, do not post anything. On a skip or bow_out the reason names the rule that fired — see the settings below, which is where each of those reasons comes from. aware is the same cross-surface summary returned by POST /comments.

Branch on decision, not on the presence of reply: the two nudge decisions both carry text and mean different things downstream.


What the comment settings control

Every decision you get back is the product of the character's comment settings for that platform. They live on the platform's workflow — the Comments editor in the dashboard, next to the DM flow — and they are the reason two identical calls can behave differently on two platforms. You don't send any of them in a request; they are configuration, and this table is here so an unexpected skip is diagnosable rather than mysterious.

SettingDefaultWhat you observe
EnableonOff makes both endpoints answer 200 with ignore_reason: "comments_disabled" and generate nothing. See below.
Modefull_threadcomment_once posts the top-level comment and never replies: every /comments/reply returns skip · mode=comment_once. The inbound reply is still recorded.
Reply to strangersonOff means only the post's author gets replies; anyone else returns skip · reply_to_strangers=off.
Bow out on hostileonA bot accusation or insult returns bow_out · hostile/bot-accusation and marks the thread BOWED_OUT.
Max replies per thread6Once the character has replied this many times under one post: skip · max_replies_per_thread.
Max replies per author3Per person, within one thread: skip · max_replies_per_author.
Max replies per author (total)0 = unlimitedPer person across every thread: skip · max_replies_per_author_total.
Drive to DMonEnables the drive_to_dm_nudge decision at all.
Drive to DM after warmth35How warm the public thread must get before that nudge fires. This is the thread's own warmth, not DM rapport.
Nudge once per persononThe nudge fires once per Person in total, not once under every post they comment on.
Graduate strangers to leadsonA warmed commenter becomes a real Person, so your POST /chat calls for them arrive with history.
Allow links in commentsoffLeave it off. On, the character may post a URL publicly — the single biggest shadowban trigger on every platform this runs on. The funnel moves people through the DM.
Comment / reply / drive prompts, style notebuilt-inThe character's intent and extra style guidance for each kind of comment. Changes what it writes, never whether it writes.

A skip or bow_out is not an error and not a wasted charge: every stop rule above is a deterministic check that runs before any model call, so the character decided to stay quiet without generating anything.


Cross-surface awareness

This is the headline of the Characters API: one coherent persona everywhere. A lead is identified by (platform, handle), and the same lead who comments on a post and who slides into the DMs resolves to a single Person. The character remembers across surfaces — DMs ↔ comments — and across multiple posts and threads.

  • recognized_lead (on POST /comments) is true when the post's author_handle is already a known Person.
  • aware (on both endpoints) is a compact summary of what the character already knows about that Person — earlier DMs, prior comments, other threads — so a public reply lands consistently with whatever was said in private, and vice-versa.

You don't wire any of this up. As long as you pass the real author_handle / replier_handle, awareness is resolved for you. For the full model, see Core concepts.


Driving a comment into a DM

When a thread is warming up, POST /comments/reply may return decision: "drive_to_dm_nudge". Post the reply text as usual — it gently steers the replier toward the DMs. When that person then messages the character, feed the inbound DM to POST /chat with the same (platform, handle). Because it's the same Person, the character picks the conversation up with full context from the public thread — no handoff state to manage on your side.


When comments are switched off

Comments are configured per character and per platform, and they are ON by default — a character with a bound workflow you have never opened the Comments settings on will comment. The setting exists so you can opt a platform out.

When someone has done that, every call you make here returns a 200 success envelope whose data is a no-op marker rather than a comment.

If your comment calls seem to do nothing, this is the first thing to check.

json
{
  "data": {
    "ok": true,
    "ignored": "comments not configured",
    "ignore_reason": "comments_disabled",
    "comment": null,
    "explain": "Comments are switched OFF for workflow 'IG Flow' (character 'Ava', platform 'instagram') — comments are on by default, so this workflow has been set that way deliberately. Turn Comments back on in the workflow's settings; nothing you send to this endpoint will generate anything until you do."
  },
  "request_id": "req_5a08…"
}

This is not an error — there is no comment / reply field, so simply post nothing. It lets you call the comment endpoints unconditionally from your connector and let the dashboard decide whether the character speaks publicly on that platform.

Branch on ignore_reason, not on the explain prose. To check the flag before you write any code, call GET /self-test — it reports comments_enabled per binding.


Errors you may hit here

Statuserror.codeMeaning in this context
400invalid_requestMalformed body — e.g. missing post_ref or replier_handle, or invalid JSON.
401invalid_tokenMissing, invalid, or revoked connector token.
402payment_requiredThe character owner's wallet can't cover the generation. Returned before any model call — top up in the dashboard and retry.
403forbiddenThe token isn't allowed to act on this platform/surface.
404not_foundThe thread or referenced comment isn't yours, or doesn't exist.
422validation_errorA field failed validation (e.g. image_urls not a list of strings).
429rate_limitedToo many requests — honor the Retry-After header.
500internal_errorSomething broke on our side; retry with backoff.

Note that comments not configured is a 200 no-op (above), not a 404. The SDKs raise typed errors — PaymentRequiredError for 402, NotFoundError for 404, ValidationError for 422, and so on. See errors.md for the full error model and rate-limits.md for per-token limits.


FluidTalk Characters API — part of the Fluidvip ecosystem.