REST API reference
Objective. Authenticate a server-side integration with a workspace API key, then read and operate the bots that key is allowed to access through the versioned REST API.
Prerequisites
- A server-side runtime that can make HTTPS requests. The REST API has no browser CORS support.
- A REST API or Both audience key created by a workspace owner.
Use the server-side base URL
The base URL is https://<your-host>/api/v1. The API is server-to-server only and sends no CORS headers. Never put an API key in browser code, a mobile bundle, or any client-side configuration.
Authenticate every request
Send the key in Authorization: Bearer flu_.... The key must have the REST API or Both audience. See the API keys guide to create, scope, rotate, and revoke it.
Read the rate limit before retrying
Each key can make 120 requests per fixed 60-second window. Windows are aligned to the clock, not to your first request: the counter resets at the boundary reported by X-RateLimit-Reset, so a fresh quota can arrive in anywhere from a moment to a full minute. Reset is a Unix timestamp in seconds.
Every response from a documented endpoint to a request carrying a valid key includes X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset — successes and errors alike, because a rejected request still spends a unit. Two cases carry no quota headers and spend nothing: a 401, because an absent, invalid, or revoked key cannot be metered; and a request that never reaches an endpoint at all — an unrouted path answers 404 not_found in the usual JSON envelope, and an unsupported method on a real path answers a bare 405.
The request is metered as soon as the key is recognised, before the audience, role, and bot-scope checks run. A loop hammering an endpoint its key may not use is therefore still rate limited, and X-RateLimit-Remaining on those 403 and 404 responses is the real remaining quota. When it is exhausted the API returns 429 with Retry-After; wait that many seconds before retrying.
Handle errors as JSON
Every response produced by the API — including a 404 for a path that matches no endpoint — uses the same envelope. Read error.code for program logic and show or log error.message for the specific request. The single exception is an unsupported method on a real path, which the framework rejects with a bare 405 and no body; treat a non-JSON body as a bug in the request, not in the response.
Error envelope
{
"error": {
"code": "invalid_request",
"message": "A field or request value is invalid."
}
}| Code | Status | Meaning |
|---|---|---|
| invalid_request | 400 | The request body, query, or cursor is invalid. |
| unauthorized | 401 | The bearer key is absent, invalid, or revoked. |
| forbidden | 403 | The key audience or role cannot use the endpoint. |
| not_found | 404 | The resource is absent, belongs to another workspace, or is outside a bot pin. |
| conflict | 409 | The requested state change conflicts with the current resource state. |
| rate_limited | 429 | The key has exceeded its request quota for the current window. |
| internal_error | 500 | The request could not be completed. |
Use the documented pagination shape
- 1Cursor lists return
{ data, nextCursor }.GET /bots/{botId}/conversationsandGET /bots/{botId}/ticketsuse this shape. Echo a non-nullnextCursoras?cursor=and stop only when it isnull. - 2Offset lists return
{ data, total, limit, offset }.GET /bots/{botId}/catalog-itemsandGET /bots/{botId}/contactsuse this shape. Increaseoffsetby the returned page length until it reachestotal. - 3The other collection endpoints return their documented
dataarray without a cursor or offset. For conversation filters, use__null__asflowIdornodeIdto select records without that descriptor.
Meet the role and bot-scope requirement
Viewer is the read baseline. Agent includes Viewer and adds Inbox ticket work and contact access. Editor includes Agent and adds Operations, conversation history, campaign changes, and appointment cancellation. Owner includes every lower role; no current v1 endpoint requires Owner above Editor. The endpoint table lists the minimum role, and higher roles are accepted.
A key pinned to a bot can call only that bot's /bots/{botId}/... endpoints. Calls for every other bot return 404, including bots in the same workspace. The API uses the same response for a missing bot, a bot in another workspace, and a bot outside the key's pin.
Endpoint reference
Paths below are relative to https://<your-host>/api/v1. Every endpoint lists its minimum role, its path and query parameters, its request body where it takes one, and a response sample with the exact fields the endpoint returns. Timestamps in the samples are epoch milliseconds and Micro values are integer micro-units; the values themselves are illustrative.
Identity
Inspect the API key currently making the request and its workspace scope.
/meviewer minimum roleReturn the authenticated API key and its workspace.
200 response
{
"key": {
"id": "key_123",
"name": "Production API",
"prefix": "flu_abcd123",
"role": "viewer",
"audience": "api",
"botId": "bot_123"
},
"workspaceId": "workspace_123"
}Bots
List the bots available to the key and retrieve a bot's flows and publish status.
/botsviewer minimum roleList bots available to the authenticated API key.
200 response
{
"data": [
{
"id": "bot_123",
"name": "Support Bot",
"workspaceId": "workspace_123",
"flowCount": 2,
"nodeCount": 12,
"connected": true,
"publishedVersion": 3,
"updatedAt": 1735689600000
}
]
}- A bot-pinned key returns only its pinned bot.
/bots/{botId}viewer minimum roleReturn one bot with its flows and latest publish status.
| Name | In | Type | Description |
|---|---|---|---|
| botIdrequired | path | string | Bot identifier. |
200 response
{
"bot": {
"id": "bot_123",
"name": "Support Bot",
"workspaceId": "workspace_123",
"flowCount": 2,
"nodeCount": 12,
"connected": true,
"publishedVersion": 3,
"updatedAt": 1735689600000
},
"flows": [
{
"id": "flow_123",
"name": "Main",
"sortOrder": 0,
"updatedAt": 1735689600000
}
],
"publish": {
"version": 3,
"publishedAt": 1735689600000,
"dirty": false
}
}- publish is null when the bot has never been published.
Usage
Inspect the authenticated key's workspace balance and grouped usage totals.
/usageviewer minimum roleReturn the authenticated key's workspace balance and usage totals.
| Name | In | Type | Description |
|---|---|---|---|
| since | query | integer (epoch ms) | 0 through the request time; defaults to 30 days before the request. |
200 response
{
"since": 1735689600000,
"balance": {
"includedMicro": 1000000,
"purchasedMicro": 250000,
"periodKey": "2025-01"
},
"usage": [
{
"kind": "message_send",
"botId": "bot_123",
"count": 42,
"costMicro": 12500
}
]
}- Usage is always for the authenticated key's workspace; no workspace parameter is accepted.
Analytics
Cohort analytics and flow-level funnel evidence.
/bots/{botId}/analytics/overviewviewer minimum roleSummarize conversation, order, handoff, and knowledge metrics for a cohort.
| Name | In | Type | Description |
|---|---|---|---|
| botIdrequired | path | string | Bot identifier. |
| from | query | integer (epoch ms) | Inclusive start timestamp. Defaults to now minus 7 days; must be nonnegative, at least now minus 90 days, no later than to, and within a 90-day span. |
| to | query | integer (epoch ms) | Inclusive end timestamp. Defaults to now; must be nonnegative, no later than now, at least from, and within a 90-day span. |
200 response
{
"range": {
"from": 1735084800000,
"to": 1735689600000
},
"overview": {
"conversations": {
"count": 40,
"uniqueContacts": 36,
"completed": 30,
"completionRate": 0.75
},
"orders": {
"total": 12,
"conversationsWithOrder": 10,
"conversionRate": 0.25
},
"handoff": {
"tickets": 5,
"conversations": 4,
"rate": 0.1,
"byReason": {
"node": 3,
"keyword": 1,
"manual": 1
}
},
"knowledge": {
"invocations": 18,
"conversationsAsked": 12,
"conversationsAnswered": 9,
"deflectionRate": 0.75
}
}
}- Returns 404 unless the Analytics plugin is installed.
/bots/{botId}/analytics/funnelviewer minimum roleReturn graph-ordered node reach and abandonment metrics for one flow.
| Name | In | Type | Description |
|---|---|---|---|
| botIdrequired | path | string | Bot identifier. |
| flowId | query | string | Flow identifier. When omitted, the current definition's mainFlowId is used. |
| from | query | integer (epoch ms) | Inclusive start timestamp. Defaults to now minus 7 days; must be nonnegative, at least now minus 90 days, no later than to, and within a 90-day span. |
| to | query | integer (epoch ms) | Inclusive end timestamp. Defaults to now; must be nonnegative, no later than now, at least from, and within a 90-day span. |
200 response
{
"range": {
"from": 1735084800000,
"to": 1735689600000
},
"flow": {
"id": "flow_main",
"name": "Main flow"
},
"flows": [
{
"id": "flow_main",
"name": "Main flow"
}
],
"totals": {
"entered": 40,
"completedConversations": 30
},
"nodes": [
{
"nodeId": "node_start",
"type": "start",
"label": "Start",
"reached": 40,
"abandoned": 0,
"abandonRate": 0,
"removed": false
}
]
}- Returns 404 unless the Analytics plugin is installed.
Operations
Operational health metrics and masked conversation evidence.
/bots/{botId}/operations/overvieweditor minimum roleSummarize operational cohorts, delivery, friction, and review-queue evidence.
| Name | In | Type | Description |
|---|---|---|---|
| botIdrequired | path | string | Bot identifier. |
| from | query | integer (epoch ms) | Inclusive start timestamp. Defaults to now minus 7 days; must be nonnegative, at least now minus 90 days, no later than to, and within a 90-day span. |
| to | query | integer (epoch ms) | Inclusive end timestamp. Defaults to now; must be nonnegative, no later than now, at least from, and within a 90-day span. |
200 response
{
"range": {
"from": 1735084800000,
"to": 1735689600000
},
"conversations": {
"count": 40,
"uniqueContacts": 36,
"filters": {
"from": 1735084800000,
"to": 1735689600000
}
},
"needsReview": {
"count": 3,
"filters": {
"from": 1735084800000,
"to": 1735689600000,
"needsReview": true
}
},
"completed": {
"numerator": 30,
"denominator": 32,
"rate": 0.9375,
"filters": {
"from": 1735084800000,
"to": 1735689600000,
"completed": true
}
},
"deliverySuccess": {
"numerator": 27,
"denominator": 30,
"rate": 0.9,
"filters": {
"from": 1735084800000,
"to": 1735689600000,
"delivery": "success"
}
},
"outcomes": [
{
"outcome": "completed",
"count": 30,
"filters": {
"from": 1735084800000,
"to": 1735689600000,
"outcome": "completed"
}
}
],
"friction": [
{
"flowId": "flow_main",
"nodeId": "node_order_status",
"count": 3,
"filters": {
"from": 1735084800000,
"to": 1735689600000,
"flowId": "flow_main",
"nodeId": "node_order_status"
}
}
],
"reviewQueue": [
{
"severity": "high",
"count": 2,
"filters": {
"from": 1735084800000,
"to": 1735689600000,
"queueSeverity": "high"
}
}
]
}/bots/{botId}/conversationseditor minimum roleList masked conversation summaries using cursor pagination and operational filters.
| Name | In | Type | Description |
|---|---|---|---|
| botIdrequired | path | string | Bot identifier. |
| cursor | query | non-empty opaque string | Non-empty cursor returned as nextCursor by an earlier response. |
| limit | query | integer | 1–100, default 50. |
| from | query | integer (epoch ms) | Inclusive conversation start timestamp; nonnegative. |
| to | query | integer (epoch ms) | Inclusive conversation start timestamp upper bound; nonnegative and not earlier than from when both are supplied. |
| reviewState | query | new | in_review | resolved | Filter by review state. |
| signalType | query | delivery_failed | send_unknown | inbound_failed | repeated_prompt | stalled | manual_flag | Filter by unresolved signal type. |
| severity | query | high | medium | Filter by unresolved signal severity. |
| outcome | query | active | completed | errored | stalled | Filter by conversation outcome. |
| flowId | query | string (1–200 characters) | __null__ | Filter friction evidence by flow identifier; use __null__ for evidence without a flow. |
| nodeId | query | string (1–200 characters) | __null__ | Filter friction evidence by node identifier; use __null__ for evidence without a node. |
| needsReview | query | true | false | true filters to new or in-review conversations with unresolved review evidence; false is accepted without adding a filter. |
| completed | query | true | false | true filters to closed conversations with completed outcome; false is accepted without adding a filter. |
| delivery | query | success | failure | Filter by whether a conversation has a successful or terminal-failure outbound delivery. |
| queueSeverity | query | high | medium | Filter the unresolved review queue by its highest severity. |
200 response
{
"data": [
{
"id": "conv_123",
"contactRef": "contact_123",
"profileName": "Ada Lovelace",
"channel": "whatsapp",
"maskedPhone": "•••• ••• 1234",
"lastMessageAt": 1735689600000,
"outcome": "active",
"reviewState": "in_review",
"reviewRevision": 2,
"issueSeverity": "high"
}
],
"nextCursor": "eyJsYXN0TWVzc2FnZUF0IjoxNzM1Njg5NjAwMDAwLCJpZCI6ImNvbnZfMTIzIn0"
}- Conversation summaries use masked identity; raw phone identity is not exposed.
- The public API rejects query and phone search parameters.
- nextCursor is opaque and must be echoed verbatim as cursor.
/bots/{botId}/conversations/{conversationId}editor minimum roleReturn a masked conversation summary with messages, diagnostics, review evidence, and delivery jobs.
| Name | In | Type | Description |
|---|---|---|---|
| botIdrequired | path | string | Bot identifier. |
| conversationIdrequired | path | string | Conversation identifier. |
| beforeSeq | query | integer | Nonnegative message sequence cursor. When supplied, returns messages with seq less than this value; omit for the newest page. Each page contains at most 50 messages. |
200 response
{
"conversation": {
"id": "conv_123",
"contactRef": "contact_123",
"profileName": "Ada Lovelace",
"channel": "whatsapp",
"maskedPhone": "•••• ••• 1234",
"lastMessageAt": 1735689600000,
"outcome": "active",
"reviewState": "in_review",
"reviewRevision": 2,
"issueSeverity": "high",
"messages": [
{
"id": "msg_123",
"seq": 50,
"direction": "in",
"payload": {
"type": "text",
"text": "Where is my order?"
},
"flowId": "flow_main",
"nodeId": "node_order_status",
"status": "sent",
"error": null,
"aiNote": null,
"createdAt": 1735689600000
}
],
"nextMessageCursor": 49,
"hasOlder": true,
"signals": [
{
"id": "signal_123",
"type": "delivery_failed",
"severity": "high",
"explanation": "The delivery provider rejected the message.",
"detectedAt": 1735689600000,
"resolvedAt": null
}
],
"notes": [
{
"id": "note_123",
"authorUserId": "user_123",
"body": "Follow up after the carrier status updates.",
"createdAt": 1735689600000
}
],
"reviewHistory": [
{
"id": "review_123",
"actorUserId": "user_123",
"fromState": "new",
"toState": "in_review",
"createdAt": 1735689600000
}
],
"diagnostics": [
{
"id": "diagnostic_123",
"publicationId": "pub_123",
"path": [
{
"flowId": "flow_main",
"nodeId": "node_order_status"
}
],
"aiNotes": "[\"ai: nudge\"]",
"variableDiff": {
"orderStatus": {
"before": null,
"after": "shipped"
}
},
"terminalState": "waiting:flow_main:node_order_status",
"createdAt": 1735689600000
}
],
"publication": {
"id": "pub_123",
"version": 7
},
"inboundJobs": [
{
"id": "inbound_job_123",
"state": "succeeded",
"attemptCount": 1,
"lastError": null,
"createdAt": 1735689600000,
"updatedAt": 1735689600000
}
],
"outboundJobs": [
{
"id": "outbound_job_123",
"state": "delivered",
"attemptCount": 1,
"originFlowId": "flow_main",
"originNodeId": "node_order_status",
"lastError": null,
"createdAt": 1735689600000,
"updatedAt": 1735689600000,
"attempts": [
{
"id": "attempt_123",
"attempt": 1,
"state": "sent",
"errorCode": null,
"errorMessage": null,
"startedAt": 1735689600000,
"finishedAt": 1735689600000
}
]
}
]
}
}Inbox
Claim, reply to, and resolve tickets created by human handoffs.
/bots/{botId}/ticketsagent minimum roleList human-handoff tickets for a bot.
| Name | In | Type | Description |
|---|---|---|---|
| botIdrequired | path | string | Bot identifier. |
| status | query | active | resolved | all | Filters tickets; active includes open and assigned tickets. Default active. |
| limit | query | integer | 1–100, default 50. |
| cursor | query | string | Opaque nextCursor value; at least 1 character. |
200 response
{
"data": [
{
"id": "ticket_123",
"botId": "bot_123",
"conversationId": "conversation_123",
"contactRef": "contact_123",
"phone": "+15551234567",
"status": "assigned",
"reason": "keyword",
"assigneeUserId": "user_123",
"handbackMessage": "Thanks for your patience. I’m handing you back to the assistant.",
"openedByUserId": null,
"resolvedByUserId": null,
"openedAt": 1735689600000,
"assignedAt": 1735689660000,
"resolvedAt": null,
"lastMessageAt": 1735689720000,
"createdAt": 1735689600000,
"updatedAt": 1735689720000,
"profileName": "Ada Lovelace",
"assigneeName": "Alex Agent",
"lastMessagePreview": "I need help with my order."
}
],
"nextCursor": "eyJsYXN0TWVzc2FnZUF0IjoxNzM1Njg5NzIwMDAwLCJpZCI6InRpY2tldF8xMjMifQ"
}/bots/{botId}/tickets/{ticketId}agent minimum roleGet a ticket and its conversation messages.
| Name | In | Type | Description |
|---|---|---|---|
| botIdrequired | path | string | Bot identifier. |
| ticketIdrequired | path | string | Ticket identifier. |
| before | query | integer (message sequence) | Non-negative sequence; returns messages before it. Mutually exclusive with since. |
| since | query | integer (message sequence) | Non-negative sequence; returns messages after it. Mutually exclusive with before. |
200 response
{
"ticket": {
"id": "ticket_123",
"botId": "bot_123",
"conversationId": "conversation_123",
"contactRef": "contact_123",
"phone": "+15551234567",
"status": "assigned",
"reason": "keyword",
"assigneeUserId": "user_123",
"handbackMessage": "Thanks for your patience. I’m handing you back to the assistant.",
"openedByUserId": null,
"resolvedByUserId": null,
"openedAt": 1735689600000,
"assignedAt": 1735689660000,
"resolvedAt": null,
"lastMessageAt": 1735689720000,
"createdAt": 1735689600000,
"updatedAt": 1735689720000,
"profileName": "Ada Lovelace",
"assigneeName": "Alex Agent"
},
"messages": [
{
"id": "message_123",
"seq": 42,
"direction": "out",
"payload": {
"kind": "text",
"text": "I can help with that."
},
"flowId": null,
"nodeId": null,
"status": "queued",
"error": null,
"aiNote": null,
"authorUserId": "user_123",
"createdAt": 1735689720000
}
],
"hasOlder": false,
"lastInboundAt": 1735689540000
}- Every route with a ticket ID returns 404 when that ticket belongs to a different bot.
- Messages are capped at 50 per response. before and since are mutually exclusive sequence cursors.
/bots/{botId}/tickets/{ticketId}/messagesagent minimum roleQueue an agent reply for a claimed ticket.
| Name | In | Type | Description |
|---|---|---|---|
| botIdrequired | path | string | Bot identifier. |
| ticketIdrequired | path | string | Ticket identifier. |
Request body
{
"text": "I can help with that."
}201 response
{
"messageId": "message_123"
}- Every route with a ticket ID returns 404 when that ticket belongs to a different bot.
- Workflow: claim the ticket, reply, then resolve it. A reply returns 409 unless the ticket is assigned to the user who created the API key.
- For WhatsApp, a free-form text reply returns 409 when the 24-hour customer service window is closed; send an approved template instead.
- Template variant: { templateId, params }. templateId is a non-empty string; params has 0–10 strings, each at most 1024 characters. The text field is trimmed and must be 1–4096 characters.
- The template must exist and be APPROVED, and params.length must equal the template’s body placeholder count; otherwise the request returns 400.
/bots/{botId}/tickets/{ticketId}/claimagent minimum roleClaim an open ticket for the API key’s user.
| Name | In | Type | Description |
|---|---|---|---|
| botIdrequired | path | string | Bot identifier. |
| ticketIdrequired | path | string | Ticket identifier. |
200 response
{
"ticket": {
"id": "ticket_123",
"botId": "bot_123",
"conversationId": "conversation_123",
"contactRef": "contact_123",
"phone": "+15551234567",
"status": "assigned",
"reason": "keyword",
"assigneeUserId": "user_123",
"handbackMessage": "Thanks for your patience. I’m handing you back to the assistant.",
"openedByUserId": null,
"resolvedByUserId": null,
"openedAt": 1735689600000,
"assignedAt": 1735689660000,
"resolvedAt": null,
"lastMessageAt": 1735689720000,
"createdAt": 1735689600000,
"updatedAt": 1735689720000
}
}- Every route with a ticket ID returns 404 when that ticket belongs to a different bot.
- Returns 409 if the ticket is already assigned to another agent or is resolved.
/bots/{botId}/tickets/{ticketId}/resolveagent minimum roleReturn a ticketed conversation to the bot.
| Name | In | Type | Description |
|---|---|---|---|
| botIdrequired | path | string | Bot identifier. |
| ticketIdrequired | path | string | Ticket identifier. |
200 response
{
"ticket": {
"id": "ticket_123",
"botId": "bot_123",
"conversationId": "conversation_123",
"contactRef": "contact_123",
"phone": "+15551234567",
"status": "resolved",
"reason": "keyword",
"assigneeUserId": "user_123",
"handbackMessage": "Thanks for your patience. I’m handing you back to the assistant.",
"openedByUserId": null,
"resolvedByUserId": "user_123",
"openedAt": 1735689600000,
"assignedAt": 1735689660000,
"resolvedAt": 1735689780000,
"lastMessageAt": 1735689720000,
"createdAt": 1735689600000,
"updatedAt": 1735689780000
}
}- Every route with a ticket ID returns 404 when that ticket belongs to a different bot.
- Resolving sends the configured handback message to the customer through handleHandback; it is not a silent status flip.
Campaigns
Broadcast campaign state and delivery statistics.
/bots/{botId}/campaignsviewer minimum roleList every campaign for a bot.
| Name | In | Type | Description |
|---|---|---|---|
| botIdrequired | path | string | Bot identifier. |
200 response
{
"data": [
{
"id": "campaign_123",
"botId": "bot_123",
"name": "January promotion",
"state": "running",
"template": {
"templateName": "january_offer",
"templateLanguage": "en_US",
"bodyParams": [
"Hi {{name}}, save 20% today."
]
},
"audience": {
"lastActiveWithinDays": 30,
"outcomes": [
"completed"
],
"signalTypes": [
"purchase_intent"
],
"phones": [
"+15551234567"
]
},
"scheduledAt": 1735689600000,
"startedAt": 1735689660000,
"completedAt": null,
"recipientCount": 240,
"lastError": null,
"createdAt": 1735603200000,
"updatedAt": 1735689660000,
"stats": {
"recipients": 240,
"skipped": 4,
"inFlight": 12,
"sent": 30,
"delivered": 90,
"read": 100,
"failed": 4
}
}
]
}- Returns 404 unless the campaigns plugin is installed.
- This unbounded list returns every campaign for the bot.
/bots/{botId}/campaigns/{campaignId}viewer minimum roleGet one campaign and its delivery statistics.
| Name | In | Type | Description |
|---|---|---|---|
| botIdrequired | path | string | Bot identifier. |
| campaignIdrequired | path | string | Campaign identifier. |
200 response
{
"campaign": {
"id": "campaign_123",
"botId": "bot_123",
"name": "January promotion",
"state": "running",
"template": {
"templateName": "january_offer",
"templateLanguage": "en_US",
"bodyParams": [
"Hi {{name}}, save 20% today."
]
},
"audience": {
"lastActiveWithinDays": 30,
"outcomes": [
"completed"
],
"signalTypes": [
"purchase_intent"
],
"phones": [
"+15551234567"
]
},
"scheduledAt": 1735689600000,
"startedAt": 1735689660000,
"completedAt": null,
"recipientCount": 240,
"lastError": null,
"createdAt": 1735603200000,
"updatedAt": 1735689660000,
"stats": {
"recipients": 240,
"skipped": 4,
"inFlight": 12,
"sent": 30,
"delivered": 90,
"read": 100,
"failed": 4
}
}
}- Returns 404 unless the campaigns plugin is installed.
/bots/{botId}/campaigns/{campaignId}/launcheditor minimum roleSchedule a draft campaign for launch.
| Name | In | Type | Description |
|---|---|---|---|
| botIdrequired | path | string | Bot identifier. |
| campaignIdrequired | path | string | Campaign identifier. |
200 response
{
"campaign": {
"id": "campaign_123",
"botId": "bot_123",
"name": "January promotion",
"state": "scheduled",
"template": {
"templateName": "january_offer",
"templateLanguage": "en_US",
"bodyParams": [
"Hi {{name}}, save 20% today."
]
},
"audience": {
"lastActiveWithinDays": 30
},
"scheduledAt": 1735689600000,
"startedAt": null,
"completedAt": null,
"recipientCount": null,
"lastError": null,
"createdAt": 1735603200000,
"updatedAt": 1735689600000
}
}- Returns 404 unless the campaigns plugin is installed.
- Returns 409 when the campaign cannot transition from draft to scheduled.
/bots/{botId}/campaigns/{campaignId}/canceleditor minimum roleCancel a scheduled or running campaign.
| Name | In | Type | Description |
|---|---|---|---|
| botIdrequired | path | string | Bot identifier. |
| campaignIdrequired | path | string | Campaign identifier. |
200 response
{
"campaign": {
"id": "campaign_123",
"botId": "bot_123",
"name": "January promotion",
"state": "cancelled",
"template": {
"templateName": "january_offer",
"templateLanguage": "en_US",
"bodyParams": [
"Hi {{name}}, save 20% today."
]
},
"audience": {
"lastActiveWithinDays": 30
},
"scheduledAt": 1735689600000,
"startedAt": 1735689660000,
"completedAt": null,
"recipientCount": 240,
"lastError": null,
"createdAt": 1735603200000,
"updatedAt": 1735693200000
}
}- Returns 404 unless the campaigns plugin is installed.
- Returns 409 when the campaign cannot transition from scheduled or running to cancelled.
Bookings
Appointments created through the booking plugin.
/bots/{botId}/appointmentsviewer minimum roleList appointments in a selected time scope.
| Name | In | Type | Description |
|---|---|---|---|
| botIdrequired | path | string | Bot identifier. |
| scope | query | upcoming | past | all | Defaults to upcoming. |
| limit | query | integer | 1–200, default 50. |
200 response
{
"data": [
{
"id": "appointment_123",
"botId": "bot_123",
"phone": "+15551234567",
"profileName": "Avery Lee",
"conversationId": "conversation_123",
"serviceName": "Initial consultation",
"reference": "APT-ABCD23",
"startAt": 1735689600000,
"endAt": 1735691400000,
"status": "confirmed",
"cancelledAt": null,
"createdAt": 1735603200000,
"updatedAt": 1735603200000
}
]
}- Returns 404 unless the booking plugin is installed.
/bots/{botId}/appointments/{appointmentId}/canceleditor minimum roleCancel an appointment.
| Name | In | Type | Description |
|---|---|---|---|
| botIdrequired | path | string | Bot identifier. |
| appointmentIdrequired | path | string | Appointment identifier. |
200 response
{
"appointment": {
"id": "appointment_123",
"botId": "bot_123",
"phone": "+15551234567",
"profileName": "Avery Lee",
"conversationId": "conversation_123",
"serviceName": "Initial consultation",
"reference": "APT-ABCD23",
"startAt": 1735689600000,
"endAt": 1735691400000,
"status": "cancelled",
"cancelledAt": 1735689700000,
"createdAt": 1735603200000,
"updatedAt": 1735689700000
}
}- Returns 404 unless the booking plugin is installed.
Catalog
Safe catalog-item listings for a bot.
/bots/{botId}/catalog-itemsviewer minimum roleList safe catalog item summaries with offset pagination.
| Name | In | Type | Description |
|---|---|---|---|
| botIdrequired | path | string | Bot identifier. |
| catalogId | query | string | At least 1 character; filters to a catalog ID. |
| category | query | string | At least 1 character; filters to an exact category. |
| query | query | string | At least 1 non-whitespace character; case-insensitive substring search of name, description, or retailer ID. |
| availability | query | string | At least 1 character; filters to an exact availability value. |
| limit | query | integer | 1–200, default 50. |
| offset | query | integer | 0 or greater, default 0. |
200 response
{
"data": [
{
"id": "catalog_item_123",
"catalogId": "catalog_123",
"catalogSourceId": "catalog_source_123",
"retailerId": "SKU-001",
"name": "Canvas Tote",
"description": "Durable everyday tote bag.",
"priceMinor": 2999,
"currency": "USD",
"imageUrl": "https://cdn.example.com/catalog/canvas-tote.jpg",
"category": "Bags",
"availability": "in_stock"
}
],
"total": 1,
"limit": 50,
"offset": 0
}- Safe-field projection; never includes rawJson or contentHash.
Contacts
Contact summaries for a bot.
/bots/{botId}/contactsagent minimum roleList contact summaries with offset pagination.
| Name | In | Type | Description |
|---|---|---|---|
| botIdrequired | path | string | Bot identifier. |
| limit | query | integer | 1–200, default 50. |
| offset | query | integer | 0 or greater, default 0. |
200 response
{
"data": [
{
"phone": "+15551234567",
"profileName": "Avery Lee",
"lastSeenAt": 1735689600000
}
],
"total": 1,
"limit": 50,
"offset": 0
}Fetch an analytics overview
Supply an explicit range when the integration reports a defined period. The response returns the applied range and the metric groups for that cohort.
curl
curl "https://<your-host>/api/v1/bots/bot_123/analytics/overview?from=1735689600000&to=1735776000000" \
-H "Authorization: Bearer flu_..."200 response
{
"range": {
"from": 1735689600000,
"to": 1735776000000
},
"overview": {
"conversations": {
"count": 42,
"uniqueContacts": 37,
"completed": 28,
"completionRate": 0.6667
},
"orders": {
"total": 14,
"conversationsWithOrder": 12,
"conversionRate": 0.2857
},
"handoff": {
"tickets": 5,
"conversations": 5,
"rate": 0.119,
"byReason": { "node": 3, "keyword": 1, "manual": 1 }
},
"knowledge": {
"invocations": 31,
"conversationsAsked": 20,
"conversationsAnswered": 16,
"deflectionRate": 0.8
}
}
}Paginate conversations in JavaScript
Keep the opaque cursor exactly as returned. Do not decode it, synthesize it, or stop based on an expected page count.
JavaScript
const baseUrl = "https://<your-host>/api/v1";
const botId = "bot_123";
const conversations = [];
let cursor: string | null | undefined;
do {
const url = new URL(`bots/${botId}/conversations`, `${baseUrl}/`);
url.searchParams.set("limit", "50");
if (cursor) url.searchParams.set("cursor", cursor);
const response = await fetch(url, {
headers: { Authorization: "Bearer flu_..." },
});
if (!response.ok) throw new Error(`Request failed: ${response.status}`);
const page = await response.json();
conversations.push(...page.data);
cursor = page.nextCursor;
} while (cursor !== null);Work an Inbox ticket in order
- 1Claim the ticket with
POST /bots/{botId}/tickets/{ticketId}/claim. - 2Reply with the message endpoint. A reply returns
409 conflictunless the ticket is assigned to the user who created the API key. A reply also returns409 conflictwhen the ticket cannot receive messages because it is resolved or closed. - 3For WhatsApp, a free-form reply returns
409 conflictwhen the 24-hour customer service window is closed. This is Meta's rule, not Fluenta's. Send an approved template with{ templateId, params }instead. - 4A template must already exist and be
APPROVEDin the workspace. A missing template, an unapproved template, or a parameter count that does not match the template's body placeholders returns400 invalid_request. - 5Resolve only after the final reply.
POST /bots/{botId}/tickets/{ticketId}/resolvesends the configured handback message to the customer and resolves the ticket.
Post a ticket reply
curl
curl -X POST "https://<your-host>/api/v1/bots/bot_123/tickets/ticket_123/messages" \
-H "Authorization: Bearer flu_..." \
-H "Content-Type: application/json" \
--data '{"text":"Your appointment is confirmed for Tuesday at 14:00."}'Keep units explicit
Timestamps are epoch milliseconds throughout the API. Values with a Micro suffix are integer micro-units, not decimal currency strings. Preserve both formats as integers in client code to avoid rounding or timezone conversion errors.