Agent API

Overview

The Flutch External API allows you to integrate AI agents into your applications programmatically. Use this API to send messages to agents and receive responses either synchronously or via streaming.

Base URL

https://api.flutch.ai/api/v1

Authentication

All API requests require an API key passed in the X-API-Key header.

Getting Your API Key

  1. Log in to your Flutch admin panel
  2. Navigate to /tokens (hidden page, accessible by direct URL)
  3. Click "Generate API Key"
  4. Important: Copy and save the key immediately - it will only be shown once!

API Key Format

flutch_<64 hexadecimal characters>

Example:

flutch_f6f1a55e248f04eb6896c2d77e5060ba4d192951db74aa21045e23b7ac39622e

Security Best Practices

  • Never commit API keys to version control
  • Store keys securely using environment variables or secrets management
  • Rotate keys periodically
  • Revoke compromised keys immediately via the admin panel

Identifying the user

Every request names the end user in one of two ways:

  • externalUserId: your own id for the user (your database id, a UUID, whatever you have; up to 255 characters). Flutch creates the user the first time it sees the id and recognizes it afterwards, so there is nothing to register or store on your side. Ids are scoped to your company: your 42 is never another company's 42.
  • userId: the Flutch id of an existing user, as returned in the userId field of messages.

Send exactly one of them (both is 400). They go in the JSON body of invoke and stream; for requests without a JSON body (GET, PATCH, and the multipart uploads) and for cancel, put them in the query string: ?externalUserId=customer-42.

API keys are secrets. The key identifies your whole company. Never ship it to a browser or mobile app: call the API from your own backend and let your frontend talk to that backend.

Endpoints

MethodPathPurpose
POST/agents/{agentId}/invokeSend a message, get the complete reply as JSON
POST/agents/{agentId}/streamSend a message, get the reply as a Server-Sent Events stream
POST/agents/{agentId}/cancelCancel a running streamed generation
GET/agents/{agentId}/threadsList the user's threads with this agent
GET/agents/{agentId}/threads/{threadId}/messagesMessage history of a thread
PATCH/agents/{agentId}/messages/{messageId}/reactionsLike / dislike a message
POST/agents/{agentId}/uploadsUpload a file to attach to a message

All endpoints require the X-API-Key header. Endpoints with a JSON body also require Content-Type: application/json.

A user only ever sees their own threads and messages. List endpoints return at most 50 items per page.

1. Invoke Agent (Synchronous)

Send a message to an agent and receive the complete response.

POST /agents/{agentId}/invoke

Request body

{
  "externalUserId": "customer-42",
  "content": {
    "text": "Hello, what can you help me with?",
    "attachments": [
      { "type": "image", "value": [{ "url": "https://example.com/photo.jpg" }] }
    ],
    "metadata": { "orderNumber": "A-1042" }
  },
  "threadId": "thread-456",
  "metadata": {
    "utm": { "source": "newsletter", "campaign": "spring" },
    "currentPage": "https://example.com/pricing"
  }
}
FieldTypeRequiredDescription
externalUserIdstringOne of the twoYour own id for the user. Created on first use. See Identifying the user.
userIdstringOne of the twoFlutch id of an existing user of your company.
contentobjectYesThe message. See Message content.
threadIdstringNoContinue an existing conversation. Omit to create a new thread. The user must have access to the thread, otherwise 403.
metadataobjectNoRequest context (UTM, current page, screen resolution, …). Merged over the values detected from HTTP headers (IP, country, device, browser, language). Used for lead qualification and analytics. Fields are listed in Request metadata.

Response

201 Created, the agent's message object.

{
  "id": "694d21839750ec2392a293c2",
  "threadId": "694d21829750ec2392a293b7",
  "userId": "67b3447044b9e7a1c9b71756",
  "agentId": "67b88f7ec6092d7bc711a934",
  "role": "assistant",
  "platform": "api",
  "reason": "agent_reply",
  "content": {
    "text": "I can help you with various tasks...",
    "contentChains": [
      {
        "channel": "text",
        "steps": [{ "index": 0, "type": "text", "text": "I can help you with various tasks..." }],
        "isComplete": true
      }
    ],
    "attachments": [],
    "metadata": {}
  },
  "status": "delivered",
  "replyToId": "694d21829750ec2392a293b9",
  "createdAt": "2025-12-25T11:35:31.498Z",
  "updatedAt": "2025-12-25T11:35:31.498Z"
}

