BrightshoalDocsOpen console
MenuPublic chat API

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

  • keystringrequired

    Publishable key, pk_live_....

  • assistantstring

    Required with a project-scoped publishable key; ignored-if-matching for assistant-pinned keys.

Returns

200 OK

Response attributes
  • dataPublicWidgetConfigrequired

    The 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
    • namestringrequired

      Display name: appearance.display_name, or the assistant's name.

    • greetingstring

    • quick_repliesarray of string

    • hero_topicsarray of string

      Topic buttons shown before the first message.

    • appearanceobject

      Colours, position, display name and avatar.

    • proactiveobject

      When and where to open a teaser on its own.

    • stringsobject

      Interface text overrides.

    • callobject | null

      A phone number to offer in the header, when set.

    • handoff_messagestring | null

      What to show when the visitor asks for a person.

    • powered_byany

    • feedbackobject

      Whether to ask for reply feedback and a conversation rating.

    • attachmentsobject

      Present only when the assistant takes files and this deployment can store them.

      4 attributes
      • enabledboolean

        One of true

      • max_bytesinteger

      • max_filesinteger

        Per message.

      • acceptarray of string

        Accepted content types.

    • analyticsobject

      Off for test and preview chats, so they never reach your analytics.

      4 attributes
      • data_layerboolean

      • gtagboolean

      • event_prefixstring

      • require_consentboolean

    • identity_scopestringrequired

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

    • modestring

      Present only for a test key or a preview link; a live key's config has no mode.

      One of test preview

    • uiobject

      Present only when the assistant uses the quiet chat style.

      1 attribute
      • stylestring

        One of quiet

    • dynamic_uiobject

      Present only when the assistant shows contact details as links.

      1 attribute
      • contact_linksboolean

        One of true

    • lead_capturePublicLeadSchema

      The lead details the chat asks for, so your page can show what it has captured. Present only when lead capture and dynamic_ui.lead_panel are both on.

      2 attributes
      • require_completebooleanrequired

        No lead is saved until every required field and an email or phone are in.

      • fieldsarray of objectrequired

        4 attributes
        • keystringrequired

        • labelstringrequired

          The field's short name for people to read.

        • typestringrequired

          One of string email phone

        • requiredbooleanrequired

    • voiceobject

      Present 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
      • enabledbooleanrequired

        One of true

      • max_secondsintegerrequired

        Each voice session's length cap.

  • 403 origin_not_allowed: the request's Origin is not in the assistant's allowed_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). Wait Retry-After seconds. 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).
