All guides

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

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."
  }
}
CodeStatusMeaning
invalid_request400The request body, query, or cursor is invalid.
unauthorized401The bearer key is absent, invalid, or revoked.
forbidden403The key audience or role cannot use the endpoint.
not_found404The resource is absent, belongs to another workspace, or is outside a bot pin.
conflict409The requested state change conflicts with the current resource state.
rate_limited429The key has exceeded its request quota for the current window.
internal_error500The request could not be completed.

Use the documented pagination shape

  1. 1Cursor lists return { data, nextCursor }. GET /bots/{botId}/conversations and GET /bots/{botId}/tickets use this shape. Echo a non-null nextCursor as ?cursor= and stop only when it is null.
  2. 2Offset lists return { data, total, limit, offset }. GET /bots/{botId}/catalog-items and GET /bots/{botId}/contacts use this shape. Increase offset by the returned page length until it reaches total.
  3. 3The other collection endpoints return their documented data array without a cursor or offset. For conversation filters, use __null__ as flowId or nodeId to 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.

GET/meviewer minimum role

Return 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.

GET/botsviewer minimum role

List 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.
GET/bots/{botId}viewer minimum role

Return one bot with its flows and latest publish status.

NameInTypeDescription
botIdrequiredpathstringBot 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.

GET/usageviewer minimum role

Return the authenticated key's workspace balance and usage totals.

