Skip to content

Python SDK

The fluidtalk package is the official Python client for the FluidTalk Characters API. It wraps every endpoint your connector token can call, returns the inner data payload as light objects, and maps HTTP errors to typed exceptions.

This page is the method-by-method reference. For the concepts behind a character — sessions, people, bubbles, triggers, follow-ups, comment threads — see Concepts. For the underlying HTTP shapes, see the API reference. For how usage is metered, see Billing.

Install

bash
pip install "fluidtalk>=2"

The client requires Python 3.8+ and pulls in httpx as its only runtime dependency.

Construct a client

python
from fluidtalk import FluidTalk

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

Your token authenticates as one character. There is no account-level key and no character_id field — the token is the character. You name the platform in each call, and the same token works across every platform the character runs on. Get the token from the character's platform settings in the FluidTalk dashboard; it is shown once, so treat it like a password. See Authentication for details.

The connector owns platform I/O

The API generates the character's messages and tracks the relationship; it never posts anything itself. Reading and sending on Instagram, X, Reddit, Fanvue, TikTok, or anywhere else is your connector's job — how you connect to the platform is your concern.

Constructor parameters

ParameterTypeDefaultDescription
tokenstr— (required)Your per-character connector token (ftc_live_...). Sent automatically as the X-Connector-Token header.
base_urlstrhttps://api-talk.fluidvip.comAPI origin. The default is production; you should not need to change it.
timeoutfloat60.0Per-request timeout in seconds. Each generated turn runs an LLM, so the default is generous.
python
ft = FluidTalk(
    token="ftc_live_8f3c...",
    base_url="https://api-talk.fluidvip.com",
    timeout=60.0,
)

All requests target the versioned Characters API under /api/v1/characters, so the effective endpoint base is https://api-talk.fluidvip.com/api/v1/characters. You pass the bare origin; the client appends the /api/v1/characters prefix for you.

Lifecycle

The client opens a pooled HTTP connection. Close it when you are done, or use it as a context manager so it closes automatically:

python
with FluidTalk(token="ftc_live_8f3c...") as ft:
    reply = ft.chat(platform="instagram", handle="mark", message="hey ava")
# connection closed here

# or, explicitly:
ft = FluidTalk(token="ftc_live_8f3c...")
try:
    ...
finally:
    ft.close()

Methods

A constructed client exposes the character's actions directly, plus a ft.followups namespace for the follow-up pull-queue. Each method calls one endpoint and returns the inner data as a light object (see Return values).

MethodEndpointReturns
ft.chat(platform, handle, message="", image_url=None, session_id=None, own_username=None)POST /chatChatReply
ft.event(platform, handle, event_type="purchase", external_event_id=, amount=None, currency=None)POST /eventsEventResult
ft.trigger(platform, handle, event_id=, external_event_id=, context=None, own_username=None)POST /triggersTriggerResult
ft.followups.list(platform, own_username=None, limit=100)GET /followupslist[Followup]
ft.followups.ack(followup_id)POST /followups/{id}/ackAckResult
ft.comment(platform, post_ref, caption=None, image_urls=None, author_handle=None)POST /commentsCommentResult
ft.comment_reply(platform, post_ref, replier_handle=, reply_text="", parent_comment_ref=None)POST /comments/replyCommentReplyResult
ft.inbound_media(platform, data, filename=None, content_type=None)POST /inbound-mediaInboundMedia

ft.chat

The lead sent the character a DM — get the character's reply. The session for (platform, handle) is auto-created or resumed, so you rarely pass session_id.

python
reply = ft.chat(
    platform="instagram",
    handle="mark",
    message="hey ava, loved your latest post",
)

for bubble in reply.bubbles:
    # send bubble.text on the platform, pausing bubble.delay_ms before each
    send_dm("mark", bubble.text, image_url=bubble.image_url)

print("session:", reply.session_id)

The reply is a list of bubbles — the character may send several short chat messages with human-like delays. Send them in order. If the conversation is already closed, bubbles is [].

Parameters:

  • platform — the platform the DM arrived on (required).
  • handle — the lead's @username (required).
  • message — the text the lead sent.
  • image_url — an image the lead sent; it is described via vision and folded into the turn.
  • session_id — resume a specific conversation. Omit to auto-resume or create one for (platform, handle).
  • own_username — the bot account that received this DM, for multi-account dedup.

If dedup is on and another account already claimed this lead, the call returns ignored=True and you send nothing:

python
reply = ft.chat(platform="instagram", handle="mark", message="hi", own_username="ava.daily")
if reply.ignored:
    print("ignored:", reply.ignore_reason)   # "lead_claimed_by_other_account"

A generated turn may carry a billing object — see Billing. See the Chat reference.

ft.event

Report that money moved (purchase, refund, or chargeback). The API authoritatively flips the lead's lifecycle; your connector only reports the fact. external_event_id is the idempotency key — re-delivering the same event applies exactly once.

python
res = ft.event(
    platform="instagram",
    handle="mark",
    event_type="purchase",          # "purchase" | "refund" | "chargeback"
    external_event_id="stripe_evt_9f12",
    amount=24.0,
    currency="USD",
)
print(res.stage, res.sale_count, res.total_spend)
if res.deduped:
    print("already applied — not re-counted")