GET /public/v1/config
curl "https://api.myleadsfactory.tech/public/v1/config?key=pk_live_..."
Response 200
{
  "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 empty

    Publishable key, pk_live_....

  • assistantstringnot empty

    Assistant id; required with a project-scoped publishable key.

  • parent_originstring

    The site the chat runs on, for requests that carry no Origin header of their own (your server, for example) and for the widget's own frame. A browser calling from another site is checked by its real Origin header instead, and this field is ignored, so a page cannot borrow an allowed site's name.

  • visitor_idstring

    Your 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 characters

    The 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 characters

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

  • attributionChatAttribution

    Where the visitor came from. Sanitized, never a reason to refuse the chat. Only chat_page is kept without analytics consent.

    3 attributes
    • chat_pagestring

      The path the visitor started chatting on.

    • visitChatAttributionTouch

      This visit's landing.

      12 attributes
      • landing_pagestring

        A path starting with /, without query or fragment.

      • referrer_hoststring

      • utm_sourcestring

      • utm_mediumstring

      • utm_campaignstring

      • utm_termstring

      • utm_contentstring

      • gclidstring

      • gbraidstring

      • wbraidstring

      • fbclidstring

      • msclkidstring

    • firstobject

      The visitor's first-ever landing.

      13 attributes
      • landing_pagestring

        A path starting with /, without query or fragment.

      • referrer_hoststring

      • utm_sourcestring

      • utm_mediumstring

      • utm_campaignstring

      • utm_termstring

      • utm_contentstring

      • gclidstring

      • gbraidstring

      • wbraidstring

      • fbclidstring

      • msclkidstring

      • atstringISO 8601 timestamp

        When the visitor first landed.

  • metadataobject

    Free-form context stored on the conversation and sent in its webhooks (for example a region or campaign). The preview and attribution keys are reserved and dropped; send attribution in attribution.

  • client_message_idstringup to 128 characters

    The 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 characters

    The page view the chat started on, as reported to POST /public/v1/telemetry.

  • consentstring

    The visitor's analytics consent, which decides whether analytics ids are stored.

    One of granted denied not_required

  • preamblearray of objectup to 5 items

    Assistant 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
  • dataobjectrequired

    5 attributes
    • conversation_idstringrequired

    • tokenstringrequired

      Conversation token, ct_...: send it as Authorization: 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).

    • statusstringrequired

      One of open

    • messagesarray of PublicMessagerequired

      Any preamble lines you sent, stored as the assistant's first messages.

      6 attributes
      • idstringrequired

        msg_...

      • rolestringrequired

        One of user assistant agent

      • contentstringrequired

      • richRichContent | nullrequired

        4 attributes
        • quick_repliesarray of object

          2 attributes
          • labelstringrequired

          • valuestringrequired

        • buttonsarray of object

          3 attributes
          • typestringrequired

            One of link

          • labelstringrequired

          • urlstringrequireduri

        • metadataobject

          3 attributes
          • pageobject

            Visitor messages from the web widget: the page they sent the message from.

            2 attributes
            • pathstringrequired

            • titlestringrequired

          • channelstring

            A turn spoken in a voice session of this conversation rather than typed.

            One of voice

          • voice_session_idstring

            The voice session the turn was spoken in.

        • attachmentsarray of ConversationAttachment

          Files sent with the message. Each url is a signed link that works without credentials.

          5 attributes
          • idstringrequired

          • namestringrequired

          • content_typestringrequired

            One of image/jpeg image/png image/webp image/gif application/pdf application/vnd.openxmlformats-officedocument.wordprocessingml.document

          • sizeintegerrequired

            Bytes.

          • urlstringrequireduri

            Signed download link (GET /public/chat/attachments/{id}), valid for 24 hours (7 days in webhooks).

      • seqintegerrequired

        Position in the conversation; pass the highest you have as after_seq when polling.

      • created_atstringrequiredISO 8601 timestamp

    • configPublicWidgetConfigrequired

      The 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
      • namestringrequired

        Display name: appearance.display_name, or the assistant's name.

      • greetingstring

      • quick_repliesarray of string

      • hero_topicsarray of string

        Topic buttons shown before the first message.

      • appearanceobject

        Colours, position, display name and avatar.

      • proactiveobject

        When and where to open a teaser on its own.

      • stringsobject

        Interface text overrides.

      • callobject | null

        A phone number to offer in the header, when set.

      • handoff_messagestring | null

        What to show when the visitor asks for a person.

      • powered_byany

      • feedbackobject

        Whether to ask for reply feedback and a conversation rating.

      • attachmentsobject

        Present only when the assistant takes files and this deployment can store them.

        4 attributes
        • enabledboolean

          One of true

        • max_bytesinteger

        • max_filesinteger

          Per message.

        • acceptarray of string

          Accepted content types.

      • analyticsobject

        Off for test and preview chats, so they never reach your analytics.

        4 attributes
        • data_layerboolean

        • gtagboolean

        • event_prefixstring

        • require_consentboolean

      • identity_scopestringrequired

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

      • modestring

        Present only for a test key or a preview link; a live key's config has no mode.

        One of test preview

      • uiobject

        Present only when the assistant uses the quiet chat style.

        1 attribute
        • stylestring

          One of quiet

      • dynamic_uiobject

        Present only when the assistant shows contact details as links.

        1 attribute
        • contact_linksboolean

          One of true

      • lead_capturePublicLeadSchema

        The lead details the chat asks for, so your page can show what it has captured. Present only when lead capture and dynamic_ui.lead_panel are both on.

        2 attributes
        • require_completebooleanrequired

          No lead is saved until every required field and an email or phone are in.

        • fieldsarray of objectrequired

          4 attributes
          • keystringrequired

          • labelstringrequired

            The field's short name for people to read.

          • typestringrequired

            One of string email phone

          • requiredbooleanrequired

      • voiceobject

        Present 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
        • enabledbooleanrequired

          One of true

        • max_secondsintegerrequired

          Each voice session's length cap.

  • 400 Validation error: usually invalid_request, with the failing fields in issues when there are any
  • 403 origin_not_allowed: the calling site is not in the assistant's allowed_origins (see parent_origin for 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_limited is the ordinary per-address limit. Both carry Retry-After, in seconds.
