Skip to content

API & MCP

Available on Pro and Business. On other plans the API returns 402.

The API is for servers, scripts and coding agents. Never put an API key in browser code: /v1 and /mcp send no CORS headers, so browsers block cross-site calls.

Create a key in Settings → API & MCP with Create API key. Only workspace owners and admins can manage keys. A key starts with fb_live_ and is shown once.

Send the key as a bearer token:

Terminal window
curl https://api.feedbotai.com/v1/bots \
-H "Authorization: Bearer $FEEDBOT_API_KEY"

X-API-Key: <key> works too. A missing or revoked key gets 401 with WWW-Authenticate: Bearer.

When you create a key, choose Read only or Read & write, or open Advanced and pick granular scopes.

Scope Allows
read Every read: bots, conversations, stats, issues, the agent report, insights, leads, tickets and ticket types.
write Everything: all reads and all writes.
leads:read List, get and export leads.
leads:write Mark leads as lead / not a lead. Includes leads:read.
tickets:read List and get tickets and ticket types.
tickets:write Create, update, comment on and resolve tickets. Includes tickets:read.
issues:write Update product issues. Also allows reading issues and the agent report.
insights:write Generate insight reports. Also allows reading insights.

A call without the needed scope gets 403, for example {"error": "this API key lacks the \"tickets:write\" scope"}. Older keys were created as read or read + write and keep working.

  • 120 requests per minute per key. REST and MCP share this budget: every HTTP request to /mcp counts.
  • 30 write calls per minute per workspace: POST and PATCH requests and MCP write tools.

Over the limit you get 429 with a Retry-After header. Responses also carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset.

Base URL: https://api.feedbotai.com. Responses are JSON wrapped in {"data": ...}.

The OpenAPI 3.1 document describes every endpoint, parameter and response. It is public (no API key), so you can import it into Postman, Insomnia or Swagger UI, or generate a client from it:

Terminal window
curl https://api.feedbotai.com/v1/openapi.json -o feedbot-openapi.json

Errors are always JSON: {"error": "message"}, sometimes with extra fields (a duplicate ticket 409 also has ticketId).

Status When
400 Invalid JSON, unknown JSON fields, an empty PATCH, or an invalid query parameter (date, limit, enum value, boolean, id, cursor).
401 Missing or invalid API key.
402 Your plan doesn’t include the API or this feature.
403 The key lacks the needed scope.
404 Unknown route, or the object doesn’t exist in your workspace. Objects of other workspaces are always 404.
405 Method not allowed on this route.
409 Conflict: an open ticket of that type already exists, the ticket is already resolved, or a request with the same Idempotency-Key is still running.
422 The Idempotency-Key was already used for a different request.
429 Rate limit exceeded.
500 Server error. Safe to retry with the same Idempotency-Key.

All timestamps in responses are RFC 3339 in UTC, for example 2026-10-01T12:00:00Z.

Date parameters (from, to, since, updatedSince) accept RFC 3339 with any offset (converted to UTC) or YYYY-MM-DD (a UTC day). from, since and updatedSince are inclusive. to is exclusive for a timestamp, but to=2026-09-30 includes the whole of September 30.

Booleans are true or false. limit is a positive integer; values above the maximum (200) are lowered to it. The default is 50 (20 for insights).

Lists return {"data": [...], "nextCursor": "..."}, including the short lists that are not paginated (/v1/bots, /v1/ticket-types), where nextCursor is always null. When nextCursor is not null, pass it as cursor to get the next page with the same filters. Cursors are opaque; don’t build them yourself.

Terminal window
curl "https://api.feedbotai.com/v1/conversations?botId=BOT_ID&limit=100&cursor=NEXT_CURSOR" \
-H "Authorization: Bearer $FEEDBOT_API_KEY"

/v1/conversations still accepts the old before parameter and returns nextBefore when there is a next page. Both are deprecated; use cursor.

Any POST accepts an Idempotency-Key header (1–255 characters, for example a UUID). POST /v1/tickets/{id}/resolve requires it.

  • The same key with the same method, path and body within 24 hours returns the stored response again, with Idempotent-Replayed: true. Nothing is done twice.
  • The same key with a different request gets 422.
  • While the first request is still running, a retry gets 409.
  • 5xx responses are not stored, so you can retry them with the same key.

