Skip to content

API Reference — Triggers

Fire a conversation trigger so a character reacts to something that happened on the platform — a story reaction, a new follower, or any custom entry event you configured. A trigger either opens a brand-new conversation (when the lead has no live chat) or feeds the event into an existing one as the next turn's stimulus.

All routes are under the production base URL:

https://api-talk.fluidvip.com/api/v1/characters

This endpoint generates the character's opener (or folds the event into a running chat) and returns the message bubbles for you to send. It never reads or posts on the platform itself — how you connect to the platform is your concern. To handle an inbound DM the lead sent you, see Chat; to report that money moved, see Events.

Authentication. Every request carries your per-character connector token in the X-Connector-Token header. The token is the character — there is no account key and no character_id; you name the platform in the body and the token selects that platform's bound workflow. See Authentication.


Entry points, cold vs warm

A trigger maps to an entry point you configured in the dashboard for this character and platform — keyed by event_id (for example outreach, story_reaction, new_follower, or a custom key). The trigger's effect depends on whether the lead (platform, handle) already has a live conversation:

  • Cold — no live chat exists. The character opens the conversation, reacting to the event. The response carries opened: true and the opener bubbles to send.
  • Warm — a chat already exists. The event becomes the next turn's stimulus: the character weaves it into the conversation rather than starting a new one. The response carries opened: false.

Fail-closed on unknown events

An event_id that isn't configured for this character/platform is a safe no-op — it never errors and never opens the wrong conversation. The call returns 200 with { "ok": true, "ignored": "event not configured" }, so a stray or misrouted webhook can't accidentally make the character speak.

Idempotency

Triggers are idempotent on external_event_id (unique per owner + platform). A re-delivered webhook applies exactly once; repeats return { "ok": true, "deduped": true } and do not re-open or re-stimulate the conversation. Always pass a stable external_event_id from your platform's event. See Concepts.


POST /triggers

Fire an entry event for a lead. Opens a cold conversation, or applies the event to a warm one.

Request body

NameTypeRequiredDescription
platformstringYesThe platform the event happened on, e.g. instagram. Selects the character's bound workflow.
handlestringYesThe lead's @username. Together with platform it identifies the lead (and resumes their conversation if one exists).
event_idstringYesThe configured entry-point key, e.g. story_reaction. An unknown key is a safe no-op (see fail-closed).
external_event_idstringYesYour idempotency key for this event (unique per owner + platform). A repeat is deduped.
contextstring | objectNoA short detail for the opener — e.g. the emoji a lead reacted with ("🔥") or a label describing the event.
own_usernamestringNoThe bot account that received this event, for multi-account dedup. Omit when dedup is off.
rawobjectNoConnector echo for your own logs. Stored as provided; any token-like values are stripped.

Response

Every response is wrapped in the standard envelope — the real payload is under data, and an X-Request-Id header mirrors request_id. The data shape depends on the outcome.

Cold start — the character opened the conversation:

json
{
  "data": {
    "ok": true,
    "deduped": false,
    "opened": true,
    "session_id": "ses_3f9a1c2d",
    "bubbles": [
      { "text": "omg thank you for the love on my story 🥹", "delay_ms": 0, "image_url": null },
      { "text": "how's your day going?", "delay_ms": 1400, "image_url": null }
    ]
  },
  "request_id": "req_8a2c4e7f"
}

Warm — the event was applied to an existing chat as the next turn's stimulus:

json
{
  "data": {
    "ok": true,
    "deduped": false,
    "opened": false,
    "session_id": "ses_3f9a1c2d",
    "bubbles": []
  },
  "request_id": "req_91b0d3aa"
}
FieldTypeDescription
okbooleanAlways true on a 2xx.
dedupedbooleantrue if this external_event_id was already processed — the event was not re-applied.
openedbooleantrue if the trigger opened a new conversation (cold); false if it was folded into an existing one (warm).
session_idstringThe conversation this trigger belongs to.
bubblesarrayThe opener message(s) to send, oldest first — present when the character opens. Each bubble: { text, delay_ms, image_url }. Send them in order, honoring delay_ms.

Other outcomes

The data object is shaped differently when the event is ignored:

  • Event not configured{ "ok": true, "ignored": "event not configured" }. Unknown/unconfigured event_id; send nothing.
  • Duplicate event{ "ok": true, "deduped": true }. This external_event_id was already handled; send nothing.
  • Dedup ignored{ "ok": true, "ignored": true }. With multi-account dedup on, the lead is claimed by a different account than the own_username you passed; send nothing.
  • Paused{ "ok": true, "paused": true, "pause_reason": "plan_limit" }. The character is paused (e.g. the account is over its plan's character limit after a downgrade); it won't open or advance a conversation until the plan is upgraded. Send nothing. See Paused characters.

Treat a truthy ignored (string or true) as "do nothing." Only act on bubbles when opened is true.

Examples

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_evt_88212",
    "context": "🔥"
  }'
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_evt_88212",
    context="🔥",
)

# Only send when the character opened the conversation (cold start)
if res.opened:
    for bubble in res.bubbles:
        send_dm("mark", bubble.text)  # your platform I/O
typescript
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_evt_88212",
  context: "🔥",
});

// Only send when the character opened the conversation (cold start)
if (res.opened) {
  for (const bubble of res.bubbles) {
    await sendDm("mark", bubble.text); // your platform I/O
  }
}

Errors

Errors use the standard envelope: { "error": { "code", "message", "type", "request_id" } }.

StatuscodeWhen
400invalid_requestMalformed body or a missing required field (platform, handle, event_id, external_event_id).
401invalid_tokenMissing, invalid, or revoked connector token.
402payment_requiredThe character owner's wallet can't cover a cold opener. Charged before any model call, so a refused trigger costs nothing — top up the wallet. See Billing.
403forbiddenThe token isn't permitted for this platform or action.
404not_foundThe addressed resource doesn't exist for this token.
409conflictThe request conflicts with the current state of the conversation.
422validation_errorThe body failed validation (e.g. a malformed field value).
429rate_limitedToo many requests; retry after the Retry-After header.
500internal_errorUnexpected server error; safe to retry the same external_event_id.

An unknown event_id is not an error — it returns 200 with ignored: "event not configured" (fail-closed).

The SDKs raise typed errors (AuthError, PaymentRequiredError, PermissionError, NotFoundError, ConflictError, ValidationError, RateLimitError, ApiError) — see Python and TypeScript. Full payload shapes are on the Errors page.


FluidTalk Characters API — part of the Fluidvip ecosystem.