NameInTypeDescription
sincequeryinteger (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.

GET/bots/{botId}/analytics/overviewviewer minimum role

Summarize conversation, order, handoff, and knowledge metrics for a cohort.

NameInTypeDescription
botIdrequiredpathstringBot identifier.
fromqueryinteger (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.
toqueryinteger (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.
GET/bots/{botId}/analytics/funnelviewer minimum role

Return graph-ordered node reach and abandonment metrics for one flow.

NameInTypeDescription
botIdrequiredpathstringBot identifier.
flowIdquerystringFlow identifier. When omitted, the current definition's mainFlowId is used.
fromqueryinteger (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.
toqueryinteger (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.

GET/bots/{botId}/operations/overvieweditor minimum role

Summarize operational cohorts, delivery, friction, and review-queue evidence.

NameInTypeDescription
botIdrequiredpathstringBot identifier.
fromqueryinteger (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.
toqueryinteger (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"
      }
    }
  ]
}
GET/bots/{botId}/conversationseditor minimum role

List masked conversation summaries using cursor pagination and operational filters.

NameInTypeDescription
botIdrequiredpathstringBot identifier.
cursorquerynon-empty opaque stringNon-empty cursor returned as nextCursor by an earlier response.
limitqueryinteger1–100, default 50.
fromqueryinteger (epoch ms)Inclusive conversation start timestamp; nonnegative.
toqueryinteger (epoch ms)Inclusive conversation start timestamp upper bound; nonnegative and not earlier than from when both are supplied.
reviewStatequerynew | in_review | resolvedFilter by review state.
signalTypequerydelivery_failed | send_unknown | inbound_failed | repeated_prompt | stalled | manual_flagFilter by unresolved signal type.
severityqueryhigh | mediumFilter by unresolved signal severity.
outcomequeryactive | completed | errored | stalledFilter by conversation outcome.
flowIdquerystring (1–200 characters) | __null__Filter friction evidence by flow identifier; use __null__ for evidence without a flow.
nodeIdquerystring (1–200 characters) | __null__Filter friction evidence by node identifier; use __null__ for evidence without a node.
needsReviewquerytrue | falsetrue filters to new or in-review conversations with unresolved review evidence; false is accepted without adding a filter.
completedquerytrue | falsetrue filters to closed conversations with completed outcome; false is accepted without adding a filter.
deliveryquerysuccess | failureFilter by whether a conversation has a successful or terminal-failure outbound delivery.
queueSeverityqueryhigh | mediumFilter 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.
GET/bots/{botId}/conversations/{conversationId}editor minimum role

Return a masked conversation summary with messages, diagnostics, review evidence, and delivery jobs.

NameInTypeDescription
botIdrequiredpathstringBot identifier.
conversationIdrequiredpathstringConversation identifier.
beforeSeqqueryintegerNonnegative 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.

GET/bots/{botId}/ticketsagent minimum role

List human-handoff tickets for a bot.

NameInTypeDescription
botIdrequiredpathstringBot identifier.
statusqueryactive | resolved | allFilters tickets; active includes open and assigned tickets. Default active.
limitqueryinteger1–100, default 50.
cursorquerystringOpaque 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"
}
GET/bots/{botId}/tickets/{ticketId}agent minimum role

Get a ticket and its conversation messages.

NameInTypeDescription
botIdrequiredpathstringBot identifier.
ticketIdrequiredpathstringTicket identifier.
beforequeryinteger (message sequence)Non-negative sequence; returns messages before it. Mutually exclusive with since.
sincequeryinteger (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.
POST/bots/{botId}/tickets/{ticketId}/messagesagent minimum role

Queue an agent reply for a claimed ticket.

NameInTypeDescription
botIdrequiredpathstringBot identifier.
ticketIdrequiredpathstringTicket 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.
POST/bots/{botId}/tickets/{ticketId}/claimagent minimum role

Claim an open ticket for the API key’s user.

NameInTypeDescription
botIdrequiredpathstringBot identifier.
ticketIdrequiredpathstringTicket 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.
POST/bots/{botId}/tickets/{ticketId}/resolveagent minimum role

Return a ticketed conversation to the bot.

NameInTypeDescription
botIdrequiredpathstringBot identifier.
ticketIdrequiredpathstringTicket 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.

GET/bots/{botId}/campaignsviewer minimum role

List every campaign for a bot.

NameInTypeDescription
botIdrequiredpathstringBot 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.
GET/bots/{botId}/campaigns/{campaignId}viewer minimum role

Get one campaign and its delivery statistics.

NameInTypeDescription
botIdrequiredpathstringBot identifier.
campaignIdrequiredpathstringCampaign 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.
POST/bots/{botId}/campaigns/{campaignId}/launcheditor minimum role

Schedule a draft campaign for launch.

NameInTypeDescription
botIdrequiredpathstringBot identifier.
campaignIdrequiredpathstringCampaign 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.
POST/bots/{botId}/campaigns/{campaignId}/canceleditor minimum role

Cancel a scheduled or running campaign.

NameInTypeDescription
botIdrequiredpathstringBot identifier.
campaignIdrequiredpathstringCampaign 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.

GET/bots/{botId}/appointmentsviewer minimum role

List appointments in a selected time scope.

NameInTypeDescription
botIdrequiredpathstringBot identifier.
scopequeryupcoming | past | allDefaults to upcoming.
limitqueryinteger1–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.
POST/bots/{botId}/appointments/{appointmentId}/canceleditor minimum role

Cancel an appointment.

NameInTypeDescription
botIdrequiredpathstringBot identifier.
appointmentIdrequiredpathstringAppointment 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.

GET/bots/{botId}/catalog-itemsviewer minimum role

List safe catalog item summaries with offset pagination.

NameInTypeDescription
botIdrequiredpathstringBot identifier.
catalogIdquerystringAt least 1 character; filters to a catalog ID.
categoryquerystringAt least 1 character; filters to an exact category.
queryquerystringAt least 1 non-whitespace character; case-insensitive substring search of name, description, or retailer ID.
availabilityquerystringAt least 1 character; filters to an exact availability value.
limitqueryinteger1–200, default 50.
offsetqueryinteger0 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.

GET/bots/{botId}/contactsagent minimum role

List contact summaries with offset pagination.

NameInTypeDescription
botIdrequiredpathstringBot identifier.
limitqueryinteger1–200, default 50.
offsetqueryinteger0 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

  1. 1Claim the ticket with POST /bots/{botId}/tickets/{ticketId}/claim.
  2. 2Reply with the message endpoint. A reply returns 409 conflict unless the ticket is assigned to the user who created the API key. A reply also returns 409 conflict when the ticket cannot receive messages because it is resolved or closed.
  3. 3For WhatsApp, a free-form reply returns 409 conflict when the 24-hour customer service window is closed. This is Meta's rule, not Fluenta's. Send an approved template with { templateId, params } instead.
  4. 4A template must already exist and be APPROVED in the workspace. A missing template, an unapproved template, or a parameter count that does not match the template's body placeholders returns 400 invalid_request.
  5. 5Resolve only after the final reply. POST /bots/{botId}/tickets/{ticketId}/resolve sends 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.

Expected result. The integration sends a REST-enabled key only from a server-side runtime, checks the documented role and bot scope, follows the returned pagination shape, and handles rate-limit and error envelopes before retrying work.

Related guides