See the Events reference.

ft.trigger

Fire a conversation trigger / entry event — a story reaction, a new follower, a custom event. If the lead has no live chat, the character opens the conversation reacting to the event; if a chat already exists, the event becomes the next turn's stimulus. event_id is the entry-point key configured in the dashboard (an unknown one is a safe no-op); external_event_id is the idempotency key.

python
res = ft.trigger(
    platform="instagram",
    handle="mark",
    event_id="story_reaction",
    external_event_id="ig_story_react_55",
    context="🔥",                    # a short detail for the opener
)

if res.opened:
    for bubble in res.bubbles:
        send_dm("mark", bubble.text)

Parameters: platform, handle, event_id, external_event_id (all required), plus optional context (a short label/emoji) and own_username (dedup). See the Triggers reference.

ft.followups.list

Pull this connector's pending proactive follow-ups, oldest first. The character periodically queues "hey stranger" rekindles for dormant leads; you pull them, send each on the platform, then ack. This is a pull-queue.

python
for f in ft.followups.list(platform="instagram", limit=100):
    send_dm(f.handle, f.message)
    ft.followups.ack(f.id)

Parameters: platform (required for character-token callers), optional own_username (a claimed row is delivered only to that account), and limit (default 100, max 500). See the Follow-ups reference.

ft.followups.ack

Acknowledge that a pulled follow-up was delivered (pending → delivered). Terminal and idempotent.

python
res = ft.followups.ack("fu_7c2a1e")
print(res.delivered_at)

ft.comment

Generate a top-level public comment for a post. Your connector posts the returned text — the API does not.

python
res = ft.comment(
    platform="instagram",
    post_ref="https://instagram.com/p/Cxyz123",
    caption="sunset hike 🌄",
    image_urls=["https://cdn.example.com/post.jpg"],
    author_handle="mark",
)

if res.comment:
    post_comment("https://instagram.com/p/Cxyz123", res.comment)
    print("recognized lead:", res.recognized_lead)   # cross-surface awareness

