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
- Log in to your Flutch admin panel
- Navigate to
/tokens(hidden page, accessible by direct URL) - Click "Generate API Key"
- 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: your42is never another company's42.userId: the Flutch id of an existing user, as returned in theuserIdfield 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
| Method | Path | Purpose |
|---|---|---|
| POST | /agents/{agentId}/invoke | Send a message, get the complete reply as JSON |
| POST | /agents/{agentId}/stream | Send a message, get the reply as a Server-Sent Events stream |
| POST | /agents/{agentId}/cancel | Cancel a running streamed generation |
| GET | /agents/{agentId}/threads | List the user's threads with this agent |
| GET | /agents/{agentId}/threads/{threadId}/messages | Message history of a thread |
| PATCH | /agents/{agentId}/messages/{messageId}/reactions | Like / dislike a message |
| POST | /agents/{agentId}/uploads | Upload 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" } }
| Field | Type | Required | Description |
|---|---|---|---|
externalUserId | string | One of the two | Your own id for the user. Created on first use. See Identifying the user. |
userId | string | One of the two | Flutch id of an existing user of your company. |
content | object | Yes | The message. See Message content. |
threadId | string | No | Continue an existing conversation. Omit to create a new thread. The user must have access to the thread, otherwise 403. |
metadata | object | No | Request 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 supportsGETand cannot send headers, so it cannot call this endpoint. Read the response body withfetch(see the example below) or use an SSE client that supportsPOST.
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"}
| Field | Description |
|---|---|
threadId | Thread ID. Use it as threadId in the next request. |
requestId | ID 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.type | Fields | Meaning |
|---|---|---|
text_chunk | text | Next piece of text. Append it to the current text step. |
step_started | step (a step, e.g. type: "tool_use" with name, id) | A new block started, usually a tool call. |
tool_input_chunk | stepId, chunk | Next piece of the tool's input JSON. |
tool_output_chunk | stepId, chunk | The tool's result. |
output_chunk | stepId, chunk | Output 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}
reason | Meaning |
|---|---|
insufficient_funds | The company balance is not enough. |
no_lead_credits | No lead credits left. |
generation_error | The agent failed to generate an answer. |
thread_already_escalated | The 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
finalevent carrying that partial message. - If nothing had been generated yet, no reply is saved and the stream ends without a
finalevent. - 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 }
like | Meaning |
|---|---|
1 | Like |
2 | Dislike |
null | Remove 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.
| Field | Type | Description |
|---|---|---|
text | string | The message text. |
attachments | array | Files and structured items sent with the message. See Attachments. Audio and voice attachments are transcribed automatically before the agent sees them. |
metadata | object | Free-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": {} }
| Field | Type | Description |
|---|---|---|
type | string | One of the attachment types below. |
value | object / array | Payload; its shape depends on type. |
metadata | object | Optional, free-form. |
Attachment types you can send in a user message:
type | value |
|---|---|
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
| Field | Type | Description |
|---|---|---|
id | string | Message ID. |
threadId | string | Thread the message belongs to. Save it to continue the conversation. |
userId | string | The user the thread belongs to. |
agentId | string | The agent that replied. |
role | string | assistant for replies. |
platform | string | api. |
reason | string | Why the message was produced. agent_reply for a normal reply; manual_reply when a human manager answered; system_error / system_notification for system messages. |
status | string | Delivery status, e.g. delivered. |
replyToId | string | ID of the user message this answers. |
content.text | string | Final reply text. Convenience copy of the text channel. |
content.contentChains | array | Structured reply, one chain per channel. See Content chains. |
content.attachments | array | Buttons, cards, links, etc. See Reply attachments. |
content.metadata | object | Extra data attached by the agent. |
content.reasoningChains | array | Deprecated. Legacy copy of contentChains. |
createdAt, updatedAt | string | ISO timestamps. |
Content chains
Everything the agent produced is split into channels:
channel | Meaning |
|---|---|
text | The answer shown to the user. |
processing | Intermediate work (reasoning, tool calls). |
tools | Tool activity. |
Each chain is { "channel", "steps": [...], "isComplete": boolean }. A step has:
| Field | Type | Description |
|---|---|---|
index | number | Position inside the chain. |
type | string | text or tool_use. |
text | string | Text of a text step. |
name, id | string | Tool name and call ID (tool_use). |
input | string | Tool parameters as a JSON string. |
output | string | Tool result (long outputs are truncated). |
metadata | object | Optional. |
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:
type | Meaning | value |
|---|---|---|
button | Buttons 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. |
suggestion | Suggested follow-up | { "text", "description?" } |
citation | Source reference | { "source": { "url", "title", "type": "webpage" | "pdf" | "article", "articleId?", "knowledgeBaseId?" } } |
card | Interactive card | { "title", "description?", "fields?": [{ "label", "value", "type?" }], "buttons?" } |
chart | Chart | { "type": "line" | "bar" | "pie" | "area", "title", "datasets": [...] } |
gallery | Carousel of cards | list of cards |
image, video, file, audio, voice | Media | as in Attachments |
product_link, checkout_link, payment_link | Product / checkout / payment button | { "articleId", "kmsId", "text?", "showPrice?", "utm?" } |
order_link | Link to an order page | { "orderId", "page?", "text?" } |
recipe_card | Card for a single article with action buttons | { "articleId", "kmsId", "title?", "subtitle?", "imageUrl?", "actions": [...] } |
lead_form | In-chat contact form | { "title?", "text?", "fields?", "requiredFields?", "submitText?", "successText?" } |
telegram_deeplink, max_deeplink | Link into a messenger bot | bot 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 } }
| Status | When |
|---|---|
400 | Invalid request: both userId and externalUserId, neither of them, an externalUserId over 255 characters or with control characters, an unknown userId, an unsupported upload type. |
401 | Missing or invalid API key. |
403 | The 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. |
404 | The agent, thread or message does not exist (or, for reactions, is not yours). |
500 | Server error. |
Best Practices
Thread Management
Creating Conversations:
- Omit
threadIdin the first request to create a new conversation thread - Save the
threadIdfrom the response - Include the same
threadIdin 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
/invokeendpoint for synchronous responses/streamendpoint for real-time streaming- API key authentication
Version 1.1
partialevents documented as JSON deltas (channel+delta)- Added
requestIdtothread_created,escalatedevent,:heartbeatcomments - Added
POST /agents/{agentId}/cancel - Added
metadatarequest field and fullcontent/ attachments reference - Replaced the
EventSourceexample with afetch-based one
Version 1.2
- Added
GET /agents/{agentId}/threads,GET /agents/{agentId}/threads/{threadId}/messages,PATCH /agents/{agentId}/messages/{messageId}/reactionsandPOST /agents/{agentId}/uploads - Customers can be identified with your own id:
externalUserIdis accepted whereveruserIdis, and the user is created on first use userId/externalUserIdcan be passed in the query string for requests without a JSON body
Version 1.3
- Stopping a reply with
/cancelno longer escalates the thread to a manager. The partial reply is saved withcontent.metadata.cancelled: true - Documented that closing the stream connection does not stop generation: the reply is finished and saved. Only
/cancelstops it