POST /public/v1/conversations
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"
}'
Response 201
{
  "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

  • idstringrequired

    Conversation id, conv_.... An id the token does not belong to answers 401.

Request body

  • client_message_idstringup to 128 characters

    Your 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 characters

    May be empty when attachment_ids is given. A message needs non-empty content, attachment_ids, or both (otherwise 422 invalid_content).

    Default ""

  • consentstring

    The visitor's analytics consent as your interface knows it now.

    One of granted denied not_required

  • attachment_idsarray of stringup to 5 items

    Ids returned by POST /public/v1/conversations/{id}/attachments for this conversation, at most 5. Each file can be sent once.

  • pageobject

    The 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 characters

    • titlestringrequiredup to 200 characters

  • page_view_idstringup to 128 characters

    The page view the visitor sent this from, as reported to POST /public/v1/telemetry. Ignored when malformed.

  • testboolean

    Marks 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 chat attachments.enabled is false
  • 409 turn_in_progress (another turn is still running), conversation_ended, message_id_conflict (a replayed client_message_id with different content or attachments), attachment_already_sent or llm_not_configured
  • 422 invalid_content (no text and no files, or text over 4,000 characters), attachment_not_found or too_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_limited is the ordinary per-address limit. Both carry Retry-After, in seconds.
  • 503 chat_temporarily_unavailable: retry after Retry-After seconds (1)
POST /public/v1/conversations/{id}/messages
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"
  }
}'
Response 200
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

  • idstringrequired

    Conversation id, conv_.... An id the token does not belong to answers 401.

Query parameters

  • after_seqinteger

    Return messages with a seq greater 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
  • dataobjectrequired

    2 attributes
    • conversationobjectrequired

      6 attributes
      • statusstringrequired

        One of open ended

      • resumableboolean

        Ended only because the visitor went quiet: the next message reopens it.

      • handoff_requested_atstring | nullrequiredISO 8601 timestamp

      • taken_overbooleanrequired

      • lead_idstring | null

        The lead this conversation created, once the visitor's details are in.

      • leadPublicChatLead | null

        Present only when the assistant's web chat dynamic_ui.lead_panel is on: what this visitor has given so far, or null before there is a lead.

        One of 2 shapes

        PublicChatLead

        • fieldsobjectrequired

        • completebooleanrequired

          An email or phone and every required field are in.

        Option 2

    • messagesarray of PublicMessagerequired

      6 attributes
      • idstringrequired

        msg_...

      • rolestringrequired

        One of user assistant agent

      • contentstringrequired

      • richRichContent | nullrequired

        4 attributes
        • quick_repliesarray of object

          2 attributes
          • labelstringrequired

          • valuestringrequired

        • buttonsarray of object

          3 attributes
          • typestringrequired

            One of link

          • labelstringrequired

          • urlstringrequireduri

        • metadataobject

          3 attributes
          • pageobject

            Visitor messages from the web widget: the page they sent the message from.

            2 attributes
            • pathstringrequired

            • titlestringrequired

          • channelstring

            A turn spoken in a voice session of this conversation rather than typed.

            One of voice

          • voice_session_idstring

            The voice session the turn was spoken in.

        • attachmentsarray of ConversationAttachment

          Files sent with the message. Each url is a signed link that works without credentials.

          5 attributes
          • idstringrequired

          • namestringrequired

          • content_typestringrequired

            One of image/jpeg image/png image/webp image/gif application/pdf application/vnd.openxmlformats-officedocument.wordprocessingml.document

          • sizeintegerrequired

            Bytes.

          • urlstringrequireduri

            Signed download link (GET /public/chat/attachments/{id}), valid for 24 hours (7 days in webhooks).

      • seqintegerrequired

        Position in the conversation; pass the highest you have as after_seq when 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). Wait Retry-After seconds. 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).
