API reference
Public chat API
The versioned API for your own chat interface, in a page or an app: start a conversation with a publishable key, then send messages with the conversation token it returns. It runs the same conversations as the chat widget.
- GET
/public/v1/configGet the widget configuration - POST
/public/v1/conversationsStart a conversation - POST
/public/v1/conversations/{id}/messagesSend a message and stream the reply - GET
/public/v1/conversations/{id}/updatesPoll for new messages - GET
/public/v1/conversations/{id}Get a conversation's visible history - POST
/public/v1/conversations/{id}/attachmentsUpload a file for a message - GET
/public/chat/attachments/{id}Download an attachment - POST
/public/v1/conversations/{id}/endEnd a conversation - POST
/public/v1/conversations/{id}/messages/{messageId}/feedbackRate an assistant reply - POST
/public/v1/conversations/{id}/ratingRate the conversation - POST
/public/v1/conversations/{id}/presenceReport the visitor is still here - POST
/public/v1/conversations/{id}/consentUpdate the visitor's analytics consent - POST
/public/v1/telemetryReport chat analytics events - POST
/public/v1/conversations/{id}/voiceStart a voice session - GET
/public/v1/conversations/{id}/voice/{voiceSessionId}Get a voice session - POST
/public/v1/conversations/{id}/voice/{voiceSessionId}/endEnd a voice session
Get the widget configuration
GET/public/v1/config
What a chat interface needs before the first message: display name, greeting, quick replies, appearance, proactive rules, text overrides and (when on) attachment limits. Safe to call from a browser and cached for 60 seconds. When the assistant lists allowed_origins, a request whose Origin is not on the list is refused with 403.
Publishable key (pk_live_... or pk_test_...), from an origin it allows. How it works
Query parameters
keystringrequiredPublishable key,
pk_live_....assistantstringRequired with a project-scoped publishable key; ignored-if-matching for assistant-pinned keys.
Returns
200 OK
Response attributes
dataPublicWidgetConfigrequiredThe assistant's public web chat settings, as GET /public/v1/config (the widget's GET /public/chat/widget-config) and conversation start return them. Never includes instructions, model settings or secrets.
19 attributes
namestringrequiredDisplay name:
appearance.display_name, or the assistant's name.greetingstringquick_repliesarray of stringhero_topicsarray of stringTopic buttons shown before the first message.
appearanceobjectColours, position, display name and avatar.
proactiveobjectWhen and where to open a teaser on its own.
stringsobjectInterface text overrides.
callobject | nullA phone number to offer in the header, when set.
handoff_messagestring | nullWhat to show when the visitor asks for a person.
powered_byanyfeedbackobjectWhether to ask for reply feedback and a conversation rating.
attachmentsobjectPresent only when the assistant takes files and this deployment can store them.
4 attributes
enabledbooleanOne of
truemax_bytesintegermax_filesintegerPer message.
acceptarray of stringAccepted content types.
analyticsobjectOff for test and preview chats, so they never reach your analytics.
4 attributes
data_layerbooleangtagbooleanevent_prefixstringrequire_consentboolean
identity_scopestringrequiredAn opaque id for the project, the same for every key in it. Use it to name where you keep a visitor id, so a rotated or second key keeps the same visitors.
modestringPresent only for a test key or a preview link; a live key's config has no
mode.One of
testpreviewuiobjectPresent only when the assistant uses the quiet chat style.
1 attribute
stylestringOne of
quiet
dynamic_uiobjectPresent only when the assistant shows contact details as links.
1 attribute
contact_linksbooleanOne of
true
lead_capturePublicLeadSchemaThe lead details the chat asks for, so your page can show what it has captured. Present only when lead capture and
dynamic_ui.lead_panelare both on.2 attributes
require_completebooleanrequiredNo lead is saved until every required field and an email or phone are in.
fieldsarray of objectrequired4 attributes
keystringrequiredlabelstringrequiredThe field's short name for people to read.
typestringrequiredOne of
stringemailphonerequiredbooleanrequired
voiceobjectPresent only when the assistant takes voice in the browser and voice calls can be placed: offer a microphone button (POST /public/v1/conversations/{id}/voice).
2 attributes
enabledbooleanrequiredOne of
truemax_secondsintegerrequiredEach voice session's length cap.
- 403
origin_not_allowed: the request'sOriginis not in the assistant'sallowed_origins - 404
not_found: unknown or revoked key, no such assistant for it, or web chat is off for the assistant - 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/public/v1/config?key=pk_live_..."{
"data": {
"name": "Front desk",
"greeting": "Hi! Planning a trip? I can help.",
"quick_replies": [
"Lisbon in May",
"Group bookings"
],
"hero_topics": [],
"appearance": {
"position": "bottom-right",
"header_style": "accent"
},
"proactive": {
"enabled": false,
"url_rules": []
},
"handoff_message": null,
"powered_by": {
"enabled": true,
"label": "Powered by MyLeadsFactory",
"url": "https://www.myleadsfactory.com"
},
"feedback": {
"replies": false,
"rating": false
},
"analytics": {
"data_layer": false,
"gtag": false,
"event_prefix": "chat_",
"require_consent": false
}
}
}Start a conversation
POST/public/v1/conversations
Starts a conversation with the assistant behind a publishable key and returns its id and token. Send the token as Authorization: Bearer ct_... on every later call for this conversation; it is returned only here. The widget starts a conversation when the visitor sends a first message, not when the panel opens. A browser page calling from its own site is checked by its Origin header against the assistant's allowed_origins (an empty list allows every site).
Publishable key (pk_live_... or pk_test_...), from an origin it allows. How it works
Request body
widget_keystringrequirednot emptyPublishable key,
pk_live_....assistantstringnot emptyAssistant id; required with a project-scoped publishable key.
parent_originstringThe site the chat runs on, for requests that carry no
Originheader of their own (your server, for example) and for the widget's own frame. A browser calling from another site is checked by its realOriginheader instead, and this field is ignored, so a page cannot borrow an allowed site's name.visitor_idstringYour stable id for the visitor, so their conversations can be grouped. Stored only when analytics consent is granted or not required.
visit_idstringup to 256 charactersThe visit this conversation started in. Kept in metadata.visit_id only when analytics consent is granted or not required, and only when it is 1 to 128 letters, digits,
_or-; any other string up to 256 characters is ignored, and a longer one answers 400.timezonestringup to 64 charactersThe visitor's IANA time zone (for example
America/New_York), so dates and callback times are theirs. A recognised zone is stored as metadata.visitor_timezone; an unknown one is ignored.attributionChatAttributionWhere the visitor came from. Sanitized, never a reason to refuse the chat. Only chat_page is kept without analytics consent.
3 attributes
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.
metadataobjectFree-form context stored on the conversation and sent in its webhooks (for example a region or campaign). The
previewandattributionkeys are reserved and dropped; send attribution inattribution.client_message_idstringup to 128 charactersThe id of the first message you are about to send, so this request is linked to it in chat analytics.
page_view_idstringup to 128 charactersThe page view the chat started on, as reported to POST /public/v1/telemetry.
consentstringThe visitor's analytics consent, which decides whether analytics ids are stored.
One of
granteddeniednot_requiredpreamblearray of objectup to 5 itemsAssistant lines your interface already showed (a greeting or proactive teaser), stored first so the transcript reads as the visitor saw it.
1 attribute
contentstringrequiredup to 2000 characters
Returns
201 Conversation created
Response attributes
dataobjectrequired5 attributes
conversation_idstringrequiredtokenstringrequiredConversation token,
ct_...: send it asAuthorization: Bearer ct_...on this conversation's routes. Returned once; valid for at most 30 days (less when a test key behind it is revoked, or a preview link turned off or expired).statusstringrequiredOne of
openmessagesarray of PublicMessagerequiredAny
preamblelines you sent, stored as the assistant's first messages.6 attributes
idstringrequiredmsg_...rolestringrequiredOne of
userassistantagentcontentstringrequiredrichRichContent | nullrequired4 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).
seqintegerrequiredPosition in the conversation; pass the highest you have as
after_seqwhen polling.created_atstringrequiredISO 8601 timestamp
configPublicWidgetConfigrequiredThe assistant's public web chat settings, as GET /public/v1/config (the widget's GET /public/chat/widget-config) and conversation start return them. Never includes instructions, model settings or secrets.
19 attributes
namestringrequiredDisplay name:
appearance.display_name, or the assistant's name.greetingstringquick_repliesarray of stringhero_topicsarray of stringTopic buttons shown before the first message.
appearanceobjectColours, position, display name and avatar.
proactiveobjectWhen and where to open a teaser on its own.
stringsobjectInterface text overrides.
callobject | nullA phone number to offer in the header, when set.
handoff_messagestring | nullWhat to show when the visitor asks for a person.
powered_byanyfeedbackobjectWhether to ask for reply feedback and a conversation rating.
attachmentsobjectPresent only when the assistant takes files and this deployment can store them.
4 attributes
enabledbooleanOne of
truemax_bytesintegermax_filesintegerPer message.
acceptarray of stringAccepted content types.
analyticsobjectOff for test and preview chats, so they never reach your analytics.
4 attributes
data_layerbooleangtagbooleanevent_prefixstringrequire_consentboolean
identity_scopestringrequiredAn opaque id for the project, the same for every key in it. Use it to name where you keep a visitor id, so a rotated or second key keeps the same visitors.
modestringPresent only for a test key or a preview link; a live key's config has no
mode.One of
testpreviewuiobjectPresent only when the assistant uses the quiet chat style.
1 attribute
stylestringOne of
quiet
dynamic_uiobjectPresent only when the assistant shows contact details as links.
1 attribute
contact_linksbooleanOne of
true
lead_capturePublicLeadSchemaThe lead details the chat asks for, so your page can show what it has captured. Present only when lead capture and
dynamic_ui.lead_panelare both on.2 attributes
require_completebooleanrequiredNo lead is saved until every required field and an email or phone are in.
fieldsarray of objectrequired4 attributes
keystringrequiredlabelstringrequiredThe field's short name for people to read.
typestringrequiredOne of
stringemailphonerequiredbooleanrequired
voiceobjectPresent only when the assistant takes voice in the browser and voice calls can be placed: offer a microphone button (POST /public/v1/conversations/{id}/voice).
2 attributes
enabledbooleanrequiredOne of
truemax_secondsintegerrequiredEach voice session's length cap.
- 400 Validation error: usually
invalid_request, with the failing fields inissueswhen there are any - 403
origin_not_allowed: the calling site is not in the assistant'sallowed_origins(seeparent_originfor which site that is) - 404
not_found: unknown or revoked key, no such assistant for it, or web chat is off for the assistant - 429
key_quota_exceeded: the publishable key has used a budget shared by every visitor of your site: 300 new conversations an hour, 30 an hour from one IP address, or 5,000 assistant replies a day.rate_limitedis the ordinary per-address limit. Both carryRetry-After, in seconds.
curl -X POST "https://api.myleadsfactory.tech/public/v1/conversations" \
-H "Origin: https://www.example.com" \
-H "Content-Type: application/json" \
-d '{
"widget_key": "pk_live_4b1f0c9e2a7d4e6b8c3f5a1d9e0b7c62"
}'{
"data": {
"conversation_id": "conv_0c8e5f2a9b7d4c1e86f3a0d2b4c6e8f1",
"token": "ct_...",
"status": "open",
"messages": [],
"config": {
"name": "Front desk"
}
}
}Send a message and stream the reply
POST/public/v1/conversations/{id}/messages
Sends the visitor's message and streams the reply as server-sent events (see ChatSseEvents): usually message.accepted, then message.delta chunks and any tool.status, always ending with exactly one message.completed or error. A 200 status does not mean the turn succeeded: read to the terminal event. While a person has taken over, the stream is a single message.completed with message: null, and their reply arrives through the updates route. One turn runs at a time per conversation. Send a client_message_id so a retry after a dropped connection replays the stored result as one terminal event instead of answering twice. A retry of a turn whose reply was cut off (none stored, or a truncated one) is answered again when no person has taken over and nothing but read-only work (such as knowledge searches) ran; otherwise it ends with error turn_interrupted.
Conversation token (ct_...) returned when the conversation started. How it works
Path parameters
idstringrequiredConversation id,
conv_.... An id the token does not belong to answers 401.
Request body
client_message_idstringup to 128 charactersYour id for this message, unique within the conversation. Resending the same id with the same content replays the stored turn; with different content or files it is 409
message_id_conflict.contentstringup to 4000 charactersMay be empty when
attachment_idsis given. A message needs non-empty content, attachment_ids, or both (otherwise 422invalid_content).Default
""consentstringThe visitor's analytics consent as your interface knows it now.
One of
granteddeniednot_requiredattachment_idsarray of stringup to 5 itemsIds returned by POST /public/v1/conversations/{id}/attachments for this conversation, at most 5. Each file can be sent once.
pageobjectThe host page the visitor sent this from. A new message stores its path (no origin, query or hash) and title as
rich.metadata.page, and a web chat model reads them beside that message. A retry keeps the page stored first. A malformed or non-http(s) page is ignored, never refused.2 attributes
urlstringrequiredup to 2048 characterstitlestringrequiredup to 200 characters
page_view_idstringup to 128 charactersThe page view the visitor sent this from, as reported to POST /public/v1/telemetry. Ignored when malformed.
testbooleanMarks the conversation as a test from now on, for a chat that began before the page was marked as one. Cannot be undone.
One of
true
Returns
200 SSE assistant turn
- 401 Missing, wrong or expired conversation token, or a conversation id the token does not belong to (
unauthorized; an unknown id is a 401, never a 404) - 403
attachments_disabled: the assistant's web chatattachments.enabledis false - 409
turn_in_progress(another turn is still running),conversation_ended,message_id_conflict(a replayedclient_message_idwith different content or attachments),attachment_already_sentorllm_not_configured - 422
invalid_content(no text and no files, or text over 4,000 characters),attachment_not_foundortoo_many_attachments - 429
key_quota_exceeded: the publishable key has used a budget shared by every visitor of your site: 300 new conversations an hour, 30 an hour from one IP address, or 5,000 assistant replies a day.rate_limitedis the ordinary per-address limit. Both carryRetry-After, in seconds. - 503
chat_temporarily_unavailable: retry afterRetry-Afterseconds (1)
curl -N -X POST "https://api.myleadsfactory.tech/public/v1/conversations/CONVERSATION_ID/messages" \
-H "Authorization: Bearer $CONVERSATION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"content": "Do you have trips to Lisbon in May?",
"client_message_id": "9b2f6c1e-3d4a-4f5b-8c7d-0e1f2a3b4c5d",
"page": {
"url": "https://www.acmetravel.com/lisbon",
"title": "Lisbon breaks"
}
}'event: message.accepted
data: {}
event: message.delta
data: {"text":"We do. "}
event: message.delta
data: {"text":"Which dates suit you?"}
event: message.completed
data: {"message":{"id":"msg_...","role":"assistant","content":"We do. Which dates suit you?","seq":2},"conversation":{"status":"open","taken_over":false,"lead_id":null}}Poll for new messages
GET/public/v1/conversations/{id}/updates
Messages after a sequence number, plus the conversation's state. Poll this while a person has taken over (their replies arrive here, not on the send stream) or after a dropped stream. Keep the highest seq you have seen and pass it as after_seq. Polling has its own rate limit: 60 a minute per conversation and IP address.
Conversation token (ct_...) returned when the conversation started. How it works
Path parameters
idstringrequiredConversation id,
conv_.... An id the token does not belong to answers 401.
Query parameters
after_seqintegerReturn messages with a
seqgreater than this. A missing or invalid value reads as 0 (everything).Default
0
Returns
200 Conversation state and visible messages after the cursor
Response attributes
dataobjectrequired2 attributes
conversationobjectrequired6 attributes
statusstringrequiredOne of
openendedresumablebooleanEnded only because the visitor went quiet: the next message reopens it.
handoff_requested_atstring | nullrequiredISO 8601 timestamptaken_overbooleanrequiredlead_idstring | nullThe lead this conversation created, once the visitor's details are in.
leadPublicChatLead | nullPresent only when the assistant's web chat
dynamic_ui.lead_panelis on: what this visitor has given so far, or null before there is a lead.One of 2 shapes
PublicChatLead
fieldsobjectrequiredcompletebooleanrequiredAn email or phone and every required field are in.
Option 2
messagesarray of PublicMessagerequired6 attributes
idstringrequiredmsg_...rolestringrequiredOne of
userassistantagentcontentstringrequiredrichRichContent | nullrequired4 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).
seqintegerrequiredPosition in the conversation; pass the highest you have as
after_seqwhen polling.created_atstringrequiredISO 8601 timestamp
- 401 Missing, wrong or expired conversation token, or a conversation id the token does not belong to (
unauthorized; an unknown id is a 401, never a 404) - 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/public/v1/conversations/CONVERSATION_ID/updates" \
-H "Authorization: Bearer $CONVERSATION_TOKEN"{
"data": {
"conversation": {
"status": "open",
"resumable": true,
"handoff_requested_at": "2026-10-28T09:30:00.000Z",
"taken_over": true,
"lead_id": "lead_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"lead": {
"fields": {
"key": {}
},
"complete": true
}
},
"messages": [
{
"id": "msg_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": [
{}
]
},
"seq": 1,
"created_at": "2026-10-28T09:30:00.000Z"
}
]
}
}Get a conversation's visible history
GET/public/v1/conversations/{id}
Everything the visitor can see, for rebuilding the chat after a reload: status, whether a quiet conversation can be reopened, handoff state and the visible messages. lead_name is the first name of the captured lead, when there is one.
Conversation token (ct_...) returned when the conversation started. How it works
Path parameters
idstringrequiredConversation id,
conv_.... An id the token does not belong to answers 401.
Returns
200 OK
Response attributes
dataPublicConversationrequired9 attributes
conversation_idstringrequiredstatusstringrequiredOne of
openendedended_reasonstring | nullrequiredWhy it ended. Null while it is open.
One of
visitor_endedidle_timeoutmax_messageserrorresumablebooleanrequiredEnded only because the visitor went quiet: the next message reopens it.
handoff_requested_atstring | nullrequiredISO 8601 timestamptaken_overbooleanrequiredA person holds the conversation now.
lead_namestringFirst name of the captured lead. Present only when there is one.
leadPublicChatLead | nullPresent only when the assistant's web chat
dynamic_ui.lead_panelis on: what this visitor has given so far, or null before there is a lead.One of 2 shapes
PublicChatLead
fieldsobjectrequiredcompletebooleanrequiredAn email or phone and every required field are in.
Option 2
messagesarray of PublicMessagerequired6 attributes
idstringrequiredmsg_...rolestringrequiredOne of
userassistantagentcontentstringrequiredrichRichContent | nullrequired4 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).
seqintegerrequiredPosition in the conversation; pass the highest you have as
after_seqwhen polling.created_atstringrequiredISO 8601 timestamp
- 401 Missing, wrong or expired conversation token, or a conversation id the token does not belong to (
unauthorized; an unknown id is a 401, never a 404) - 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/public/v1/conversations/CONVERSATION_ID" \
-H "Authorization: Bearer $CONVERSATION_TOKEN"{
"data": {
"conversation_id": "conv_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"status": "open",
"ended_reason": "visitor_ended",
"resumable": true,
"handoff_requested_at": "2026-10-28T09:30:00.000Z",
"taken_over": true,
"lead_name": "string",
"lead": {
"fields": {
"key": "string"
},
"complete": true
},
"messages": [
{
"id": "msg_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": [
{}
]
},
"seq": 1,
"created_at": "2026-10-28T09:30:00.000Z"
}
]
}
}Upload a file for a message
POST/public/v1/conversations/{id}/attachments
The request body is the raw file bytes (any content type), not a multipart form. Send the file name URI-encoded in X-MLF-Filename. The type is detected from the bytes, not the header or extension: JPEG, PNG, WebP, GIF, PDF and DOCX are accepted. Limits: 10 MB per file, 5 per message, 20 per conversation. Refused unless the assistant's web chat attachments.enabled is true. The file joins a message when POST /public/v1/conversations/{id}/messages names its id in attachment_ids.
Conversation token (ct_...) returned when the conversation started. How it works
Path parameters
idstringrequiredConversation id,
conv_.... An id the token does not belong to answers 401.
Headers
X-MLF-FilenamestringURI-encoded file name. Defaults to
file.
Request body
The file's raw bytes as the body, with the file's own Content-Type. Not multipart form data.
Returns
201 File stored
Response attributes
dataConversationAttachmentrequired5 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).
- 401 Missing, wrong or expired conversation token, or a conversation id the token does not belong to (
unauthorized; an unknown id is a 401, never a 404) - 403
attachments_disabled - 409
conversation_endedorattachment_limit_reached(20 files per conversation) - 413
attachment_too_large(over 10 MB) - 415
attachment_type_not_supported - 422
attachment_empty - 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). - 503
attachments_unavailable: this deployment cannot store files
curl -X POST "https://api.myleadsfactory.tech/public/v1/conversations/CONVERSATION_ID/attachments" \
-H "Authorization: Bearer $CONVERSATION_TOKEN" \
-H "Content-Type: image/png" \
--data-binary @photo.png{
"data": {
"id": "conv_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"name": "Front desk",
"content_type": "image/jpeg",
"size": 1,
"url": "https://example.com/hook"
}
}Download an attachment
GET/public/chat/attachments/{id}
Public: the signed link is the only credential. Links come from API responses (url on an upload or on a message's rich.attachments) and from webhook payloads. Links from the widget and console API last 24 hours; links in webhooks last 7 days. Both stop working sooner when the chat came from a test key that is revoked or a preview link that is turned off or expires.
The signed URL is the credential; no key needed. How it works
Path parameters
idstringrequiredAttachment id,
att_....
Query parameters
expiresintegerrequiredLink expiry, Unix seconds.
signaturestringrequiredThe link's signature. Use the URL as given; do not build it yourself.
Returns
200 The file bytes, with the file's own Content-Type. Content-Disposition is inline for images and attachment for documents.
- 404 Invalid or expired link, or the file no longer exists
- 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/public/chat/attachments/ATTACHMENT_ID?expires=1&signature=string" \
-o attachmentEnd a conversation
POST/public/v1/conversations/{id}/end
Ends the conversation for good when the visitor closes it, and triggers its conversation.ended webhook and analysis. A conversation that ended only because the visitor went quiet reopens on their next message; one ended here does not.
Conversation token (ct_...) returned when the conversation started. How it works
Path parameters
idstringrequiredConversation id,
conv_.... An id the token does not belong to answers 401.
Returns
200 Conversation ended
Response attributes
dataobjectrequired1 attribute
statusstringrequiredOne of
ended
- 401 Missing, wrong or expired conversation token, or a conversation id the token does not belong to (
unauthorized; an unknown id is a 401, never a 404) - 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/public/v1/conversations/CONVERSATION_ID/end" \
-H "Authorization: Bearer $CONVERSATION_TOKEN"{
"data": {
"status": "ended"
}
}Rate an assistant reply
POST/public/v1/conversations/{id}/messages/{messageId}/feedback
Thumbs up or down on one of the assistant's replies, with an optional reason for a thumbs down. Sending again replaces the verdict; none takes it back. Feeds the quality view in chat analytics.
Conversation token (ct_...) returned when the conversation started. How it works
Path parameters
idstringrequiredConversation id,
conv_.... An id the token does not belong to answers 401.messageIdstringrequiredAn assistant reply in this conversation,
msg_....
Request body
ratingstringrequiredOne of
updownnonereasonstringOne of
wrongunhelpfulincompleteother
Returns
200 OK
Response attributes
dataobjectrequired1 attribute
ratingstringrequiredOne of
updownnone
- 401 Missing, wrong or expired conversation token, or a conversation id the token does not belong to (
unauthorized; an unknown id is a 401, never a 404) - 404
not_found: the message is not one of this conversation's assistant replies - 422
invalid_feedback - 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/public/v1/conversations/CONVERSATION_ID/messages/MESSAGE_ID/feedback" \
-H "Authorization: Bearer $CONVERSATION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"rating": "up"
}'{
"data": {
"rating": "up"
}
}Rate the conversation
POST/public/v1/conversations/{id}/rating
The visitor's 1 to 5 rating of the whole chat, with an optional comment. Sending again replaces it.
Conversation token (ct_...) returned when the conversation started. How it works
Path parameters
idstringrequiredConversation id,
conv_.... An id the token does not belong to answers 401.
Request body
ratingintegerrequired1 to 5commentstringup to 500 characters
Returns
200 OK
Response attributes
dataobjectrequired1 attribute
ratingintegerrequired1 to 5
- 401 Missing, wrong or expired conversation token, or a conversation id the token does not belong to (
unauthorized; an unknown id is a 401, never a 404) - 422
invalid_rating - 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/public/v1/conversations/CONVERSATION_ID/rating" \
-H "Authorization: Bearer $CONVERSATION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"rating": 1
}'{
"data": {
"rating": 1
}
}Report the visitor is still here
POST/public/v1/conversations/{id}/presence
A heartbeat while the chat is open, so a person who has taken over can see the visitor is still on the page. Presence lapses 150 seconds after the last call; send one every minute or so. No request body.
Conversation token (ct_...) returned when the conversation started. How it works
Path parameters
idstringrequiredConversation id,
conv_.... An id the token does not belong to answers 401.
Returns
200 OK
Response attributes
dataobjectrequired1 attribute
okbooleanrequiredOne of
true
- 401 Missing, wrong or expired conversation token, or a conversation id the token does not belong to (
unauthorized; an unknown id is a 401, never a 404) - 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/public/v1/conversations/CONVERSATION_ID/presence" \
-H "Authorization: Bearer $CONVERSATION_TOKEN"{
"data": {
"ok": true
}
}Update the visitor's analytics consent
POST/public/v1/conversations/{id}/consent
Call when the visitor answers your cookie banner while a chat is open, so later analytics follow their choice.
Conversation token (ct_...) returned when the conversation started. How it works
Path parameters
idstringrequiredConversation id,
conv_.... An id the token does not belong to answers 401.
Request body
consentstringrequiredOne of
granteddeniednot_required
Returns
200 OK
Response attributes
dataobjectrequired1 attribute
consentstringrequiredOne of
granteddeniednot_required
- 401 Missing, wrong or expired conversation token, or a conversation id the token does not belong to (
unauthorized; an unknown id is a 401, never a 404) - 422
invalid_consent - 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/public/v1/conversations/CONVERSATION_ID/consent" \
-H "Authorization: Bearer $CONVERSATION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"consent": "granted"
}'{
"data": {
"consent": "granted"
}
}Report chat analytics events
POST/public/v1/telemetry
The funnel events a chat interface reports (loaded, shown, opened, typed, sent, result), up to 50 per batch, which chat analytics is built from. Optional for a custom interface. Rate limited to 120 a minute per IP address (429). Past the limiter it always answers 204 with no body, even for a body it cannot read or a key it does not know, so analytics never breaks a page; such batches are dropped.
Publishable key (pk_live_... or pk_test_...), from an origin it allows. How it works
Request body
widget_keystringrequiredup to 256 charactersPublishable key,
pk_live_....assistantstringup to 128 charactersAssistant id; needed with a project-scoped publishable key.
parent_originstringup to 2048 charactersRead as on conversation start: ignored from a browser on another site.
visitor_idstringup to 128 characterspagestringup to 2048 charactersThe page URL the events happened on.
page_view_idstringvisit_idstringThe visit this page view belongs to. Stored only with analytics consent.
visit_numberinteger1 to 1000000Which visit this is for the visitor: 1 for the first.
consentstringOne of
granteddeniednot_requiredwidget_versionstringup to 40 charactersreferrerstringup to 2048 characterslandingstringup to 2048 charactersThe landing URL's query, read for campaign tags and never stored.
testbooleanga_client_idstringup to 128 charactersga_session_idstringup to 64 characterseventsarray of objectrequiredup to 50 items14 attributes
event_idstringrequiredUnique per event, so a resent batch counts once.
namestringrequiredOne of
widget_loadedwidget_shownwidget_openedcomposer_typedmessage_sendmessage_resultoccurred_atintegerMilliseconds since the epoch, browser clock.
reasonstringwidget_opened: why the panel opened.One of
proactivevisitorapipagestringup to 2048 charactersconversation_idstringclient_message_idstringstatusstringmessage_resultonly.One of
okrescuedfailederror_codestringduration_msinteger0 to 3600000first_token_msinteger0 to 3600000attemptsinteger0 to 20quick_replybooleanreply_shownboolean
Returns
204 Accepted (or ignored). No body.
- 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/public/v1/telemetry" \
-H "Origin: https://www.example.com" \
-H "Content-Type: application/json" \
-d '{
"widget_key": "string",
"events": [
{
"event_id": "evt_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"name": "widget_loaded"
}
]
}'Start a voice session
POST/public/v1/conversations/{id}/voice
Lets the visitor talk to the assistant through their microphone, in the same conversation. Available when the assistant's web chat voice.enabled is on (the widget config then has voice), never in a preview link's chat. Join the LiveKit room at url with token before expires_at, publish the microphone and play the agent's audio. The agent knows the conversation so far and carries on from it. Live captions arrive as LiveKit text streams on topic lk.transcription: the visitor's words come from identity web_{voice_session_id}, the assistant's from the agent, with attributes lk.segment_id and lk.transcription_final. The session ends after max_seconds, when either side hangs up, or when the browser never joins. The spoken turns then join the conversation as messages with rich.metadata.channel voice (read them through the updates route), and the conversation is checked for a lead. One session per conversation at a time.
Conversation token (ct_...) returned when the conversation started. How it works
Path parameters
idstringrequiredConversation id,
conv_.... An id the token does not belong to answers 401.
Request body
consentobjectrequiredThe visitor agreed to use their microphone. Ask before you call this.
1 attribute
microphonebooleanrequiredOne of
true
Returns
201 Voice session started
Response attributes
dataobjectrequired5 attributes
voice_session_idstringrequiredKeep it to read or end the session.
urlstringrequiredThe LiveKit server to join,
wss://....tokenstringrequiredA room token for this browser only.
expires_atstringrequiredISO 8601 timestampJoin before this. The session itself then runs up to
max_seconds.max_secondsintegerrequiredThe session's length cap, 60 to 600 seconds.
- 400
consent_required: the body did not carryconsent.microphone: true - 401 Missing, wrong or expired conversation token, or a conversation id the token does not belong to (
unauthorized; an unknown id is a 401, never a 404) - 403
voice_disabled(voice is off for the assistant, or this is a preview chat) ororigin_not_allowed - 404 Not found, or outside what the key can see
- 409
voice_session_active(one is already running: end it first;voice_session_idnames it when known),conversation_ended, orconversation_taken_over(a person is answering) - 429
key_quota_exceeded: the publishable key has started 60 voice sessions this hour, 10 from this IP address, or used its 300 voice minutes for the UTC day.voice_busy: the key already has 5 sessions running.rate_limited: the ordinary per-address limit. All carryRetry-After, in seconds. - 503
voice_unavailable: voice calls cannot be placed right now. When the cause is passing it carriesRetry-After(5 seconds); without the header, voice is not set up on this deployment.
curl -X POST "https://api.myleadsfactory.tech/public/v1/conversations/CONVERSATION_ID/voice" \
-H "Authorization: Bearer $CONVERSATION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"consent": {
"microphone": true
}
}'{
"data": {
"voice_session_id": "id_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"url": "https://example.com/hook",
"token": "string",
"expires_at": "2026-10-28T09:30:00.000Z",
"max_seconds": 1
}
}Get a voice session
GET/public/v1/conversations/{id}/voice/{voiceSessionId}
Whether the session is still connecting, running or over, and why it ended. Only sessions of this conversation are found.
Conversation token (ct_...) returned when the conversation started. How it works
Path parameters
idstringrequiredConversation id,
conv_.... An id the token does not belong to answers 401.voiceSessionIdstringrequiredThe
voice_session_idthat starting the session returned.
Returns
200 OK
Response attributes
dataobjectrequired4 attributes
voice_session_idstringrequiredThe voice session's id, which is also its call's id.
statusstringrequiredOne of
connectingactiveendedended_reasonstring | nullrequiredNull until the session ends. Then the call's ended reason (for example
customer_hangup,assistant_endedormax_duration),visitor_endedwhen you ended it through this API,not_joinedwhen the browser never joined, orexpired.max_secondsinteger | nullrequiredThe session's length cap, in seconds.
- 401 Missing, wrong or expired conversation token, or a conversation id the token does not belong to (
unauthorized; an unknown id is a 401, never a 404) - 404
not_found: no such voice session in this conversation - 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/public/v1/conversations/CONVERSATION_ID/voice/VOICE_SESSION_ID" \
-H "Authorization: Bearer $CONVERSATION_TOKEN"{
"data": {
"voice_session_id": "id_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"status": "connecting",
"ended_reason": "string",
"max_seconds": 1
}
}End a voice session
POST/public/v1/conversations/{id}/voice/{voiceSessionId}/end
Hangs up for both sides. Safe to repeat: a session that already ended answers with its state. The conversation stays open for typing.
Conversation token (ct_...) returned when the conversation started. How it works
Path parameters
idstringrequiredConversation id,
conv_.... An id the token does not belong to answers 401.voiceSessionIdstringrequiredThe
voice_session_idthat starting the session returned.
Returns
200 OK
Response attributes
dataobjectrequired4 attributes
voice_session_idstringrequiredThe voice session's id, which is also its call's id.
statusstringrequiredOne of
connectingactiveendedended_reasonstring | nullrequiredNull until the session ends. Then the call's ended reason (for example
customer_hangup,assistant_endedormax_duration),visitor_endedwhen you ended it through this API,not_joinedwhen the browser never joined, orexpired.max_secondsinteger | nullrequiredThe session's length cap, in seconds.
- 401 Missing, wrong or expired conversation token, or a conversation id the token does not belong to (
unauthorized; an unknown id is a 401, never a 404) - 404
not_found: no such voice session in this conversation - 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). - 503
voice_unavailable: the call could not be ended just now and may still be running. Retry afterRetry-Afterseconds.
curl -X POST "https://api.myleadsfactory.tech/public/v1/conversations/CONVERSATION_ID/voice/VOICE_SESSION_ID/end" \
-H "Authorization: Bearer $CONVERSATION_TOKEN"{
"data": {
"voice_session_id": "id_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
"status": "connecting",
"ended_reason": "string",
"max_seconds": 1
}
}