Keys are per workspace and kept for 24 hours.

Method Path Scope Description
GET /v1/openapi.json none The OpenAPI document.
GET /v1/bots read Your bots.
GET /v1/conversations read Search conversations.
GET /v1/conversations/{id} read A conversation with its analysis and transcript.
GET /v1/attachments/{id} read Redirects to a file a visitor attached (link valid for 10 minutes).
GET /v1/stats read Counts for a period.
GET /v1/issues read or issues:write Product issues, most reported first.
GET /v1/issues/{id} read or issues:write One issue with quotes and linked conversations.
PATCH /v1/issues/{id} issues:write Update an issue.
GET /v1/reports/agent.md read or issues:write Agent-ready report in Markdown.
GET /v1/insights read or insights:write Insight reports.
GET /v1/insights/{id} read or insights:write One insight report.
POST /v1/insights insights:write Generate an insight report.
GET /v1/leads leads:read Leads.
GET /v1/leads/{id} leads:read One lead.
GET /v1/leads/export.csv leads:read Leads as CSV.
POST /v1/leads/{id}/status leads:write Mark as lead or not a lead.
GET /v1/ticket-types tickets:read Enabled ticket types.
GET /v1/tickets tickets:read Tickets.
GET /v1/tickets/{id} tickets:read One ticket with comments.
POST /v1/tickets tickets:write Create a ticket.
PATCH /v1/tickets/{id} tickets:write Update a ticket.
POST /v1/tickets/{id}/comments tickets:write Add a team comment.
POST /v1/tickets/{id}/resolve tickets:write Close a ticket, optionally replying to the customer.

GET /v1/bots returns id, name, siteUrl, status, archivedAt, installedAt and createdAt for each bot. It is not paginated: nextCursor is always null.

GET /v1/conversations, newest activity first. Filters:

Parameter Description
botId One bot.
q Full-text search in messages and summaries.
category support, sales, payments, complaint, feedback, bug, partnership, other or useless.
tag One tag.
minPriority 1–5.
humanNeeded true: only conversations that need a person.
leads true: only conversations of visitors who are leads.
from, to Last message time. from defaults to 30 days ago.
limit, cursor Pagination.

Spam is never returned.

GET /v1/conversations/{id} returns {"conversation": {...}, "messages": [...], "truncated": false}. Messages have id, role (user, assistant or operator), content, createdAt and, for operator replies, author (the team member’s name). Visitor messages with files have attachments: id, name, contentType, size, expiresAt and url. The url is GET /v1/attachments/{id}: call it with your API key and it redirects (302) to the file, or answers 410 after the retention period. Up to the last 1,000 messages are returned; truncated: true means older ones were left out.

Terminal window
curl "https://api.feedbotai.com/v1/conversations?botId=BOT_ID&humanNeeded=true&from=2026-09-01" \
-H "Authorization: Bearer $FEEDBOT_API_KEY"

GET /v1/stats?botId=&from=&to= (from defaults to 30 days ago) returns conversations, messages, leads, humanNeeded, resolved, avgSentiment, byCategory, topTags, openIssues and ctaClicks. Spam is excluded from every count. leads counts conversations whose visitor is a lead.

GET /v1/issues filters: botId, status (open, in_progress, fixed, wontfix or all; default open and in_progress), type (bug, ux, feature_request, content_gap, pricing), minPriority (1-5), since (last report time), limit, cursor. Ordered by priority, then by number of reports, then by the latest report.

Each issue has a priority from 1 to 5 (5 = urgent). The AI sets it when it first finds the issue and raises it when a more urgent report comes in. It never lowers it, and once someone sets the priority (in the dashboard or with PATCH), the AI leaves it alone.

New reports are matched to existing issues of any status, so a closed issue doesn’t come back as a duplicate. A report from a user message written after the issue was closed reopens a fixed issue and sets regressedAt (a possible regression; the agent report marks it Reopened). On a wontfix issue the report is counted in reportsSinceClosed and the status stays. closedAt is when the issue was last marked fixed or wontfix.

PATCH /v1/issues/{id} accepts status, priority, fixedNote, externalRef and notifyReporters:

Terminal window
curl -X PATCH https://api.feedbotai.com/v1/issues/ISSUE_ID \
-H "Authorization: Bearer $FEEDBOT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"status":"fixed","fixedNote":"CSV export now works in Safari","notifyReporters":true}'

GET /v1/reports/agent.md takes the same filters as /v1/issues (without cursor) and returns Markdown.

GET /v1/insights filters: botId, limit (default 20), cursor. Newest first.

POST /v1/insights with {"botId": "...", "from": "...", "to": "..."} (default: the last 30 days) generates a report. It needs on-demand reports, which are on Pro and Business. The response is 202 with {"id": "...", "status": "pending", "deduplicated": false}; poll GET /v1/insights/{id} until status is ready (about 30 seconds). If a report for the same bot and the same period (by UTC day) is already pending or processing, you get that one with deduplicated: true instead of a new one.

Terminal window
curl -X POST https://api.feedbotai.com/v1/insights \
-H "Authorization: Bearer $FEEDBOT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"botId":"BOT_ID","from":"2026-09-01","to":"2026-09-30"}'

Leads follow the same rule as the Leads page. The lead id is the visitor id.

{
"id": "0b6f…",
"name": "Ann Smith",
"firstName": "Ann",
"lastName": "Smith",
"email": "[email protected]",
"phone": "+1 555 010 2030",
"country": "US",
"company": "Acme Inc",
"leadScore": 85,
"leadStatus": "none",
"summary": "Team of 5, wants Pro yearly, asked about SSO.",
"bot": { "id": "5521cdf1-…", "name": "Acme" },
"userId": "",
"verified": false,
"conversationsCount": 2,
"conversationId": "9f0c…",
"lastConversationId": "9f0c…",
"conversationUrl": "https://app.feedbotai.com/c/9f0c…",
"pageUrl": "https://acme.io/pricing",
"firstSeenAt": "2026-09-29T10:02:11Z",
"lastSeenAt": "2026-09-29T10:15:40Z",
"exportedAt": null
}
  • leadStatus: lead (marked by your team), none (listed because of the AI score) or not_lead (dismissed).
  • leadScore is the highest score of the visitor and their last 5 conversations. summary is the latest summary of those conversations.
  • exportedAt is when the lead was sent to your CRM webhook, or null.
  • conversationId equals lastConversationId; it matches the lead.created webhook.

GET /v1/leads filters: botId, q (name, email, phone or user id), includeAnonymous=true (also visitors with no contact), marked (true: only marked by your team; false: only listed by the AI score), since (last seen), limit, cursor. Most recently seen first.

GET /v1/leads/{id} returns any visitor of your workspace; check leadStatus.

GET /v1/leads/export.csv takes the same filters and returns the CSV export (up to 10,000 rows).

POST /v1/leads/{id}/status with {"status": "lead"} or {"status": "not_lead"} marks the visitor. It is idempotent and answers at once: it never waits for your CRM. Marking as lead queues the lead for your CRM webhook, like the dashboard does; it is sent in the background within seconds, and a failed delivery is retried automatically. The response is {"data": <lead>, "export": "..."}:

export Meaning
queued The lead will be sent to your CRM webhook. Watch exportedAt on GET /v1/leads/{id} to see when it was delivered.
skipped Nothing to send: the status is not_lead, or the lead was already sent.
not_configured The workspace has no enabled CRM webhook, or the plan doesn’t include webhooks.

The response also has exported (whether the lead had been sent before this call). It is deprecated: use export and data.exportedAt.

Terminal window
curl -X POST https://api.feedbotai.com/v1/leads/VISITOR_ID/status \
-H "Authorization: Bearer $FEEDBOT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"status":"lead"}'

Tickets are requests only your team can handle.

{
"id": "7d1e…",
"botId": "5521cdf1-…",
"botName": "Acme",
"conversationId": "9f0c…",
"conversationUrl": "https://app.feedbotai.com/c/9f0c…",
"url": "https://app.feedbotai.com/t/7d1e…",
"type": "refund",
"typeName": "Refund",
"status": "open",
"priority": 4,
"title": "Refund · order #1234",
"description": "Charged twice for the yearly plan.",
"source": "bot",
"assignee": { "id": "u_1…", "name": "Kate", "email": "[email protected]" },
"dueAt": null,
"resolution": "",
"visitor": { "id": "0b6f…", "name": "Ann Smith", "email": "[email protected]", "phone": "", "country": "US" },
"canReply": true,
"createdAt": "2026-09-29T10:16:05Z",
"updatedAt": "2026-09-29T10:16:05Z",
"resolvedAt": null
}

