Skip to content

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; the platform in 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 pointTypical event_idFires whenWhat the character does (cold)
OutreachoutreachYou decide to reach out first to a lead the character isn't talking to yetSends a cold opener in the character's voice
Story reactionstory_reactionA lead reacts to / replies to the character's storyOpens reacting to the reaction (pass the emoji/label as context)
New followernew_followerA new account follows the characterOpens with a welcome in-voice
Customyour own keyAny 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 their event_id keys 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-made event_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, a session_id for the new conversation, and bubbles to 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 opened is false and bubbles is typically empty. The lead's awareness of the event surfaces when the conversation next moves — i.e. on your next POST /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

FieldTypeRequiredDescription
platformstringYesThe platform the event happened on, e.g. "instagram". Selects the character's bound workflow.
handlestringYesThe lead's @username on that platform. The (platform, handle) pair identifies the lead.
event_idstringYesThe configured entry-point key (e.g. "story_reaction", "new_follower", or your custom key). An unknown key is a safe no-op.
external_event_idstringYesIdempotency key for this event. Unique per owner + platform; a re-delivered event applies exactly once.
contextstring or objectNoA short detail that colours the opener — an emoji, a label, the story caption, etc.
own_usernamestringNoThe bot account that observed the event, for multi-account dedup.
rawobjectNoConnector 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).

FieldTypeDescription
okboolAlways true on a 2xx.
dedupedbooltrue if this external_event_id was already processed — nothing was re-applied.
openedbooltrue if the character opened a new conversation (cold); false for a warm stimulus, a dedup, or an ignored event.
session_idstringThe conversation this event resolved to.
bubblesarrayThe 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 bubbles

typescript (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_id per 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

Statuserror.codeMeaningWhat to do
400invalid_requestMalformed body or bad field shapeFix the request.
401invalid_tokenMissing / invalid / revoked connector tokenCheck the X-Connector-Token header. See Authentication.
402payment_requiredWallet can't cover a cold opener (charged before generation)Top up in the dashboard; surface to the operator. See Billing.
403forbiddenToken can't act on this platform/characterVerify the character's platform binding.
422validation_errorA required field (platform, handle, event_id, external_event_id) is missing or invalidCheck field types and presence.
429rate_limitedToo many requestsHonour Retry-After. See Rate limits.
500internal_errorServer 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.


FluidTalk Characters API — part of the Fluidvip ecosystem.