Example

curl -X POST https://api.flutch.ai/api/v1/agents/67b88f7ec6092d7bc711a934/invoke \
  -H "Content-Type: application/json" \
  -H "X-API-Key: flutch_f6f1a55e248f04eb6896c2d77e5060ba..." \
  -d '{
    "externalUserId": "customer-42",
    "content": {
      "text": "Hello, what can you help me with?"
    }
  }'

2. Stream Agent Response (Server-Sent Events)

Send a message to an agent and receive the response in real-time via Server-Sent Events (SSE).

POST /agents/{agentId}/stream

Request body: same as invoke.

Response format

200, Content-Type: text/event-stream. Each event is event: <name> + data: <payload> followed by a blank line. Lines starting with : are comments (heartbeat) and must be ignored.

Do not use the browser EventSource. It only supports GET and cannot send headers, so it cannot call this endpoint. Read the response body with fetch (see the example below) or use an SSE client that supports POST.

Event order

thread_created → partial* → [thread_renamed] → [escalated] → final
                                   (error can replace any of them and ends the stream)

Event: thread_created

Always the first event. The thread was created or loaded.

event: thread_created
data: {"threadId":"694d21829750ec2392a293b7","requestId":"8d1f6c0e-5a52-4b6f-9a5e-0c1f2d3e4a5b"}
FieldDescription
threadIdThread ID. Use it as threadId in the next request.
requestIdID of this generation. Pass it to /cancel to stop it.

Event: partial

Sent for each chunk while the answer is being generated. The payload is JSON, not plain text.

event: partial
data: {"channel":"text","delta":{"type":"text_chunk","text":"Hello"}}

event: partial
data: {"channel":"text","delta":{"type":"text_chunk","text":" world"}}

channel is text, processing or tools. delta.type tells what changed:

delta.typeFieldsMeaning
text_chunktextNext piece of text. Append it to the current text step.
step_startedstep (a step, e.g. type: "tool_use" with name, id)A new block started, usually a tool call.
tool_input_chunkstepId, chunkNext piece of the tool's input JSON.
tool_output_chunkstepId, chunkThe tool's result.
output_chunkstepId, chunkOutput of a processing step.
chain_completed—The chain of this channel is finished.

To show a live answer, concatenate delta.text of text_chunk events where channel is text. Tool events (step_started with tool_use, tool_input_chunk, tool_output_chunk) are not sent if the agent hides tool usage. The final text is always in the final event.

Event: thread_renamed

Sent if the thread title was generated or changed.

event: thread_renamed
data: {"title":"Conversation about AI"}

Event: escalated

The conversation was handed over to a human manager instead of a normal AI answer. Sent before final.

event: escalated
data: {"reason":"insufficient_funds","hasStaticReply":true}
reasonMeaning
insufficient_fundsThe company balance is not enough.
no_lead_creditsNo lead credits left.
generation_errorThe agent failed to generate an answer.
thread_already_escalatedThe thread was already handed to a manager earlier.

hasStaticReply: true means a ready-made reply was sent as the assistant message in final. Show a "manager will join" hint when it is false.

Event: final

Last event on success. Contains the complete message object.

event: final
data: {"id":"694d21839750ec2392a293c2","threadId":"…","role":"assistant","content":{"text":"Hello world!","contentChains":[…],"attachments":[…]},…}

Always take the result (text, attachments, buttons) from final: attachments are delivered only here. The one exception is a generation stopped before any text came out (see Cancel): the stream then ends without final.

Event: error

Sent if processing fails. The stream ends right after.

event: error
data: {"error":"Error message","code":"ERROR_CODE"}

Heartbeat

Every 30 seconds the server writes a comment line to keep the connection open:

:heartbeat

SSE parsers ignore comment lines. If you parse the stream by hand, skip blocks that start with :.