source is ai, bot, manual or api. priority is 1 to 5 (5 = urgent): the AI sets it for the tickets it creates, and other tickets start at 3. canReply is false for conversations from legacy widgets, which can’t receive replies.

GET /v1/ticket-types lists the enabled types: key, name, description, emoji.

GET /v1/tickets filters: botId, status (open, in_progress, done or all; default open and in_progress), type (a type key), minPriority (1-5), assignee (a member’s email, or none for unassigned), conversationId, q (case-insensitive text in the title, description or resolution), since (creation time), updatedSince (last change: status, fields or a comment), limit, cursor. Newest first.

To sync tickets into another tool, poll with status=all&updatedSince=<time of your last sync>.

GET /v1/tickets/{id} adds comments: id, body, createdAt and author ({"type": "user", "name", "email"} for team members, {"type": "api", "name"} for comments posted with an API key).

POST /v1/tickets with conversationId, type (an enabled type key) and optional title, description and priority (1-5, default 3) creates a ticket with source api and notifies the team. Without a title, one is made from the description or the type. If the conversation already has an open ticket of that type, you get it back with 200 and "created": false instead of 201 and "created": true.

PATCH /v1/tickets/{id} accepts status, type, priority, title, description, dueAt (a date, or null) and assigneeEmail (a workspace member’s email, or null to unassign). Moving a ticket to a type, or reopening it, when the conversation already has an open ticket of that type gets 409 with the other ticket’s ticketId. Setting status to done resolves the ticket without a reply.

POST /v1/tickets/{id}/comments with {"body": "..."} (up to 5,000 characters) adds an internal comment. Customers never see it. In the dashboard it shows the API key’s name.

Terminal window
curl -X POST https://api.feedbotai.com/v1/tickets/TICKET_ID/comments \
-H "Authorization: Bearer $FEEDBOT_API_KEY" \
-H "Content-Type: application/json" \
-d '{"body":"Refund issued in Stripe: re_3Q…"}'

POST /v1/tickets/{id}/resolve marks the ticket done. Body, all optional:

Field Description
reply Sent to the customer’s chat as a team reply, signed with the API key’s name. Empty = just close the ticket.
sendEmail true: also email the reply now. Default false.
closeConversation true: also close the conversation in the inbox. Default false.

It requires an Idempotency-Key header, so a retry never sends the reply twice. A reply to a legacy-widget conversation gets 400, a reply on a ticket that is already done gets 409. The response is {"ticket", "replied", "emailQueued", "emailError", "conversationClosed"}.

Terminal window
curl -X POST https://api.feedbotai.com/v1/tickets/TICKET_ID/resolve \
-H "Authorization: Bearer $FEEDBOT_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{"reply":"Hi Ann, we refunded the second charge. It will show up in 5–10 days.","sendEmail":true}'

The reply goes to a real customer. Check it before you automate this.

The MCP server is at https://api.feedbotai.com/mcp. It uses Streamable HTTP, the same API keys and scopes, and the same rate limits.

Terminal window
claude mcp add --transport http feedbot https://api.feedbotai.com/mcp \
--header "Authorization: Bearer $FEEDBOT_API_KEY"
~/.codex/config.toml
[mcp_servers.feedbot]
url = "https://api.feedbotai.com/mcp"
bearer_token_env_var = "FEEDBOT_API_KEY"

Other MCP clients work the same way: HTTP transport, the URL above and the Authorization header. Settings → API & MCP shows these snippets.

Read-only tools are marked with readOnlyHint, so clients can run them without asking. resolve_ticket is marked destructive.