GET /public/v1/conversations/{id}/updates
curl "https://api.myleadsfactory.tech/public/v1/conversations/CONVERSATION_ID/updates" \
  -H "Authorization: Bearer $CONVERSATION_TOKEN"
Response 200
{
  "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

  • idstringrequired

    Conversation id, conv_.... An id the token does not belong to answers 401.

Returns

200 OK

Response attributes
  • dataPublicConversationrequired

    9 attributes
    • conversation_idstringrequired

    • statusstringrequired

      One of open ended

    • ended_reasonstring | nullrequired

      Why it ended. Null while it is open.

      One of visitor_ended idle_timeout max_messages error

    • resumablebooleanrequired

      Ended only because the visitor went quiet: the next message reopens it.

    • handoff_requested_atstring | nullrequiredISO 8601 timestamp

    • taken_overbooleanrequired

      A person holds the conversation now.

    • lead_namestring

      First name of the captured lead. Present only when there is one.

    • leadPublicChatLead | null

      Present only when the assistant's web chat dynamic_ui.lead_panel is on: what this visitor has given so far, or null before there is a lead.

      One of 2 shapes

      PublicChatLead

      • fieldsobjectrequired

      • completebooleanrequired

        An email or phone and every required field are in.

      Option 2

    • messagesarray of PublicMessagerequired

      6 attributes
      • idstringrequired

        msg_...

      • rolestringrequired

        One of user assistant agent

      • contentstringrequired

      • richRichContent | nullrequired

        4 attributes
        • quick_repliesarray of object

          2 attributes
          • labelstringrequired

          • valuestringrequired

        • buttonsarray of object

          3 attributes
          • typestringrequired

            One of link

          • labelstringrequired

          • urlstringrequireduri

        • metadataobject

          3 attributes
          • pageobject

            Visitor messages from the web widget: the page they sent the message from.

            2 attributes
            • pathstringrequired

            • titlestringrequired

          • channelstring

            A turn spoken in a voice session of this conversation rather than typed.

            One of voice

          • voice_session_idstring

            The voice session the turn was spoken in.

        • attachmentsarray of ConversationAttachment

          Files sent with the message. Each url is a signed link that works without credentials.

          5 attributes
          • idstringrequired

          • namestringrequired

          • content_typestringrequired

            One of image/jpeg image/png image/webp image/gif application/pdf application/vnd.openxmlformats-officedocument.wordprocessingml.document

          • sizeintegerrequired

            Bytes.

          • urlstringrequireduri

            Signed download link (GET /public/chat/attachments/{id}), valid for 24 hours (7 days in webhooks).

      • seqintegerrequired

        Position in the conversation; pass the highest you have as after_seq when 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). Wait Retry-After seconds. 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).
GET /public/v1/conversations/{id}
curl "https://api.myleadsfactory.tech/public/v1/conversations/CONVERSATION_ID" \
  -H "Authorization: Bearer $CONVERSATION_TOKEN"
Response 200
{
  "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

  • idstringrequired

    Conversation id, conv_.... An id the token does not belong to answers 401.

Headers

  • X-MLF-Filenamestring

    URI-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
  • dataConversationAttachmentrequired

    5 attributes
    • idstringrequired

    • namestringrequired

    • content_typestringrequired

      One of image/jpeg image/png image/webp image/gif application/pdf application/vnd.openxmlformats-officedocument.wordprocessingml.document

    • sizeintegerrequired

      Bytes.

    • urlstringrequireduri

      Signed 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_ended or attachment_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). Wait Retry-After seconds. 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