post_ref (the platform's unique id/URL for the post) is required. When comments aren't enabled for this character/platform, comment is unset and the result is ignored. See the Comments reference.

ft.comment_reply

Generate a reply to a reply under the character's comment. Threaded — it can fire multiple times in a thread, to the original poster or to other commenters.

python
res = ft.comment_reply(
    platform="instagram",
    post_ref="https://instagram.com/p/Cxyz123",
    replier_handle="mark",
    reply_text="haha you're funny, where do you post more?",
)

if res.decision != "skip" and res.reply:
    post_reply("https://instagram.com/p/Cxyz123", res.reply)

The character decides whether to engage: decision is "engage", "drive_to_dm", or "skip", and reply is None when it chooses not to reply. Optional parent_comment_ref and post context can be supplied. See the Comments reference.

ft.inbound_media

Added in 2.1.0. Upload the bytes of a photo the lead sent, and get a permanent URL to pass to chat as image_url. For platforms that hand you a file rather than a link we can fetch — a Telegram file_id you download with your own bot token, or an Instagram CDN URL that is signed and expires.

python
up = ft.inbound_media(
    platform="instagram",
    data=raw_bytes,            # raw bytes; the base64 is done for you
    filename="photo.jpg",
)

ft.chat(platform="instagram", handle="mark", message="what do you think? 😏", image_url=up.url)

Max 10 MB decoded — larger raises ApiError (413). Skip this entirely when you already have a publicly-fetchable URL: pass it straight to chat as image_url. See the Media reference.

Return values

Methods return small objects that mirror the inner data of each API response (the { "data": ..., "request_id": ... } envelope is unwrapped for you). Timestamps are strings (ISO-8601 UTC). Each object ignores unknown fields, so a server that adds a field will not break an older SDK.

ChatReply

FieldTypeNotes
session_idstr | Nonethe conversation thread; None when an inbound was ignored by dedup
bubbleslist[Bubble]the reply, in send order; [] when the conversation is closed
ignoredboolTrue when multi-account dedup dropped this inbound
ignore_reasonstr | Nonee.g. "lead_claimed_by_other_account"
billingBilling | Nonepresent when billing is enabled

Bubble

FieldTypeNotes
textstrthe message to send
delay_msinthuman-like pause before sending this bubble
image_urlstr | Nonea photo to attach, if the character sent one

EventResult

FieldTypeNotes
okbool
dedupedboolTrue when this external_event_id was already applied
event_typestr"purchase" / "refund" / "chargeback"
stagestrthe lead's lifecycle stage after the event
sale_countint
total_spendfloat

TriggerResult

FieldTypeNotes
okbool
dedupedboolTrue on a re-delivered external_event_id
openedboolTrue when the character opened a cold conversation
session_idstr | None
bubbleslist[Bubble]the opener, when opened is True
ignoredstr | None"event not configured" when event_id is unknown
billingBilling | Nonepresent when an opener was generated and billing is enabled

Followup

FieldTypeNotes
idstrpass to followups.ack
handlestrthe dormant lead to re-engage
platformstr
kindstrthe follow-up category
messagestrthe rekindle text to send
created_atstrISO-8601 UTC

AckResult

FieldTypeNotes
okbool
dedupedboolTrue when the follow-up was already acked
delivered_atstrISO-8601 UTC

CommentResult

FieldTypeNotes
okbool
commentstr | Nonethe comment text to post; unset when comments aren't configured
awaredictcross-surface awareness summary
recognized_leadboolTrue when the commenter is a known Person
thread_idstr | None
billingBilling | Nonepresent when a comment was generated and billing is enabled

CommentReplyResult

FieldTypeNotes
okbool
decisionstr"engage" / "drive_to_dm" / "skip"
replystr | Nonethe reply text; None when the character chooses not to reply
reasonstr | Nonewhy the character made that decision
awaredict | Nonecross-surface awareness summary
billingBilling | Nonepresent when a reply was generated and billing is enabled

Billing

Attached to any generating response when billing is enabled. See Billing.

FieldTypeNotes
tokensfloatwhat this message cost you = real cost × plan rate (1 token = $1, e.g. 0.00127932)
tokens_usedfloatthe raw model cost of this turn, in tokens (real $ before plan rate)
balance_afterfloatwallet token balance after the charge
collectedboolwhether the charge was applied

Errors

The client raises a typed exception for every non-success status. Import them from the package:

python
from fluidtalk import (
    ApiError,              # base class for all of them
    AuthError,             # 401 — missing/invalid/revoked token
    PaymentRequiredError,  # 402 — wallet can't cover the turn
    PermissionError,       # 403 — forbidden
    NotFoundError,         # 404 — unknown character/lead/follow-up (or not yours)
    ConflictError,         # 409 — conflicting state
    ValidationError,       # 422 — a field failed validation
    RateLimitError,        # 429 — too many requests
)
ExceptionHTTP statuserror.codeWhen
AuthError401invalid_tokenMissing, invalid, or revoked connector token
PaymentRequiredError402payment_requiredBilling is on and the wallet can't cover this turn — raised before any model call
PermissionError403forbiddenThe action isn't allowed for this character/token
NotFoundError404not_foundThe resource doesn't exist, or belongs to another owner
ConflictError409conflictThe request conflicts with current state
ValidationError422validation_errorThe body is well-formed but a field failed validation
RateLimitError429rate_limitedToo many requests — honor Retry-After, then retry
ApiErrorany other (400, 500, …)invalid_request / internal_errorCatch-all base; also the parent of every class above

Every exception carries the HTTP status, the error.code, the human-readable error.message, and the request_id from the response body. Handle the specific ones you care about and let ApiError cover the rest:

python
from fluidtalk import PaymentRequiredError, RateLimitError, ApiError

try:
    reply = ft.chat(platform="instagram", handle="mark", message="hey ava")
except PaymentRequiredError:
    print("Top up the character's wallet — no turn was generated, no charge made.")
except RateLimitError as e:
    print("Rate limited — back off and retry.")
except ApiError as e:
    print(f"API error {e.status} ({e.code}): {e.message}  request_id={e.request_id}")

Because PaymentRequiredError is raised before the model runs, you are never charged for a refused turn — handle it by topping up the wallet and surfacing insufficient balance to the operator. See Errors, Billing, and Rate limits.

Full worked example

Drive an inbound DM, report a purchase, generate a public comment, and clear the follow-up queue — all for the character "Ava" on Instagram.

python
from fluidtalk import FluidTalk, PaymentRequiredError

with FluidTalk(token="ftc_live_8f3c...") as ft:
    # 1. A lead DM'd Ava — generate her reply (bubbles, in order).
    reply = ft.chat(platform="instagram", handle="mark", message="hey ava, love your posts")
    for bubble in reply.bubbles:
        send_dm("mark", bubble.text, image_url=bubble.image_url)
    print("session:", reply.session_id)

    # 2. Mark bought something — report it (idempotent on external_event_id).
    ev = ft.event(
        platform="instagram",
        handle="mark",
        event_type="purchase",
        external_event_id="stripe_evt_9f12",
        amount=24.0,
    )
    print("stage now:", ev.stage, "| sales:", ev.sale_count)

    # 3. Generate a public comment for one of Ava's posts.
    cm = ft.comment(
        platform="instagram",
        post_ref="https://instagram.com/p/Cxyz123",
        caption="sunset hike 🌄",
        author_handle="mark",
    )
    if cm.comment:
        post_comment("https://instagram.com/p/Cxyz123", cm.comment)

    # 4. Deliver any queued proactive follow-ups, then ack each.
    for f in ft.followups.list(platform="instagram"):
        send_dm(f.handle, f.message)
        ft.followups.ack(f.id)

Generating a client from the OpenAPI spec (optional)

If you prefer to generate your own typed client instead of using this SDK, the API publishes an OpenAPI specification. Any standard OpenAPI code generator can produce a client from it; remember to send your token in the X-Connector-Token header and to address the character by token (there is no character_id). For most integrations the fluidtalk package is the simplest path.

See also

FluidTalk Characters API — part of the Fluidvip ecosystem.