Disconnects

Closing the connection does not stop the reply. If your user closes the page or loses the network, the agent finishes the reply and saves it in the thread; the next history request returns it in full. To actually stop a reply, call /cancel with the requestId.

JavaScript example (fetch)

const res = await fetch(
  "https://api.flutch.ai/api/v1/agents/67b88f7ec6092d7bc711a934/stream",
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-API-Key": process.env.FLUTCH_API_KEY,
    },
    body: JSON.stringify({
      externalUserId: "customer-42",
      content: { text: "Tell me a story" },
    }),
  }
);

if (!res.ok) throw new Error(`HTTP ${res.status}`);

const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let text = "";
let threadId, requestId;

function handle(event, data) {
  switch (event) {
    case "thread_created":
      ({ threadId, requestId } = JSON.parse(data));
      break;
    case "partial": {
      const { channel, delta } = JSON.parse(data);
      if (channel === "text" && delta.type === "text_chunk") {
        text += delta.text;
        console.log(text); // update UI
      }
      break;
    }
    case "thread_renamed":
      console.log("Title:", JSON.parse(data).title);
      break;
    case "escalated":
      console.log("Handed to a manager:", JSON.parse(data).reason);
      break;
    case "final":
      console.log("Message:", JSON.parse(data));
      break;
    case "error":
      console.error("Error:", JSON.parse(data));
      break;
  }
}

while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  buffer += decoder.decode(value, { stream: true });

  const blocks = buffer.split("\n\n");
  buffer = blocks.pop(); // incomplete tail

  for (const block of blocks) {
    if (!block.trim() || block.startsWith(":")) continue; // heartbeat
    let event = "message";
    const dataLines = [];
    for (const line of block.split("\n")) {
      if (line.startsWith("event:")) event = line.slice(6).trim();
      else if (line.startsWith("data:")) dataLines.push(line.slice(5).trimStart());
    }
    handle(event, dataLines.join("\n"));
  }
}

Python example (requests)

import json
import os
import requests

BASE_URL = "https://api.flutch.ai/api/v1"
HEADERS = {
    "Content-Type": "application/json",
    "X-API-Key": os.environ["FLUTCH_API_KEY"],
}

def iter_sse(response):
    """Yield (event, data) pairs from an SSE response, skipping heartbeats."""
    event, data_lines = "message", []
    for raw in response.iter_lines(decode_unicode=True):
        if raw is None:
            continue
        if raw == "":  # blank line = end of event
            if data_lines:
                yield event, "\n".join(data_lines)
            event, data_lines = "message", []
        elif raw.startswith(":"):  # comment / heartbeat
            continue
        elif raw.startswith("event:"):
            event = raw[6:].strip()
        elif raw.startswith("data:"):
            data_lines.append(raw[5:].lstrip())

def stream_message(agent_id, user_id, text, thread_id=None):
    payload = {"externalUserId": user_id, "content": {"text": text}}
    if thread_id:
        payload["threadId"] = thread_id

    answer, final, ids = "", None, {}
    with requests.post(
        f"{BASE_URL}/agents/{agent_id}/stream",
        headers=HEADERS,
        json=payload,
        stream=True,
        timeout=(10, 120),
    ) as response:
        response.raise_for_status()
        for event, data in iter_sse(response):
            if event == "thread_created":
                ids = json.loads(data)  # {"threadId": ..., "requestId": ...}
            elif event == "partial":
                chunk = json.loads(data)
                delta = chunk["delta"]
                if chunk["channel"] == "text" and delta["type"] == "text_chunk":
                    answer += delta["text"]
                    print(delta["text"], end="", flush=True)
            elif event == "escalated":
                print("\n[handed to a manager:", json.loads(data)["reason"], "]")
            elif event == "final":
                final = json.loads(data)
            elif event == "error":
                raise RuntimeError(json.loads(data))
    return ids.get("threadId"), final

thread_id, message = stream_message(
    "67b88f7ec6092d7bc711a934", "customer-42", "Tell me a story"
)
print("\nThread:", thread_id)
print("Attachments:", message["content"].get("attachments", []))

cURL example