POST /public/v1/conversations/{id}/attachments
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
Response 201
{
  "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

  • idstringrequired

    Attachment id, att_....

Query parameters

  • expiresintegerrequired

    Link expiry, Unix seconds.

  • signaturestringrequired

    The 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). Wait Retry-After seconds. 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).
GET /public/chat/attachments/{id}
curl "https://api.myleadsfactory.tech/public/chat/attachments/ATTACHMENT_ID?expires=1&signature=string" \
  -o attachment

End 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

  • idstringrequired

    Conversation id, conv_.... An id the token does not belong to answers 401.

Returns

200 Conversation ended

Response attributes
  • dataobjectrequired

    1 attribute
    • statusstringrequired

      One 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). Wait Retry-After seconds. 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).
POST /public/v1/conversations/{id}/end
curl -X POST "https://api.myleadsfactory.tech/public/v1/conversations/CONVERSATION_ID/end" \
  -H "Authorization: Bearer $CONVERSATION_TOKEN"
Response 200
{
  "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

  • idstringrequired

    Conversation id, conv_.... An id the token does not belong to answers 401.

  • messageIdstringrequired

    An assistant reply in this conversation, msg_....

Request body

  • ratingstringrequired

    One of up down none

  • reasonstring

    One of wrong unhelpful incomplete other

Returns

200 OK

Response attributes
  • dataobjectrequired

    1 attribute
    • ratingstringrequired

      One of up down none

  • 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). Wait Retry-After seconds. 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).
POST /public/v1/conversations/{id}/messages/{messageId}/feedback
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"
}'
Response 200
{
  "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

  • idstringrequired

    Conversation id, conv_.... An id the token does not belong to answers 401.

Request body

  • ratingintegerrequired1 to 5

  • commentstringup to 500 characters

Returns

200 OK

Response attributes
  • dataobjectrequired

    1 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). Wait Retry-After seconds. 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).
POST /public/v1/conversations/{id}/rating
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
}'
Response 200
{
  "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

  • idstringrequired

    Conversation id, conv_.... An id the token does not belong to answers 401.

Returns

200 OK

Response attributes
  • dataobjectrequired

    1 attribute
    • okbooleanrequired

      One 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). Wait Retry-After seconds. 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).
POST /public/v1/conversations/{id}/presence
curl -X POST "https://api.myleadsfactory.tech/public/v1/conversations/CONVERSATION_ID/presence" \
  -H "Authorization: Bearer $CONVERSATION_TOKEN"
Response 200
{
  "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

  • idstringrequired

    Conversation id, conv_.... An id the token does not belong to answers 401.

Request body

  • consentstringrequired

    One of granted denied not_required

Returns

200 OK

Response attributes
  • dataobjectrequired

    1 attribute
    • consentstringrequired

      One of granted denied not_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). Wait Retry-After seconds. 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).
POST /public/v1/conversations/{id}/consent
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"
}'
Response 200
{
  "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 characters

    Publishable key, pk_live_....

  • assistantstringup to 128 characters

    Assistant id; needed with a project-scoped publishable key.

  • parent_originstringup to 2048 characters

    Read as on conversation start: ignored from a browser on another site.

  • visitor_idstringup to 128 characters

  • pagestringup to 2048 characters

    The page URL the events happened on.

  • page_view_idstring

  • visit_idstring

    The visit this page view belongs to. Stored only with analytics consent.

  • visit_numberinteger1 to 1000000

    Which visit this is for the visitor: 1 for the first.

  • consentstring

    One of granted denied not_required

  • widget_versionstringup to 40 characters

  • referrerstringup to 2048 characters

  • landingstringup to 2048 characters

    The landing URL's query, read for campaign tags and never stored.

  • testboolean

  • ga_client_idstringup to 128 characters

  • ga_session_idstringup to 64 characters

  • eventsarray of objectrequiredup to 50 items

    14 attributes
    • event_idstringrequired

      Unique per event, so a resent batch counts once.

    • namestringrequired

      One of widget_loaded widget_shown widget_opened composer_typed message_send message_result

    • occurred_atinteger

      Milliseconds since the epoch, browser clock.

    • reasonstring

      widget_opened: why the panel opened.

      One of proactive visitor api

    • pagestringup to 2048 characters

    • conversation_idstring

    • client_message_idstring

    • statusstring

      message_result only.

      One of ok rescued failed

    • error_codestring

    • duration_msinteger0 to 3600000

    • first_token_msinteger0 to 3600000

    • attemptsinteger0 to 20

    • quick_replyboolean

    • reply_shownboolean

Returns

204 Accepted (or ignored). No body.

  • 429 Rate limit exceeded (rate_limited). Wait Retry-After seconds. 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).
