Appearance
Triggers & openers
A trigger is a platform event you report to a character so it can start or steer a conversation — a story reaction, a new follower, an outreach moment, or any custom event you wire up. The character decides what to do with it: when there's no live chat yet it opens the conversation reacting to the event; when a chat is already running the event becomes the stimulus for the character's next reply.
This is the right primitive whenever something happens on the platform that should kick off (or nudge) a DM but isn't itself a DM. Your connector owns the platform I/O — it sees the new follower, the story reply, the like — and forwards the fact to POST /triggers. FluidTalk generates the character's opener (or folds the event into the live conversation) and hands you back the bubbles to send. It never posts anything itself.
Firing a trigger authenticates with your character connector token (
X-Connector-Token: ftc_live_...). The token is the character; theplatformin the body selects which of that character's bound workflows handles the event. The same token works across every platform the character runs on. See Authentication.
The four entry points
Every trigger names an event_id — the entry-point key you configured in the dashboard. There are four kinds:
| Entry point | Typical event_id | Fires when | What the character does (cold) |
|---|---|---|---|
| Outreach | outreach | You decide to reach out first to a lead the character isn't talking to yet | Sends a cold opener in the character's voice |
| Story reaction | story_reaction | A lead reacts to / replies to the character's story | Opens reacting to the reaction (pass the emoji/label as context) |
| New follower | new_follower | A new account follows the character | Opens with a welcome in-voice |
| Custom | your own key | Any event you define (a like, a comment-to-DM bridge, a tip, a re-follow…) | Opens using the card you wired to that key |
The first three are conventional names; the actual keys are whatever you set in the dashboard. Custom keys let you map any platform signal you can observe onto a character behaviour without touching the API surface.
Configure entry points in the dashboard
Triggers are declared, not invented. Open the character at talk.fluidvip.com, pick the platform, and define each entry point — its event_id key, the opener card that runs, and any gating. The API only ever fires keys that exist; an event_id you didn't configure is a safe no-op (see Unknown event_id is fail-closed).
Brand templates. Instead of defining every entry point by hand, the dashboard's Marketplace → Brand templates tab has installable packs of pre-configured entry points (for example an Instagram Story pack with
story_reply/story_reaction/story_mention, or a New Follower pack). Installing a pack into a character declares those entry points on it — from then on theirevent_idkeys are just regular configured triggers you can fire through this endpoint. Nothing is fireable until the pack (or a manual entry point) is actually added to the character: the SDK and the API never ship ready-madeevent_ids of their own.
This split is deliberate: your connector can forward every platform event it sees and let the dashboard config decide which ones actually do something. You change behaviour by editing the character, not by redeploying the connector.
Cold-open vs warm-stimulus
The same call does two different things depending on whether a conversation already exists for (character, platform, handle):
- Cold — no live chat with this lead. The character opens the conversation reacting to the event. The response has
opened: true, asession_idfor the new conversation, andbubblesto send. This generates a turn (and is metered like any opener). - Warm — a chat already exists. The character does not barge in with a second opener. The event is folded into the live conversation as the stimulus for the next reply, so
openedisfalseandbubblesis typically empty. The lead's awareness of the event surfaces when the conversation next moves — i.e. on your nextPOST /chat— coloured by what just happened.
You don't choose the mode; you just report the event and the character resolves it against the lead's session. The same (platform, handle) always resolves to the same lead, so a warm chat is recognised automatically.
A story reaction during a dead chat opens a fresh DM; the same reaction mid-conversation just gives the character something to riff on next. One coherent persona, either way.
Fire a trigger
POST /triggers
Report an entry-point event for a lead.
Body
| Field | Type | Required | Description |
|---|---|---|---|
platform | string | Yes | The platform the event happened on, e.g. "instagram". Selects the character's bound workflow. |
handle | string | Yes | The lead's @username on that platform. The (platform, handle) pair identifies the lead. |
event_id | string | Yes | The configured entry-point key (e.g. "story_reaction", "new_follower", or your custom key). An unknown key is a safe no-op. |
external_event_id | string | Yes | Idempotency key for this event. Unique per owner + platform; a re-delivered event applies exactly once. |
context | string or object | No | A short detail that colours the opener — an emoji, a label, the story caption, etc. |
own_username | string | No | The bot account that observed the event, for multi-account dedup. |
raw | object | No | Connector echo for your own logging. Tokens are stripped before storage. |
Response
Every response is wrapped in the standard envelope — the real payload is under data, and the request_id is also returned as the X-Request-Id header.
A cold open returns the opener bubbles:
json
{
"data": {
"ok": true,
"deduped": false,
"opened": true,
"session_id": "sess_3f9a2b7c...",
"bubbles": [
{ "text": "omg you actually replied to my story 🙈", "delay_ms": 0, "image_url": null },
{ "text": "what made you react to that one?", "delay_ms": 2200, "image_url": null }
]
},
"request_id": "req_8c21d4..."
}A warm stimulus records the event without opening anything:
json
{
"data": {
"ok": true,
"deduped": false,
"opened": false,
"session_id": "sess_3f9a2b7c...",
"bubbles": []
},
"request_id": "req_a7740f..."
}bubbles is a list of short chat messages with human-like delays — send them in order, honouring each delay_ms. Each bubble is { text, delay_ms, image_url } (image_url is null unless the character sends a photo).
| Field | Type | Description |
|---|---|---|
ok | bool | Always true on a 2xx. |
deduped | bool | true if this external_event_id was already processed — nothing was re-applied. |
opened | bool | true if the character opened a new conversation (cold); false for a warm stimulus, a dedup, or an ignored event. |
session_id | string | The conversation this event resolved to. |
bubbles | array | The opener to send (cold). Empty for warm / deduped / ignored. |
Examples
bash
bash
curl -X POST https://api-talk.fluidvip.com/api/v1/characters/triggers \
-H "X-Connector-Token: ftc_live_..." \
-H "Content-Type: application/json" \
-d '{
"platform": "instagram",
"handle": "mark",
"event_id": "story_reaction",
"external_event_id": "ig_story_reply_88231",
"context": "🔥"
}'python (pip install fluidtalk)
python
from fluidtalk import FluidTalk
ft = FluidTalk(token="ftc_live_...")
res = ft.trigger(
platform="instagram",
handle="mark",
event_id="story_reaction",
external_event_id="ig_story_reply_88231",
context="🔥",
)
if res.opened:
for b in res.bubbles:
send_dm("mark", b.text) # your platform send; honour b.delay_ms between bubblestypescript (npm install fluidtalk)
ts
import { FluidTalk } from "fluidtalk";
const ft = new FluidTalk({ token: "ftc_live_..." });
const res = await ft.trigger({
platform: "instagram",
handle: "mark",
eventId: "story_reaction",
externalEventId: "ig_story_reply_88231",
context: "🔥",
});
if (res.opened) {
for (const b of res.bubbles) {
await sendDm("mark", b.text); // your platform send; honour b.delayMs between bubbles
}
}Idempotency (external_event_id)
external_event_id is the idempotency key for the event, unique per owner + platform. Platforms re-deliver webhooks; if the same event arrives twice, the second call is a no-op:
json
{ "data": { "ok": true, "deduped": true }, "request_id": "req_..." }A deduped: true response means the character did not open a second conversation and was not charged again. Use a stable id derived from the platform's own event identifier (the webhook delivery id, the story-reply id, the follow id) so retries collapse correctly. This is the same idempotency contract as POST /events.
Don't synthesise a fresh
external_event_idper attempt — that defeats dedup and can open duplicate conversations. Reuse the platform's stable id across retries.
Unknown event_id is fail-closed
Firing an event_id that isn't configured for this character/platform does nothing — no conversation, no charge, no error:
json
{ "data": { "ok": true, "ignored": "event not configured" }, "request_id": "req_..." }This is the fail-closed rule: an unconfigured key is a safe no-op rather than a 404 or an accidental opener. It lets you forward every platform signal you observe and let the dashboard config be the single source of truth for what actually fires. When you add a new entry point, you enable it by configuring the key — not by changing what your connector sends.
context — colour the opener
context is an optional short detail the character weaves into a cold opener. Pass the thing that makes this event specific:
- a story reaction → the emoji or the reply text (
"🔥","this fit is unreal") - a new follower → nothing, or a label like
"from reel: gym routine" - a custom event → a tag, a tip amount, the comment that bridged to DM
It can be a plain string or a small object. Keep it short — it's a hint for the opener, not a payload. It's ignored on a warm stimulus and on deduped/ignored calls.
Multi-account dedup
If the character runs several accounts on one platform, pass own_username — the bot account that observed the event — so only the account that claimed the lead ever opens. A trigger from a different account for an already-claimed lead is ignored. Default off (omit own_username for normal behaviour). See Multi-account dedup.
Billing
A cold open generates an opener turn, so it's metered like any generation — the exact upstream model spend at your plan's rate, charged to the character owner's wallet. If the wallet can't cover the opener, the call returns 402 payment_required before any model call, so you're never charged for a refused open. A warm stimulus, a deduped call, and an ignored (unconfigured) event don't call the model and aren't charged. Cold-open responses carry the standard billing object. See Billing.
Errors
| Status | error.code | Meaning | What to do |
|---|---|---|---|
400 | invalid_request | Malformed body or bad field shape | Fix the request. |
401 | invalid_token | Missing / invalid / revoked connector token | Check the X-Connector-Token header. See Authentication. |
402 | payment_required | Wallet can't cover a cold opener (charged before generation) | Top up in the dashboard; surface to the operator. See Billing. |
403 | forbidden | Token can't act on this platform/character | Verify the character's platform binding. |
422 | validation_error | A required field (platform, handle, event_id, external_event_id) is missing or invalid | Check field types and presence. |
429 | rate_limited | Too many requests | Honour Retry-After. See Rate limits. |
500 | internal_error | Server error (type: server_error) | Retry with backoff; correlate via request_id. |
An unconfigured event_id is not an error — it returns 200 with ignored: "event not configured". All errors use the shared shape { "error": { "code", "message", "type", "request_id" } }; the SDKs raise typed errors (AuthError, PaymentRequiredError, ValidationError, RateLimitError, …). See the full error model.
Related
- Triggers reference — every field on
POST /triggers. - Chat — where a warm stimulus surfaces, on the lead's next DM.
- Follow-ups — proactive re-engagement the character queues for dormant leads.
- Multi-account dedup —
own_usernameand the claim model. - Billing — how openers are metered and the
402contract. - Core concepts — characters, sessions, people, bubbles, entry points.
- Errors and Rate limits.
- SDKs: Python · TypeScript.