curl -N -X POST https://api.flutch.ai/api/v1/agents/67b88f7ec6092d7bc711a934/stream \
  -H "Content-Type: application/json" \
  -H "X-API-Key: flutch_f6f1a55e248f04eb6896c2d77e5060ba..." \
  -d '{
    "externalUserId": "customer-42",
    "content": {
      "text": "Tell me a story"
    }
  }'

3. Cancel Generation

Stop a streamed generation that is still running. Call it when your user presses a "Stop" button. Closing the stream connection is not a stop: the reply keeps generating and is saved (see Disconnects).

POST /agents/{agentId}/cancel
{ "requestId": "8d1f6c0e-5a52-4b6f-9a5e-0c1f2d3e4a5b" }

requestId comes from the thread_created event. Only the user who started the request can cancel it.

Response (201 Created):

{ "success": true }

success is false if the request already finished or does not belong to the caller.

What happens to the conversation. Stopping a reply is not an error and never hands the thread to a manager.

  • The text generated so far is saved as the agent's reply, marked content.metadata.cancelled: true. It appears in the thread history like any other reply.
  • If the stream is still open, it ends with a final event carrying that partial message.
  • If nothing had been generated yet, no reply is saved and the stream ends without a final event.
  • Your next message in the same thread is answered normally.

Python Example:

requests.post(
    f"{BASE_URL}/agents/{agent_id}/cancel",
    headers=HEADERS,
    json={"requestId": ids["requestId"]},
).json()  # {"success": True}

4. List threads

GET /agents/{agentId}/threads?externalUserId=customer-42&page=1&limit=20

Threads of this user with this agent, most recently updated first. Archived threads are not listed.

Response (200):

{
  "items": [
    {
      "id": "694d21829750ec2392a293b7",
      "title": "Conversation about AI",
      "state": "current",
      "messageCount": 6,
      "createdAt": "2025-12-25T11:35:30.000Z",
      "updatedAt": "2025-12-25T11:40:02.000Z"
    }
  ],
  "page": 1,
  "limit": 20,
  "total": 1,
  "totalPages": 1
}

title is null until the thread has been named (see the thread_renamed stream event).

5. Thread history

GET /agents/{agentId}/threads/{threadId}/messages?externalUserId=customer-42&page=1&limit=20

Messages of one thread, newest first: page 1 is the latest. To render a conversation top to bottom, reverse each page and request the next page to scroll back in time. Items are message objects for both the user and the agent; user messages carry role: "user".

Response (200): same envelope as List threads, with message objects in items. A message may also have reactions: { "like": 1 | 2 }.

Errors: 403 if the thread belongs to another user or agent, 404 if it does not exist.

6. Reactions

PATCH /agents/{agentId}/messages/{messageId}/reactions?externalUserId=customer-42
{ "like": 1 }
likeMeaning
1Like
2Dislike
nullRemove the reaction

Response (200):

{ "messageId": "694d21839750ec2392a293c2", "reactions": { "like": 1 } }

404 if the message does not exist or is not in one of this user's threads with this agent.

7. Upload a file

POST /agents/{agentId}/uploads?externalUserId=customer-42
Content-Type: multipart/form-data

