API reference
Conversations
Web chat conversations with their messages, costs and lead. Your team can take one over, reply as a person and hand it back to the assistant.
- GET
/api/conversationsList conversations - GET
/api/conversations/{id}Get a conversation - GET
/api/conversations/{id}/journeyThe visits this conversation's visitor made to the site - POST
/api/conversations/{id}/messagesSend a message as a person - POST
/api/conversations/{id}/releaseHand a conversation back to the assistant - POST
/api/conversations/{id}/takeoverTake over a conversation - POST
/api/conversations/{id}/takeover/heartbeatKeep a takeover alive - GET
/api/conversations/handoff-summarySummarize human handoffs - GET
/api/test-dataCount a project's test data - POST
/api/test-data/clearClear a project's test and preview chats and leads
List conversations
GET/api/conversations
Web chat and WhatsApp conversations, newest first by default, one cursor page at a time. Each row is the conversation without its messages; fetch one by id for the transcript. Test conversations are left out unless include_test=true.
Secret API key. How it works
Query parameters
project_idstringOnly this project's conversations (an org-wide key sees every project by default).
assistant_idarray of stringOnly these assistants' conversations. Repeat the parameter or comma-separate the values; a row matching any of them is kept.
statusstringOnly open or only ended conversations.
One of
openendedqstringCase-insensitive literal substring across conversation id and lead name/email.
fromstringISO 8601 timestampCreated at or after this time (ISO 8601 with an offset).
tostringISO 8601 timestampCreated at or before this time (ISO 8601 with an offset).
engagementstringengaged: the visitor replied at least once.no_reply: they never did.One of
engagedno_replyallDefault
"all"handoffstringrequested: a handoff was asked for. pending: asked for, nobody has taken over, chat open. answered: a human took over at or after the request. missed: the chat ended before anyone took over (never an abandoned chat). missed_in_hours: missed, asked while advisors were said to be available. out_of_hours: asked while advisors were said to be unavailable (outside working hours). active: someone holds the seat now. taken_over: a human took over at least once, asked for or not. abandoned: a human took over and then left the visitor waiting (human_outcome abandoned).
One of
requestedpendingansweredmissedmissed_in_hoursout_of_hoursactivetaken_overabandonedinclude_testbooleanTest chats (metadata.test, e.g. the console's test chat) are left out unless this is true. Ignored when
modeis given.Default
falsemodestringWhich chats to include, by the mode the server gave each. Absent: test and preview chats are left out, and so are live chats a page marked as a test, as before. live: every live-mode chat, including those a page marked as a test. test: test-mode chats. preview: preview chats. all: every chat.
One of
livetestpreviewallneeds_reviewbooleanWhen true, only conversations whose analysis flagged the human advisor's replies with severity 2 or more (see ConversationAnalysis.human_review).
taken_over_bystringOnly conversations this operator id (the takeover
agent.id) holds or held.sortstringSort by creation time or by number of messages.
One of
created_atmessage_countDefault
"created_at"dirstringSort direction.
One of
ascdescDefault
"desc"cursorstringnext_cursorfrom the previous page. Keep the same filters and sort while paging.limitinteger1 to 200Page size.
Default
50include_totalbooleanWhen true, the response carries
total: the number of conversations matching every filter, ignoring the cursor.Default
false
Returns
200 OK
Response attributes
dataarray of Conversationrequired41 attributes
idstringconversation_idstringAlias used by the public resume surface.
org_idstringproject_idstringassistant_idstringwidget_key_idstring | nullchannelstringOne of
web_chatwhatsappmodestringSet by the server from the key the chat came in on: test chats run the draft, preview chats send nothing out.
One of
livetestpreviewstatusstringrequiredOne of
openendedended_reasonstring | nullOne of
visitor_endedidle_timeoutmax_messageserrorresumablebooleanPublic resume surface: ended only because the visitor went quiet, so the next message reopens it.
handoff_requested_atstring | nullISO 8601 timestamptaken_over_atstring | nullISO 8601 timestamptaken_over_heartbeat_atstring | nullISO 8601 timestamptaken_over_bystring | nulltaken_over_by_namestring | nulltaken_overbooleanfirst_taken_over_atstring | nullISO 8601 timestampFirst time a human took over; unlike taken_over_at, never cleared.
handoff_answered_atstring | nullISO 8601 timestampFirst takeover at or after handoff_requested_at.
handoff_in_hoursboolean | nullWhether the bot told the visitor advisors were available when they first asked for a person, by the assistant's working hours (none configured counts as available). Null when unknown, as on requests from before 7 October 2026 without a stored answer.
handoff_statusstringmissed: the chat ended before anyone took over. abandoned: a human took over and then left the visitor waiting; it takes precedence over the others.
One of
nonependingansweredmissedabandonedhandoff_answer_secondsinteger | nullhuman_outcomestring | nullNull when no human took part. handled: every visitor message to the human got a human reply. abandoned: at least one did not, before the bot replied, the chat ended, or (chat still open) 5 minutes passed. Recomputed when a seat is released, when the chat ends, and on each analysis run.
One of
handledabandonedhuman_metricsHumanMetrics | nullOne of 2 shapes
HumanMetrics
first_response_secondsinteger | nullrequiredFrom the first takeover to the first human message.
longest_wait_secondsinteger | nullrequiredThe longest a visitor waited on the human, answered or not.
unanswered_countintegerrequiredVisitor messages to the human that no human answered.
agent_message_countintegerrequired
Option 2
needs_reviewbooleanOperator list only: the analysis flagged the human advisor's replies with severity 2 or more.
visitor_idstring | nullparent_originstring | nulllast_message_atstring | nullISO 8601 timestampmessage_countintegerlead_idstring | nullmetadataobject | nullcallbackobject | nullOne of 2 shapes
Option 1
phonestringrequiredslot_startstringISO 8601 timestampnotestringrequested_atstringrequiredISO 8601 timestamp
Option 2
ended_atstring | nullISO 8601 timestampcreated_atstringISO 8601 timestampupdated_atstringISO 8601 timestampmessagesarray of ConversationMessage12 attributes
idstringrequiredconversation_idstringorg_idstringproject_idstringrolestringrequiredOne of
userassistantagenttoolsystemcontentstringrequiredrichRichContent | null4 attributes
quick_repliesarray of object2 attributes
labelstringrequiredvaluestringrequired
buttonsarray of object3 attributes
typestringrequiredOne of
linklabelstringrequiredurlstringrequireduri
metadataobject3 attributes
pageobjectVisitor messages from the web widget: the page they sent the message from.
2 attributes
pathstringrequiredtitlestringrequired
channelstringA turn spoken in a voice session of this conversation rather than typed.
One of
voicevoice_session_idstringThe voice session the turn was spoken in.
attachmentsarray of ConversationAttachmentFiles sent with the message. Each
urlis a signed link that works without credentials.5 attributes
idstringrequirednamestringrequiredcontent_typestringrequiredOne of
image/jpegimage/pngimage/webpimage/gifapplication/pdfapplication/vnd.openxmlformats-officedocument.wordprocessingml.documentsizeintegerrequiredBytes.
urlstringrequireduriSigned download link (GET /public/chat/attachments/{id}), valid for 24 hours (7 days in webhooks).
tool_callsarray of object | nulltool_call_idstring | nullseqintegercreated_atstringrequiredISO 8601 timestampupdated_atstringISO 8601 timestamp
costsarray of objectleadLead | nullOne of 2 shapes
Lead
idstringrequiredorg_idstringrequiredproject_idstringrequiredassistant_idstringrequiredconversation_idstring | nullnamestring | nullemailstring | nullemailphonestring | nullfieldsobject | nullsourcestringrequiredOne of
web_chatwhatsappmodestringThe mode of the chat that captured it: test and preview leads are left out of lists unless asked for.
One of
livetestpreviewoutcomestring | nullWhat became of the lead, as your CRM reports it (PUT /leads/{id}/outcome).
One of
openbookedlostoutcome_valuestring | nullBooking value as a decimal string.
outcome_currencystring | nulloutcome_atstring | nullISO 8601 timestampoutcome_refstring | nullThe CRM's own id for the booking.
customer_idstring | nullThe customer's own id for this person, set with PUT /v1/leads/{id}/outcome.
attributionChatAttribution | nullWhere the lead's chat came from. Returned by the list and detail reads; null when none was recorded.
One of 2 shapes
ChatAttribution
chat_pagestringThe path the visitor started chatting on.
visitChatAttributionTouchThis visit's landing.
12 attributes
landing_pagestringA path starting with /, without query or fragment.
referrer_hoststringutm_sourcestringutm_mediumstringutm_campaignstringutm_termstringutm_contentstringgclidstringgbraidstringwbraidstringfbclidstringmsclkidstring
firstobjectThe visitor's first-ever landing.
13 attributes
landing_pagestringA path starting with /, without query or fragment.
referrer_hoststringutm_sourcestringutm_mediumstringutm_campaignstringutm_termstringutm_contentstringgclidstringgbraidstringwbraidstringfbclidstringmsclkidstringatstringISO 8601 timestampWhen the visitor first landed.
Option 2
created_atstringISO 8601 timestampupdated_atstringISO 8601 timestamp
Option 2
summarystring | nullOperator list only: the latest background summary, when analysis is on.
analysisConversationAnalysis | nullOperator detail only: the latest background summary and extracted facts.
One of 2 shapes
ConversationAnalysis
summarystring | nullrequiredstructuredobjectrequiredThe configured
channels.web_chat.analysis.fieldsthe chat stated, keyed by field key.through_seqintegerrequiredThe last message seq the run read.
human_reviewHumanReview | nullNull when no human took part.
One of 2 shapes
HumanReview
flagsarray of objectrequired3 attributes
codestringrequiredOne of
unanswered_questionspelling_grammarunprofessional_tonebroken_promisewrong_factotherleft_mid_chatslow_replyseverityintegerrequired1 to 3evidencestringrequiredup to 200 charactersA paraphrase; never contact details.
reviewed_through_seqintegerrequiredThe last message seq the model review read.
Option 2
needs_reviewbooleanhuman_review has a flag of severity 2 or more.
updated_atstringrequiredISO 8601 timestamp
Option 2
live_handoffbooleanOperator detail only: whether the org takes web chats over live. When false, takeover is refused with 409 live_handoff_disabled.
next_cursorstring | nullrequiredOpaque pagination cursor.
has_morebooleanrequiredtotalintegermin 0Conversations matching the filters across all pages. Present only when include_total=true.
live_handoffbooleanrequiredWhether the org takes web chats over live. When false, takeover is refused and handoff requests are callback requests.
- 400 Validation error: usually
invalid_request, with the failing fields inissueswhen there are any - 401 Missing, malformed, unknown or revoked API key (
unauthorized) - 429 Rate limit exceeded (
rate_limited). WaitRetry-Afterseconds. Budgets are per 60-second window: developer API 120 per key and 600 per organization; public chat 30 per IP address, except polling for updates (60 per conversation and IP address) and analytics events (120 per IP address).
curl "https://api.myleadsfactory.tech/api/conversations" \
-H "Authorization: Bearer $MLF_API_KEY"{
"data": [
{
"id": "conv_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"conversation_id": "conv_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"org_id": "org_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"project_id": "proj_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"assistant_id": "asst_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"widget_key_id": "id_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"channel": "web_chat",
"mode": "live",
"status": "open",
"ended_reason": "visitor_ended",
"resumable": true,
"handoff_requested_at": "2026-10-28T09:30:00.000Z",
"taken_over_at": "2026-10-28T09:30:00.000Z",
"taken_over_heartbeat_at": "2026-10-28T09:30:00.000Z",
"taken_over_by": "string",
"taken_over_by_name": "string",
"taken_over": true,
"first_taken_over_at": "2026-10-28T09:30:00.000Z",
"handoff_answered_at": "2026-10-28T09:30:00.000Z",
"handoff_in_hours": true,
"handoff_status": "none",
"handoff_answer_seconds": 1,
"human_outcome": "handled",
"human_metrics": {
"first_response_seconds": 1,
"longest_wait_seconds": 1,
"unanswered_count": 1,
"agent_message_count": 1
},
"needs_review": true,
"visitor_id": "id_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"parent_origin": "https://www.example.com",
"last_message_at": "2026-10-28T09:30:00.000Z",
"message_count": 1,
"lead_id": "lead_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"metadata": {},
"callback": {
"phone": "+14155550123",
"slot_start": "2026-10-28T09:30:00.000Z",
"note": "string",
"requested_at": "2026-10-28T09:30:00.000Z"
},
"ended_at": "2026-10-28T09:30:00.000Z",
"created_at": "2026-10-28T09:30:00.000Z",
"updated_at": "2026-10-28T09:30:00.000Z",
"messages": [
{
"id": "conv_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"conversation_id": "conv_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"org_id": "org_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"project_id": "proj_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"role": "user",
"content": "Hi, can I book a trip to Lisbon in May?",
"rich": {
"quick_replies": {},
"buttons": {},
"metadata": {},
"attachments": {}
},
"tool_calls": [
{}
],
"tool_call_id": "call_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"seq": 1,
"created_at": "2026-10-28T09:30:00.000Z",
"updated_at": "2026-10-28T09:30:00.000Z"
}
],
"costs": [
{}
],
"lead": {
"id": "lead_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"org_id": "org_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"project_id": "proj_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"assistant_id": "asst_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"conversation_id": "conv_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"name": "Front desk",
"email": "ana@example.com",
"phone": "+14155550123",
"fields": {
"key": {}
},
"source": "web_chat",
"mode": "live",
"outcome": "open",
"outcome_value": "string",
"outcome_currency": "string",
"outcome_at": "2026-10-28T09:30:00.000Z",
"outcome_ref": "string",
"customer_id": "id_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"attribution": {},
"created_at": "2026-10-28T09:30:00.000Z",
"updated_at": "2026-10-28T09:30:00.000Z"
},
"summary": "string",
"analysis": {
"summary": "string",
"structured": {},
"through_seq": 1,
"human_review": {},
"needs_review": true,
"updated_at": "2026-10-28T09:30:00.000Z"
},
"live_handoff": true
}
],
"next_cursor": "eyJ0IjoiMjAyNi0xMC0yOCJ9",
"has_more": true,
"total": 1,
"live_handoff": true
}Get a conversation
GET/api/conversations/{id}
The conversation with its messages, usage lines, lead and latest analysis. Tool and system rows, and assistant tool-call steps, are returned only to instance staff, org admins and owners, and API keys. Members and viewers get the transcript the visitor saw.
Secret API key. How it works
Path parameters
idstringrequiredConversation id,
conv_....
Returns
200 OK
Response attributes
dataConversationrequired41 attributes
idstringconversation_idstringAlias used by the public resume surface.
org_idstringproject_idstringassistant_idstringwidget_key_idstring | nullchannelstringOne of
web_chatwhatsappmodestringSet by the server from the key the chat came in on: test chats run the draft, preview chats send nothing out.
One of
livetestpreviewstatusstringrequiredOne of
openendedended_reasonstring | nullOne of
visitor_endedidle_timeoutmax_messageserrorresumablebooleanPublic resume surface: ended only because the visitor went quiet, so the next message reopens it.
handoff_requested_atstring | nullISO 8601 timestamptaken_over_atstring | nullISO 8601 timestamptaken_over_heartbeat_atstring | nullISO 8601 timestamptaken_over_bystring | nulltaken_over_by_namestring | nulltaken_overbooleanfirst_taken_over_atstring | nullISO 8601 timestampFirst time a human took over; unlike taken_over_at, never cleared.
handoff_answered_atstring | nullISO 8601 timestampFirst takeover at or after handoff_requested_at.
handoff_in_hoursboolean | nullWhether the bot told the visitor advisors were available when they first asked for a person, by the assistant's working hours (none configured counts as available). Null when unknown, as on requests from before 7 October 2026 without a stored answer.
handoff_statusstringmissed: the chat ended before anyone took over. abandoned: a human took over and then left the visitor waiting; it takes precedence over the others.
One of
nonependingansweredmissedabandonedhandoff_answer_secondsinteger | nullhuman_outcomestring | nullNull when no human took part. handled: every visitor message to the human got a human reply. abandoned: at least one did not, before the bot replied, the chat ended, or (chat still open) 5 minutes passed. Recomputed when a seat is released, when the chat ends, and on each analysis run.
One of
handledabandonedhuman_metricsHumanMetrics | nullOne of 2 shapes
HumanMetrics
first_response_secondsinteger | nullrequiredFrom the first takeover to the first human message.
longest_wait_secondsinteger | nullrequiredThe longest a visitor waited on the human, answered or not.
unanswered_countintegerrequiredVisitor messages to the human that no human answered.
agent_message_countintegerrequired
Option 2
needs_reviewbooleanOperator list only: the analysis flagged the human advisor's replies with severity 2 or more.
visitor_idstring | nullparent_originstring | nulllast_message_atstring | nullISO 8601 timestampmessage_countintegerlead_idstring | nullmetadataobject | nullcallbackobject | nullOne of 2 shapes
Option 1
phonestringrequiredslot_startstringISO 8601 timestampnotestringrequested_atstringrequiredISO 8601 timestamp
Option 2
ended_atstring | nullISO 8601 timestampcreated_atstringISO 8601 timestampupdated_atstringISO 8601 timestampmessagesarray of ConversationMessage12 attributes
idstringrequiredconversation_idstringorg_idstringproject_idstringrolestringrequiredOne of
userassistantagenttoolsystemcontentstringrequiredrichRichContent | null4 attributes
quick_repliesarray of object2 attributes
labelstringrequiredvaluestringrequired
buttonsarray of object3 attributes
typestringrequiredOne of
linklabelstringrequiredurlstringrequireduri
metadataobject3 attributes
pageobjectVisitor messages from the web widget: the page they sent the message from.
2 attributes
pathstringrequiredtitlestringrequired
channelstringA turn spoken in a voice session of this conversation rather than typed.
One of
voicevoice_session_idstringThe voice session the turn was spoken in.
attachmentsarray of ConversationAttachmentFiles sent with the message. Each
urlis a signed link that works without credentials.5 attributes
idstringrequirednamestringrequiredcontent_typestringrequiredOne of
image/jpegimage/pngimage/webpimage/gifapplication/pdfapplication/vnd.openxmlformats-officedocument.wordprocessingml.documentsizeintegerrequiredBytes.
urlstringrequireduriSigned download link (GET /public/chat/attachments/{id}), valid for 24 hours (7 days in webhooks).
tool_callsarray of object | nulltool_call_idstring | nullseqintegercreated_atstringrequiredISO 8601 timestampupdated_atstringISO 8601 timestamp
costsarray of objectleadLead | nullOne of 2 shapes
Lead
idstringrequiredorg_idstringrequiredproject_idstringrequiredassistant_idstringrequiredconversation_idstring | nullnamestring | nullemailstring | nullemailphonestring | nullfieldsobject | nullsourcestringrequiredOne of
web_chatwhatsappmodestringThe mode of the chat that captured it: test and preview leads are left out of lists unless asked for.
One of
livetestpreviewoutcomestring | nullWhat became of the lead, as your CRM reports it (PUT /leads/{id}/outcome).
One of
openbookedlostoutcome_valuestring | nullBooking value as a decimal string.
outcome_currencystring | nulloutcome_atstring | nullISO 8601 timestampoutcome_refstring | nullThe CRM's own id for the booking.
customer_idstring | nullThe customer's own id for this person, set with PUT /v1/leads/{id}/outcome.
attributionChatAttribution | nullWhere the lead's chat came from. Returned by the list and detail reads; null when none was recorded.
One of 2 shapes
ChatAttribution
chat_pagestringThe path the visitor started chatting on.
visitChatAttributionTouchThis visit's landing.
12 attributes
landing_pagestringA path starting with /, without query or fragment.
referrer_hoststringutm_sourcestringutm_mediumstringutm_campaignstringutm_termstringutm_contentstringgclidstringgbraidstringwbraidstringfbclidstringmsclkidstring
firstobjectThe visitor's first-ever landing.
13 attributes
landing_pagestringA path starting with /, without query or fragment.
referrer_hoststringutm_sourcestringutm_mediumstringutm_campaignstringutm_termstringutm_contentstringgclidstringgbraidstringwbraidstringfbclidstringmsclkidstringatstringISO 8601 timestampWhen the visitor first landed.
Option 2
created_atstringISO 8601 timestampupdated_atstringISO 8601 timestamp
Option 2
summarystring | nullOperator list only: the latest background summary, when analysis is on.
analysisConversationAnalysis | nullOperator detail only: the latest background summary and extracted facts.
One of 2 shapes
ConversationAnalysis
summarystring | nullrequiredstructuredobjectrequiredThe configured
channels.web_chat.analysis.fieldsthe chat stated, keyed by field key.through_seqintegerrequiredThe last message seq the run read.
human_reviewHumanReview | nullNull when no human took part.
One of 2 shapes
HumanReview
flagsarray of objectrequired3 attributes
codestringrequiredOne of
unanswered_questionspelling_grammarunprofessional_tonebroken_promisewrong_factotherleft_mid_chatslow_replyseverityintegerrequired1 to 3evidencestringrequiredup to 200 charactersA paraphrase; never contact details.
reviewed_through_seqintegerrequiredThe last message seq the model review read.
Option 2
needs_reviewbooleanhuman_review has a flag of severity 2 or more.
updated_atstringrequiredISO 8601 timestamp
Option 2
live_handoffbooleanOperator detail only: whether the org takes web chats over live. When false, takeover is refused with 409 live_handoff_disabled.
- 401 Missing, malformed, unknown or revoked API key (
unauthorized) - 404 Not found, or outside what the key can see
- 429 Rate limit exceeded (
rate_limited). WaitRetry-Afterseconds. Budgets are per 60-second window: developer API 120 per key and 600 per organization; public chat 30 per IP address, except polling for updates (60 per conversation and IP address) and analytics events (120 per IP address).
curl "https://api.myleadsfactory.tech/api/conversations/CONVERSATION_ID" \
-H "Authorization: Bearer $MLF_API_KEY"{
"data": {
"id": "conv_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"conversation_id": "conv_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"org_id": "org_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"project_id": "proj_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"assistant_id": "asst_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"widget_key_id": "id_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"channel": "web_chat",
"mode": "live",
"status": "open",
"ended_reason": "visitor_ended",
"resumable": true,
"handoff_requested_at": "2026-10-28T09:30:00.000Z",
"taken_over_at": "2026-10-28T09:30:00.000Z",
"taken_over_heartbeat_at": "2026-10-28T09:30:00.000Z",
"taken_over_by": "string",
"taken_over_by_name": "string",
"taken_over": true,
"first_taken_over_at": "2026-10-28T09:30:00.000Z",
"handoff_answered_at": "2026-10-28T09:30:00.000Z",
"handoff_in_hours": true,
"handoff_status": "none",
"handoff_answer_seconds": 1,
"human_outcome": "handled",
"human_metrics": {
"first_response_seconds": 1,
"longest_wait_seconds": 1,
"unanswered_count": 1,
"agent_message_count": 1
},
"needs_review": true,
"visitor_id": "id_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"parent_origin": "https://www.example.com",
"last_message_at": "2026-10-28T09:30:00.000Z",
"message_count": 1,
"lead_id": "lead_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"metadata": {},
"callback": {
"phone": "+14155550123",
"slot_start": "2026-10-28T09:30:00.000Z",
"note": "string",
"requested_at": "2026-10-28T09:30:00.000Z"
},
"ended_at": "2026-10-28T09:30:00.000Z",
"created_at": "2026-10-28T09:30:00.000Z",
"updated_at": "2026-10-28T09:30:00.000Z",
"messages": [
{
"id": "conv_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"conversation_id": "conv_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"org_id": "org_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"project_id": "proj_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"role": "user",
"content": "Hi, can I book a trip to Lisbon in May?",
"rich": {
"quick_replies": [
{}
],
"buttons": [
{}
],
"metadata": {
"page": {},
"channel": "voice",
"voice_session_id": {}
},
"attachments": [
{}
]
},
"tool_calls": [
{}
],
"tool_call_id": "call_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"seq": 1,
"created_at": "2026-10-28T09:30:00.000Z",
"updated_at": "2026-10-28T09:30:00.000Z"
}
],
"costs": [
{}
],
"lead": {
"id": "lead_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"org_id": "org_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"project_id": "proj_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"assistant_id": "asst_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"conversation_id": "conv_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"name": "Front desk",
"email": "ana@example.com",
"phone": "+14155550123",
"fields": {
"key": "string"
},
"source": "web_chat",
"mode": "live",
"outcome": "open",
"outcome_value": "string",
"outcome_currency": "string",
"outcome_at": "2026-10-28T09:30:00.000Z",
"outcome_ref": "string",
"customer_id": "id_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"attribution": {
"chat_page": {},
"visit": {},
"first": {}
},
"created_at": "2026-10-28T09:30:00.000Z",
"updated_at": "2026-10-28T09:30:00.000Z"
},
"summary": "string",
"analysis": {
"summary": "string",
"structured": {},
"through_seq": 1,
"human_review": {
"flags": {},
"reviewed_through_seq": {}
},
"needs_review": true,
"updated_at": "2026-10-28T09:30:00.000Z"
},
"live_handoff": true
}
}The visits this conversation's visitor made to the site
GET/api/conversations/{id}/journey
Up to 20 recent visits, newest first, across the organization's projects, by the conversation's visitor id and by the visitor ids of other conversations in the same organization whose lead shares its email, phone number (digits only, at least 7) or customer id (at most 10 visitors, listed in visitors); test traffic excluded. A conversation without a visitor id (no analytics consent, or an older widget) and without linked chats returns visitors and visits empty.
Secret API key. How it works
Path parameters
idstringrequired
Returns
200 OK
Response attributes
dataVisitorJourneyrequired4 attributes
visitor_idstring | nullrequiredvisitorsarray of objectrequiredThe visitor ids whose visits are listed: this conversation's own, plus those of conversations in the same organization whose lead shares an email, phone number or customer id. At most 10.
3 attributes
visitor_idstringrequiredlinked_bystringrequiredOne of
this_chatemailphonecustomer_idconversation_idsarray of stringrequired
visitsarray of objectrequired14 attributes
visit_idstringrequiredvisitor_idstringrequiredsitestring | nullrequiredThe site of the visit's earliest event.
visit_numberinteger | nullrequiredstarted_atstringrequiredISO 8601 timestamplast_atstringrequiredISO 8601 timestamplanding_pagestring | nullrequiredsourcestringrequiredutm_source, else the referrer host, else direct.
page_viewsintegerrequiredpagesarray of stringrequiredThe first 10 distinct pages, in the order visited.
opened_reasonstring | nullrequiredOne of
proactivevisitorapisentbooleanrequiredconversation_idsarray of stringrequiredis_this_conversationbooleanrequired
truncatedbooleanrequiredTrue when the visitor has more than 20 visits.
- 401 Missing, malformed, unknown or revoked API key (
unauthorized) - 404 Not found, or outside what the key can see
- 429 Rate limit exceeded (
rate_limited). WaitRetry-Afterseconds. Budgets are per 60-second window: developer API 120 per key and 600 per organization; public chat 30 per IP address, except polling for updates (60 per conversation and IP address) and analytics events (120 per IP address).
curl "https://api.myleadsfactory.tech/api/conversations/CONVERSATION_ID/journey" \
-H "Authorization: Bearer $MLF_API_KEY"{
"data": {
"visitor_id": "id_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"visitors": [
{
"visitor_id": "id_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"linked_by": "this_chat",
"conversation_ids": [
"conv_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f"
]
}
],
"visits": [
{
"visit_id": "id_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"visitor_id": "id_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"site": "string",
"visit_number": 1,
"started_at": "2026-10-28T09:30:00.000Z",
"last_at": "2026-10-28T09:30:00.000Z",
"landing_page": "string",
"source": "string",
"page_views": 1,
"pages": [
"string"
],
"opened_reason": "proactive",
"sent": true,
"conversation_ids": [
"conv_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f"
],
"is_this_conversation": true
}
],
"truncated": true
}
}Send a message as a person
POST/api/conversations/{id}/messages
Only the operator holding the seat may send (403 otherwise). With client_message_id, a retry by the same operator returns the original message instead of sending it again; reusing the id for different content is 409 client_message_id_reused.
Secret API key, not read-only. How it works
Path parameters
idstringrequiredConversation id,
conv_....
Request body
contentstringrequiredup to 4000 charactersagentAgentIdentityInput3 attributes
idstringemailstringemailnamestring
client_message_idstringup to 200 charactersUnique per message from this operator; makes a retried send safe.
Returns
201 OK
Response attributes
dataConversationMessagerequired12 attributes
idstringrequiredconversation_idstringorg_idstringproject_idstringrolestringrequiredOne of
userassistantagenttoolsystemcontentstringrequiredrichRichContent | null4 attributes
quick_repliesarray of object2 attributes
labelstringrequiredvaluestringrequired
buttonsarray of object3 attributes
typestringrequiredOne of
linklabelstringrequiredurlstringrequireduri
metadataobject3 attributes
pageobjectVisitor messages from the web widget: the page they sent the message from.
2 attributes
pathstringrequiredtitlestringrequired
channelstringA turn spoken in a voice session of this conversation rather than typed.
One of
voicevoice_session_idstringThe voice session the turn was spoken in.
attachmentsarray of ConversationAttachmentFiles sent with the message. Each
urlis a signed link that works without credentials.5 attributes
idstringrequirednamestringrequiredcontent_typestringrequiredOne of
image/jpegimage/pngimage/webpimage/gifapplication/pdfapplication/vnd.openxmlformats-officedocument.wordprocessingml.documentsizeintegerrequiredBytes.
urlstringrequireduriSigned download link (GET /public/chat/attachments/{id}), valid for 24 hours (7 days in webhooks).
tool_callsarray of object | nulltool_call_idstring | nullseqintegercreated_atstringrequiredISO 8601 timestampupdated_atstringISO 8601 timestamp
- 400 Validation error: usually
invalid_request, with the failing fields inissueswhen there are any - 401 Missing, malformed, unknown or revoked API key (
unauthorized) - 403 Refused, for example a read-only key on a write (
read_only_key), a role too low for the route (insufficient_role), or a route that needs a console session - 404 Not found, or outside what the key can see
- 409 Conflict
- 429 Rate limit exceeded (
rate_limited). WaitRetry-Afterseconds. Budgets are per 60-second window: developer API 120 per key and 600 per organization; public chat 30 per IP address, except polling for updates (60 per conversation and IP address) and analytics events (120 per IP address).
curl -X POST "https://api.myleadsfactory.tech/api/conversations/CONVERSATION_ID/messages" \
-H "Authorization: Bearer $MLF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"content": "Hi Ana, this is Alex from Acme Travel. Happy to help with Lisbon.",
"agent": {
"id": "operator_1"
}
}'{
"data": {
"id": "conv_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"conversation_id": "conv_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"org_id": "org_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"project_id": "proj_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"role": "user",
"content": "Hi, can I book a trip to Lisbon in May?",
"rich": {
"quick_replies": [
{
"label": "string",
"value": "string"
}
],
"buttons": [
{
"type": "link",
"label": "string",
"url": "https://example.com/hook"
}
],
"metadata": {
"page": {
"path": "string",
"title": "Front desk"
},
"channel": "voice",
"voice_session_id": "id_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f"
},
"attachments": [
{
"id": "conv_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"name": "Front desk",
"content_type": "image/jpeg",
"size": 1,
"url": "https://example.com/hook"
}
]
},
"tool_calls": [
{}
],
"tool_call_id": "call_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"seq": 1,
"created_at": "2026-10-28T09:30:00.000Z",
"updated_at": "2026-10-28T09:30:00.000Z"
}
}Hand a conversation back to the assistant
POST/api/conversations/{id}/release
The holder releases their own seat; org admins and API keys may release anyone's, which is recorded as a forced release naming the previous holder. Send the same agent you took over with, so the release is recorded as yours rather than forced. Release still works when the org has turned live handoff off.
Secret API key, not read-only. How it works
Path parameters
idstringrequiredConversation id,
conv_....
Request body
agentAgentIdentityInput3 attributes
idstringemailstringemailnamestring
Returns
200 OK
Response attributes
dataConversationTakeoverrequired5 attributes
idstringrequiredstatusstringrequiredOne of
openendedtaken_over_atstring | nullrequiredISO 8601 timestamptaken_over_heartbeat_atstring | nullrequiredISO 8601 timestamptaken_over_bystring | nullrequired
- 401 Missing, malformed, unknown or revoked API key (
unauthorized) - 403 Refused, for example a read-only key on a write (
read_only_key), a role too low for the route (insufficient_role), or a route that needs a console session - 404 Not found, or outside what the key can see
- 409 Conflict
- 429 Rate limit exceeded (
rate_limited). WaitRetry-Afterseconds. Budgets are per 60-second window: developer API 120 per key and 600 per organization; public chat 30 per IP address, except polling for updates (60 per conversation and IP address) and analytics events (120 per IP address).
curl -X POST "https://api.myleadsfactory.tech/api/conversations/CONVERSATION_ID/release" \
-H "Authorization: Bearer $MLF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agent": {
"id": "id_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"email": "ana@example.com"
}
}'{
"data": {
"id": "conv_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"status": "open",
"taken_over_at": "2026-10-28T09:30:00.000Z",
"taken_over_heartbeat_at": "2026-10-28T09:30:00.000Z",
"taken_over_by": "string"
}
}Take over a conversation
POST/api/conversations/{id}/takeover
Only one operator holds a seat: when another already does, 409 already_taken_over names them in taken_over_by. Taking over a seat you hold refreshes it. 409 live_handoff_disabled when the org does not take web chats over live. Heartbeat and operator messages refuse the same way. With an API key, name the person in agent (otherwise the holder is api_key) and send the same agent on every message, heartbeat and release. A seat with no heartbeat for three minutes is released.
Secret API key, not read-only. How it works
Path parameters
idstringrequiredConversation id,
conv_....
Request body
agentAgentIdentityInput3 attributes
idstringemailstringemailnamestring
Returns
200 OK
Response attributes
dataConversationTakeoverrequired5 attributes
idstringrequiredstatusstringrequiredOne of
openendedtaken_over_atstring | nullrequiredISO 8601 timestamptaken_over_heartbeat_atstring | nullrequiredISO 8601 timestamptaken_over_bystring | nullrequired
- 401 Missing, malformed, unknown or revoked API key (
unauthorized) - 403 Refused, for example a read-only key on a write (
read_only_key), a role too low for the route (insufficient_role), or a route that needs a console session - 404 Not found, or outside what the key can see
- 409 Conflict
- 429 Rate limit exceeded (
rate_limited). WaitRetry-Afterseconds. Budgets are per 60-second window: developer API 120 per key and 600 per organization; public chat 30 per IP address, except polling for updates (60 per conversation and IP address) and analytics events (120 per IP address).
curl -X POST "https://api.myleadsfactory.tech/api/conversations/CONVERSATION_ID/takeover" \
-H "Authorization: Bearer $MLF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agent": {
"id": "operator_1",
"name": "Alex"
}
}'{
"data": {
"id": "conv_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"status": "open",
"taken_over_at": "2026-10-28T09:30:00.000Z",
"taken_over_heartbeat_at": "2026-10-28T09:30:00.000Z",
"taken_over_by": "string"
}
}Keep a takeover alive
POST/api/conversations/{id}/takeover/heartbeat
Call at least once a minute while a person is working the chat. A seat with no heartbeat for three minutes is released and the assistant answers again. Send the same agent you took over with.
Secret API key, not read-only. How it works
Path parameters
idstringrequiredConversation id,
conv_....
Request body
agentAgentIdentityInput3 attributes
idstringemailstringemailnamestring
Returns
200 OK
Response attributes
dataConversationTakeoverHeartbeatrequired1 attribute
taken_over_heartbeat_atstringrequiredISO 8601 timestamp
- 401 Missing, malformed, unknown or revoked API key (
unauthorized) - 403 Refused, for example a read-only key on a write (
read_only_key), a role too low for the route (insufficient_role), or a route that needs a console session - 404 Not found, or outside what the key can see
- 409 Conflict
- 429 Rate limit exceeded (
rate_limited). WaitRetry-Afterseconds. Budgets are per 60-second window: developer API 120 per key and 600 per organization; public chat 30 per IP address, except polling for updates (60 per conversation and IP address) and analytics events (120 per IP address).
curl -X POST "https://api.myleadsfactory.tech/api/conversations/CONVERSATION_ID/takeover/heartbeat" \
-H "Authorization: Bearer $MLF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"agent": {
"id": "id_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"email": "ana@example.com"
}
}'{
"data": {
"taken_over_heartbeat_at": "2026-10-28T09:30:00.000Z"
}
}Summarize human handoffs
GET/api/conversations/handoff-summary
Conversations created in the range (inclusive), test conversations excluded unless mode says otherwise. answer_seconds runs from the handoff request to the first takeover. first_response_seconds and longest_wait_seconds are taken over the conversations with human_metrics.
Secret API key. How it works
Query parameters
project_idstringOnly this project's conversations.
assistant_idarray of stringOnly these assistants' conversations. Repeat the parameter or comma-separate the values; a row matching any of them is kept.
fromstringISO 8601 timestampCreated at or after this time.
tostringISO 8601 timestampCreated at or before this time.
modestringWhich chats to include, by the mode the server gave each. Absent: test and preview chats are left out, and so are live chats a page marked as a test, as before. live: every live-mode chat, including those a page marked as a test. test: test-mode chats. preview: preview chats. all: every chat.
One of
livetestpreviewall
Returns
200 OK
Response attributes
dataConversationHandoffSummaryrequired18 attributes
conversationsintegerrequiredrequestedintegerrequiredpendingintegerrequiredansweredintegerrequiredmissedintegerrequiredrequested_in_hoursintegerrequiredRequests made while advisors were said to be available.
requested_out_of_hoursintegerrequiredRequests made outside working hours, when the bot offers a callback instead.
missed_in_hoursintegerrequiredmissed_out_of_hoursintegerrequiredtaken_overintegerrequiredConversations a human took over, requested or not.
abandonedintegerrequiredConversations a human took over and then left the visitor waiting.
handledintegerrequiredConversations a human took part in where every visitor message to them got a human reply.
unanswered_messagesintegerrequiredVisitor messages to a human that no human answered, summed.
callbacksintegerrequiredConversations where the visitor left a callback request.
live_handoffbooleanrequiredWhether the org takes web chats over live. When false, requests for a person are callback requests only.
answer_secondsSecondsPercentilesrequired2 attributes
medianinteger | nullrequiredp90integer | nullrequired
first_response_secondsSecondsPercentilesrequiredFrom the first takeover to the first human message.
2 attributes
medianinteger | nullrequiredp90integer | nullrequired
longest_wait_secondsSecondsPercentilesrequiredEach conversation's longest wait for the human, answered or not.
2 attributes
medianinteger | nullrequiredp90integer | nullrequired
- 400 Validation error: usually
invalid_request, with the failing fields inissueswhen there are any - 401 Missing, malformed, unknown or revoked API key (
unauthorized) - 429 Rate limit exceeded (
rate_limited). WaitRetry-Afterseconds. Budgets are per 60-second window: developer API 120 per key and 600 per organization; public chat 30 per IP address, except polling for updates (60 per conversation and IP address) and analytics events (120 per IP address).
curl "https://api.myleadsfactory.tech/api/conversations/handoff-summary" \
-H "Authorization: Bearer $MLF_API_KEY"{
"data": {
"conversations": 1,
"requested": 1,
"pending": 1,
"answered": 1,
"missed": 1,
"requested_in_hours": 1,
"requested_out_of_hours": 1,
"missed_in_hours": 1,
"missed_out_of_hours": 1,
"taken_over": 1,
"abandoned": 1,
"handled": 1,
"unanswered_messages": 1,
"callbacks": 1,
"live_handoff": true,
"answer_seconds": {
"median": 1,
"p90": 1
},
"first_response_seconds": {
"median": 1,
"p90": 1
},
"longest_wait_seconds": {
"median": 1,
"p90": 1
}
}
}Count a project's test data
GET/api/test-data
What POST /v1/test-data/clear would delete now: test-mode and preview chats, their files, the leads they captured, and test telemetry. An estimate, as tests may start meanwhile.
Secret API key. How it works
Query parameters
project_idstringrequired
Returns
200 OK
Response attributes
dataobjectrequired6 attributes
project_idstringrequiredconversationsintegerrequiredTest and preview chats there are now.
leadsintegerrequiredTest and preview leads.
eventsintegerrequiredChat events and request outcomes.
attachmentsintegerrequiredUploaded files.
callsintegerFinished test calls (with their costs, analysis and recording rows).
- 400 Validation error: usually
invalid_request, with the failing fields inissueswhen there are any - 401 Missing, malformed, unknown or revoked API key (
unauthorized) - 403 Refused, for example a read-only key on a write (
read_only_key), a role too low for the route (insufficient_role), or a route that needs a console session - 404 Not found, or outside what the key can see
- 429 Rate limit exceeded (
rate_limited). WaitRetry-Afterseconds. Budgets are per 60-second window: developer API 120 per key and 600 per organization; public chat 30 per IP address, except polling for updates (60 per conversation and IP address) and analytics events (120 per IP address).
curl "https://api.myleadsfactory.tech/api/test-data?project_id=proj_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f" \
-H "Authorization: Bearer $MLF_API_KEY"{
"data": {
"project_id": "proj_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"conversations": 1,
"leads": 1,
"events": 1,
"attachments": 1,
"calls": 1
}
}Clear a project's test and preview chats and leads
POST/api/test-data/clear
Deletes the project's test-mode and preview chats started before the request (with their messages, files, analysis, telemetry and webhook events) and the leads they captured. Live chats are never touched, nor are live chats whose page only claimed to be a test. Sends no webhooks. Returns what it deleted.
Secret API key, not read-only. How it works
Request body
project_idstringrequired
Returns
200 OK
Response attributes
dataobjectrequired7 attributes
project_idstringrequiredconversationsintegerrequiredTest and preview chats deleted.
leadsintegerrequiredTest and preview leads.
eventsintegerrequiredChat events and request outcomes.
attachmentsintegerrequiredUploaded files.
callsintegerFinished test calls (with their costs, analysis and recording rows).
attachment_failuresintegerFiles whose delete failed (logged).
- 400 Validation error: usually
invalid_request, with the failing fields inissueswhen there are any - 401 Missing, malformed, unknown or revoked API key (
unauthorized) - 403 Refused, for example a read-only key on a write (
read_only_key), a role too low for the route (insufficient_role), or a route that needs a console session - 404 Not found, or outside what the key can see
- 429 Rate limit exceeded (
rate_limited). WaitRetry-Afterseconds. Budgets are per 60-second window: developer API 120 per key and 600 per organization; public chat 30 per IP address, except polling for updates (60 per conversation and IP address) and analytics events (120 per IP address).
curl -X POST "https://api.myleadsfactory.tech/api/test-data/clear" \
-H "Authorization: Bearer $MLF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"project_id": "proj_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f"
}'{
"data": {
"project_id": "proj_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"conversations": 1,
"leads": 1,
"events": 1,
"attachments": 1,
"calls": 1,
"attachment_failures": 1
}
}