POST /public/v1/telemetry
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

  • idstringrequired

    Conversation id, conv_.... An id the token does not belong to answers 401.

Request body

  • consentobjectrequired

    The visitor agreed to use their microphone. Ask before you call this.

    1 attribute
    • microphonebooleanrequired

      One of true

Returns

201 Voice session started

Response attributes
  • dataobjectrequired

    5 attributes
    • voice_session_idstringrequired

      Keep it to read or end the session.

    • urlstringrequired

      The LiveKit server to join, wss://....

    • tokenstringrequired

      A room token for this browser only.

    • expires_atstringrequiredISO 8601 timestamp

      Join before this. The session itself then runs up to max_seconds.

    • max_secondsintegerrequired

      The session's length cap, 60 to 600 seconds.

  • 400 consent_required: the body did not carry consent.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) or origin_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_id names it when known), conversation_ended, or conversation_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 carry Retry-After, in seconds.
  • 503 voice_unavailable: voice calls cannot be placed right now. When the cause is passing it carries Retry-After (5 seconds); without the header, voice is not set up on this deployment.
POST /public/v1/conversations/{id}/voice
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
  }
}'
Response 201
{
  "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

  • idstringrequired

    Conversation id, conv_.... An id the token does not belong to answers 401.

  • voiceSessionIdstringrequired

    The voice_session_id that starting the session returned.

Returns

200 OK

Response attributes
  • dataobjectrequired

    4 attributes
    • voice_session_idstringrequired

      The voice session's id, which is also its call's id.

    • statusstringrequired

      One of connecting active ended

    • ended_reasonstring | nullrequired

      Null until the session ends. Then the call's ended reason (for example customer_hangup, assistant_ended or max_duration), visitor_ended when you ended it through this API, not_joined when the browser never joined, or expired.

    • max_secondsinteger | nullrequired

      The 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). Wait Retry-After seconds. 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).
GET /public/v1/conversations/{id}/voice/{voiceSessionId}
curl "https://api.myleadsfactory.tech/public/v1/conversations/CONVERSATION_ID/voice/VOICE_SESSION_ID" \
  -H "Authorization: Bearer $CONVERSATION_TOKEN"
Response 200
{
  "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

  • idstringrequired

    Conversation id, conv_.... An id the token does not belong to answers 401.

  • voiceSessionIdstringrequired

    The voice_session_id that starting the session returned.

Returns

200 OK

Response attributes
  • dataobjectrequired

    4 attributes
    • voice_session_idstringrequired

      The voice session's id, which is also its call's id.

    • statusstringrequired

      One of connecting active ended

    • ended_reasonstring | nullrequired

      Null until the session ends. Then the call's ended reason (for example customer_hangup, assistant_ended or max_duration), visitor_ended when you ended it through this API, not_joined when the browser never joined, or expired.

    • max_secondsinteger | nullrequired

      The 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). Wait Retry-After seconds. 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 after Retry-After seconds.
POST /public/v1/conversations/{id}/voice/{voiceSessionId}/end
curl -X POST "https://api.myleadsfactory.tech/public/v1/conversations/CONVERSATION_ID/voice/VOICE_SESSION_ID/end" \
  -H "Authorization: Bearer $CONVERSATION_TOKEN"
Response 200
{
  "data": {
    "voice_session_id": "id_5f0c2a9e7b1d4c3e8a6f0b2d4c6e8a1f",
    "status": "connecting",
    "ended_reason": "string",
    "max_seconds": 1
  }
}