Tool Scope What it does
list_bots read List your bots ({"data", "nextCursor": null}).
get_agent_report read Markdown report of issues with user quotes and acceptance criteria, highest priority first, then most reported. Args: botId, status, type, minPriority, since, limit.
list_product_issues read The same issues as JSON. Args: botId, status, type, minPriority, since, limit, cursor.
get_issue read One issue with all evidence.
mark_issue_fixed issues:write Set an issue’s status (default fixed) with a note and an optional externalRef. notifyReporters: true emails the users who reported it.
set_issue_priority issues:write Set an issue’s priority (1-5). The AI no longer changes it afterwards.
search_conversations read Search by text (query, or q like the REST API), botId, category, tag, priority, humanNeeded or leads over the last days (default 30). Args also: limit, cursor.
get_conversation read Transcript (last 200 messages, including team replies with their author) and analysis. Says when older messages were left out.
get_insight_report read The latest ready insight report for a bot, or one by id.
list_insight_reports read Insight reports with their status. Args: botId, limit, cursor.
generate_insight_report insights:write Start a new report for the last days (Pro and Business). Returns the pending report if one for the same period is already running.
get_stats read Conversation, lead and issue counts for the last days.
list_leads leads:read Leads. Args: botId, q, includeAnonymous, marked, since, limit, cursor.
get_lead leads:read One lead by visitor id.
mark_lead leads:write Mark a visitor as lead or not_lead. Returns {"data", "export"} at once, like POST /v1/leads/{id}/status.
list_ticket_types tickets:read Enabled ticket types ({"data", "nextCursor": null}).
list_tickets tickets:read Tickets. Args: botId, status, type, minPriority, assignee, conversationId, q, since, updatedSince, limit, cursor.
get_ticket tickets:read One ticket with comments and the conversation link.
update_ticket tickets:write Change status, type, priority, dueAt or assigneeEmail.
comment_ticket tickets:write Add an internal team comment.
resolve_ticket tickets:write Close a ticket, optionally replying to the customer (reply, sendEmail, closeConversation). Destructive: the agent must show you the exact reply and get your confirmation before sending it.

since and updatedSince accept RFC 3339 or YYYY-MM-DD. Lists return {"data": [...], "nextCursor": ...} like the REST API. Tool errors (missing scope, not found, invalid arguments) are returned as tool results with isError: true.

Show me the top 5 open Feedbot issues and which pages they affect.

Fix the most reported Feedbot issue, run the tests, then mark it fixed with a short note.

List open Feedbot refund tickets and draft a reply for each. Don’t send anything until I approve.

Feedbot can also push events to your server: conversation.analyzed, digests, ticket.created and ticket.resolved through a webhook notification channel, and lead.created through the CRM lead webhook. Webhooks are on Pro and Business.

When an issue is marked fixed (by the API, MCP, the CLI or in the dashboard):

  • for 30 days the bot knows about the fix: if a visitor mentions the problem, it says it has been fixed and asks them to try again;
  • if you set notifyReporters, users who reported the issue and left an email get a “the issue you reported is fixed” email with your note.

If you used the API before October 2026:

  • GET /v1/stats no longer returns costUsd. byCategory and topTags exclude spam, like the totals.
  • leads=true on /v1/conversations and leads in stats now use the lead rule of the Leads page (marked by your team, or score 50+ from a visitor you can reach and didn’t dismiss) instead of the conversation’s score alone.
  • Transcripts include team replies (role: "operator" with author).
  • limit above 200 is lowered to 200 instead of falling back to 50. Pagination uses cursor / nextCursor; before / nextBefore still work but are deprecated.
  • Invalid parameters (dates, limit, enum values, unknown JSON fields, an empty PATCH) return 400 instead of being ignored.
  • Read-only keys are checked on every read; see scopes.
  • /v1 and /mcp no longer send CORS headers.
  • Webhooks use a versioned envelope (version: 2); the old fields are still sent.
  • POST /v1/leads/{id}/status and mark_lead no longer wait for the CRM webhook: they return export (queued, skipped or not_configured) and the lead is sent in the background. exported is deprecated.
  • /v1/bots and /v1/ticket-types (and the list_bots / list_ticket_types tools) return nextCursor: null next to data.
  • New: q and updatedSince on /v1/tickets and list_tickets (which also takes conversationId), and the public OpenAPI document.
  • Ticket events go only to rules with Tickets: new and resolved on. It is on for every existing rule, so nothing changes until you turn it off.