One form field, file. Allowed types: audio/*, video/*, image/*, PDF, DOC, DOCX; anything else is 400.

Response (201):

{ "url": "https://…" }

Use the returned url in an attachment of the next message, for example { "type": "image", "value": [{ "url": "…" }] } or { "type": "file", "value": { "url": "…", "fileName": "contract.pdf" } }.

curl -X POST "https://api.flutch.ai/api/v1/agents/$AGENT_ID/uploads?externalUserId=customer-42" \
  -H "X-API-Key: $FLUTCH_API_KEY" \
  -F "file=@photo.jpg"

Data reference

Objects and fields shared by the endpoints above.

Message content

content is a message object. At least one of text or attachments should be present.

FieldTypeDescription
textstringThe message text.
attachmentsarrayFiles and structured items sent with the message. See Attachments. Audio and voice attachments are transcribed automatically before the agent sees them.
metadataobjectFree-form key/value data stored with the message and available to the agent graph.

Other content fields (contentChains, reasoningChains, currentToolCall, hasReasoningProcess) are produced by the platform in responses. Do not send them.

Callback tokens. If content.text is a durable callback token (starts with cb1., taken from a button in an earlier reply), the message is handled as a button press: it re-enters the automation (funnel) that produced the button instead of being treated as ordinary user text.

Attachments

{ "type": "image", "value": [{ "url": "https://…", "alt": "…" }], "metadata": {} }
FieldTypeDescription
typestringOne of the attachment types below.
valueobject / arrayPayload; its shape depends on type.
metadataobjectOptional, free-form.

Attachment types you can send in a user message:

typevalue
image[{ "url", "alt?", "caption?" }]
video{ "url", "width?", "height?", "duration?", "mimeType?", "thumbnailUrl?" }
voice, audio{ "url", "mimeType?", "fileSize?" } — transcribed to text
file{ "url", "fileName?", "mimeType?", "fileSize?" }

The agent can return more types in its reply (button, suggestion, citation, card, chart, gallery, product_link, checkout_link, payment_link, order_link, recipe_card, lead_form, telegram_deeplink, max_deeplink, webapp, source). See Reply attachments.

Request metadata

All fields are optional. Detected automatically from the request: ip, country, city, language, device (desktop | mobile | tablet | unknown), os, browser. Anything you send in metadata overrides the detected value. Typical client-supplied fields: utm (source, medium, campaign, term, content), currentPage, referrer, screenResolution.

Message object

FieldTypeDescription
idstringMessage ID.
threadIdstringThread the message belongs to. Save it to continue the conversation.
userIdstringThe user the thread belongs to.
agentIdstringThe agent that replied.
rolestringassistant for replies.
platformstringapi.
reasonstringWhy the message was produced. agent_reply for a normal reply; manual_reply when a human manager answered; system_error / system_notification for system messages.
statusstringDelivery status, e.g. delivered.
replyToIdstringID of the user message this answers.
content.textstringFinal reply text. Convenience copy of the text channel.
content.contentChainsarrayStructured reply, one chain per channel. See Content chains.
content.attachmentsarrayButtons, cards, links, etc. See Reply attachments.
content.metadataobjectExtra data attached by the agent.
content.reasoningChainsarrayDeprecated. Legacy copy of contentChains.
createdAt, updatedAtstringISO timestamps.

Content chains

Everything the agent produced is split into channels:

channelMeaning
textThe answer shown to the user.
processingIntermediate work (reasoning, tool calls).
toolsTool activity.

Each chain is { "channel", "steps": [...], "isComplete": boolean }. A step has:

FieldTypeDescription
indexnumberPosition inside the chain.
typestringtext or tool_use.
textstringText of a text step.
name, idstringTool name and call ID (tool_use).
inputstringTool parameters as a JSON string.
outputstringTool result (long outputs are truncated).
metadataobjectOptional.

If the agent has tool visibility turned off (hideToolUsage), tool_use steps are removed from both the stream and the saved message.

Reply attachments

Same { type, value, metadata? } shape as in requests. Common reply types:

typeMeaningvalue
buttonButtons under the message{ "text", "type": "url" | "reply" | "callback", "value", "row?" }. url: value is the link; reply: value is text to send back as the user's next message; callback: value is a token to send back as content.text.
suggestionSuggested follow-up{ "text", "description?" }
citationSource reference{ "source": { "url", "title", "type": "webpage" | "pdf" | "article", "articleId?", "knowledgeBaseId?" } }
cardInteractive card{ "title", "description?", "fields?": [{ "label", "value", "type?" }], "buttons?" }
chartChart{ "type": "line" | "bar" | "pie" | "area", "title", "datasets": [...] }
galleryCarousel of cardslist of cards
image, video, file, audio, voiceMediaas in Attachments
product_link, checkout_link, payment_linkProduct / checkout / payment button{ "articleId", "kmsId", "text?", "showPrice?", "utm?" }
order_linkLink to an order page{ "orderId", "page?", "text?" }
recipe_cardCard for a single article with action buttons{ "articleId", "kmsId", "title?", "subtitle?", "imageUrl?", "actions": [...] }
lead_formIn-chat contact form{ "title?", "text?", "fields?", "requiredFields?", "submitText?", "successText?" }
telegram_deeplink, max_deeplinkLink into a messenger botbot link with a signed code

Unknown attachment types should be ignored by your client, new ones can appear.

Error Responses

Errors share one shape. The details are in error; error.message can be a list of strings for request validation errors.

{
  "statusCode": 401,
  "timestamp": "2026-10-20T10:43:38.488Z",
  "path": "/api/v1/agents/67b88f7ec6092d7bc711a934/invoke",
  "error": {
    "message": "Invalid API key",
    "error": "Unauthorized",
    "statusCode": 401
  }
}
StatusWhen
400Invalid request: both userId and externalUserId, neither of them, an externalUserId over 255 characters or with control characters, an unknown userId, an unsupported upload type.
401Missing or invalid API key.
403The user has no access: a userId from another company, a thread or message that belongs to someone else, or an agent the user may not use.
404The agent, thread or message does not exist (or, for reactions, is not yours).
500Server error.

Best Practices

Thread Management

Creating Conversations:

  • Omit threadId in the first request to create a new conversation thread
  • Save the threadId from the response
  • Include the same threadId in subsequent requests to continue the conversation

Example Flow:

// First message - creates new thread
const response1 = await invoke(agentId, {
  externalUserId: "customer-42",
  content: { text: "What is AI?" }
});
const threadId = response1.threadId; // Save this!

// Follow-up message - continues conversation
const response2 = await invoke(agentId, {
  externalUserId: "customer-42",
  content: { text: "Tell me more" },
  threadId: threadId  // Use saved threadId
});

Rate Limiting

  • Current implementation has no hard rate limits
  • Recommended: Implement client-side throttling to avoid overwhelming the API
  • Suggested limit: 10 requests per second per API key

Error Handling

Always implement proper error handling:

async function invokeAgent(agentId, userId, text, threadId = null) {
  try {
    const response = await fetch(
      `https://api.flutch.ai/api/v1/agents/${agentId}/invoke`,
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-API-Key': process.env.FLUTCH_API_KEY
        },
        body: JSON.stringify({
          externalUserId: userId,
          content: { text },
          ...(threadId && { threadId })
        })
      }
    );

    if (!response.ok) {
      const error = await response.json();
      throw new Error(`API Error: ${error.message}`);
    }

    return await response.json();
  } catch (error) {
    console.error('Failed to invoke agent:', error);
    throw error;
  }
}

Integration Examples

Node.js

const axios = require('axios');

const FLUTCH_API_KEY = process.env.FLUTCH_API_KEY;
const BASE_URL = 'https://api.flutch.ai/api/v1';

async function sendMessage(agentId, userId, text, threadId = null) {
  const response = await axios.post(
    `${BASE_URL}/agents/${agentId}/invoke`,
    {
      externalUserId: userId,
      content: { text },
      ...(threadId && { threadId })
    },
    {
      headers: {
        'X-API-Key': FLUTCH_API_KEY
      }
    }
  );

  return response.data;
}

// Usage
(async () => {
  const message = await sendMessage(
    '67b88f7ec6092d7bc711a934',
    'customer-42',
    'Hello, AI!'
  );

  console.log('Agent response:', message.content.text);
  console.log('Thread ID:', message.threadId);
})();

Python

import requests
import os

FLUTCH_API_KEY = os.getenv('FLUTCH_API_KEY')
BASE_URL = 'https://api.flutch.ai/api/v1'

def send_message(agent_id, user_id, text, thread_id=None):
    url = f'{BASE_URL}/agents/{agent_id}/invoke'
    headers = {
        'Content-Type': 'application/json',
        'X-API-Key': FLUTCH_API_KEY
    }

    payload = {
        'externalUserId': user_id,
        'content': {'text': text}
    }

    if thread_id:
        payload['threadId'] = thread_id

    response = requests.post(url, headers=headers, json=payload)
    response.raise_for_status()

    return response.json()

# Usage
message = send_message(
    '67b88f7ec6092d7bc711a934',
    'customer-42',
    'Hello, AI!'
)

print(f"Agent response: {message['content']['text']}")
print(f"Thread ID: {message['threadId']}")

Python: attachments and metadata

payload = {
    "externalUserId": "customer-42",
    "threadId": thread_id,  # optional
    "content": {
        "text": "What is on this photo?",
        "attachments": [
            {"type": "image", "value": [{"url": "https://example.com/photo.jpg"}]}
        ],
        "metadata": {"orderNumber": "A-1042"},
    },
    "metadata": {
        "utm": {"source": "newsletter", "campaign": "spring"},
        "currentPage": "https://example.com/pricing",
    },
}
message = requests.post(
    f"{BASE_URL}/agents/{agent_id}/invoke", headers=HEADERS, json=payload
).json()

# Buttons and other attachments come back in the reply
for att in message["content"].get("attachments", []):
    if att["type"] == "button":
        print(att["value"]["text"], att["value"]["type"], att["value"]["value"])

Python: sending a button press

A callback button carries a token in its value. Send it back as content.text to continue the funnel:

button = next(
    a["value"] for a in message["content"]["attachments"]
    if a["type"] == "button" and a["value"]["type"] == "callback"
)
requests.post(
    f"{BASE_URL}/agents/{agent_id}/invoke",
    headers=HEADERS,
    json={"externalUserId": "customer-42", "threadId": message["threadId"],
          "content": {"text": button["value"]}},
)

cURL

#!/bin/bash

AGENT_ID="67b88f7ec6092d7bc711a934"
USER_ID="customer-42"
API_KEY="flutch_your_api_key_here"
BASE_URL="https://api.flutch.ai/api/v1"

# Send message
curl -X POST "${BASE_URL}/agents/${AGENT_ID}/invoke" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: ${API_KEY}" \
  -d "{
    \"externalUserId\": \"${USER_ID}\",
    \"content\": {
      \"text\": \"Hello, AI!\"
    }
  }"

FAQ

Q: Can I use one API key for multiple users? A: Yes, the API key authenticates your company. You send your own id for the user (externalUserId) in each request; the user is created the first time an id is seen.

Q: How do I get my agent ID? A: Agent IDs are visible in the admin panel URL when viewing an agent (e.g., /agents/67b88f7ec6092d7bc711a934).

Q: What happens if I lose my API key? A: You cannot retrieve a lost API key. Generate a new one and update your applications. Revoke the old key if necessary.

Q: Can I have multiple API keys? A: Currently, only one API key per company is supported. Generating a new key doesn't automatically revoke the old one.

Q: Is there a webhook for receiving responses? A: Not currently. Use the streaming endpoint (/stream) for real-time responses.

Q: What's the maximum message length? A: There is no hard limit, but very long messages may be truncated by the underlying AI model (typically 4000-8000 tokens depending on the model).

Support

For technical support or questions:

  • Check the admin panel documentation
  • Contact your Flutch account manager
  • Email: support@flutch.ai

Changelog

Version 1.0 (2025-01-15)

  • Initial API release
  • /invoke endpoint for synchronous responses
  • /stream endpoint for real-time streaming
  • API key authentication

Version 1.1

  • partial events documented as JSON deltas (channel + delta)
  • Added requestId to thread_created, escalated event, :heartbeat comments
  • Added POST /agents/{agentId}/cancel
  • Added metadata request field and full content / attachments reference
  • Replaced the EventSource example with a fetch-based one

Version 1.2

  • Added GET /agents/{agentId}/threads, GET /agents/{agentId}/threads/{threadId}/messages, PATCH /agents/{agentId}/messages/{messageId}/reactions and POST /agents/{agentId}/uploads
  • Customers can be identified with your own id: externalUserId is accepted wherever userId is, and the user is created on first use
  • userId / externalUserId can be passed in the query string for requests without a JSON body

Version 1.3

  • Stopping a reply with /cancel no longer escalates the thread to a manager. The partial reply is saved with content.metadata.cancelled: true
  • Documented that closing the stream connection does not stop generation: the reply is finished and saved. Only /cancel stops it