Marco widget · Technical documentation

Implementation guide

The Marco widget installs in three different ways, depending on where your application lives and where the session data has to stay. This guide covers all three. Pick your flow and follow only that column: each one is self-contained.

Message version 1 Production production.deep-talk.ai Staging staging.deep-talk.ai

Step zero

Choosing a flow

The question that settles it is where the session is created and what the browser receives. Everything else follows from that.

Flow A

Public page

Sites with no authentication. Two HTML tags and nothing more: the session starts on its own. Trust rests on the domain origin and the appId.

Flow B

Private page

Your frontend already manages authenticated users. You start the session with init() and an identity your backend signs with HMAC. No endpoint of your own.

Flow C

Server to server

The clinical context never touches the browser. Your backend creates the session over the API and the frontend only receives an opaque, single-use token.

Side by side

Public page Private page Server to server
Who creates the session The script, automatically Your frontend, via init() Your backend, over the API
What the browser receives The appId only user_id, signature and metadata An opaque token only
Authentication Origin + appId HMAC-SHA256 signature Bearer client_secret
Embedding Script + <marco> element Script + JavaScript API <iframe> with the token in the URL
Backend required No For signing only Yes
Clinical context Not applicable Passes through the browser, signed Never leaves the server
Inline rendering No, always floating Yes, via component_id Yes, you control the iframe
Resumable sessions Yes, via localStorage Yes, by user_id/external_id No, by design

State names are identical across all three flows

All three publish state names in upper case (READY, SESSION_CREATED, STARTED), so a handler written for one flow can be reused in another without normalising. What changes between flows is which states exist, not how they are spelled.

Guide by flow

Installation

The session is identified by origin (domain) and appId, both configured with Marco in advance. The widget loads from a single script tag and starts by itself, without calling any JavaScript API.

When to use it
Landing pages, informational sites, portals with no login.
Configuration
Lives in Marco's backend, tied to your appId.
Rendering
Always a floating bubble anchored to the viewport.
Backend of your own
None needed.
Sequence of the flow between Browser, Marco BROWSER MARCO 01 Loads widget.js with the app-id on the <marco> element 02 Checks the origin matches the domain against the appId allowlist 03 SESSION_CREATED or SESSION_RELOADED if one was already in localStorage 04 Conversation STARTED · RESPONSE_CREATED 05 COMPLETED created_at and finished_at
The browser is the only actor on the client side. Your backend is not in the sequence at all: the domain origin and the appId are the whole check.

Basic integration

Add these two tags to your page, one in the head and one in the body. That's all: no other call is required.

html
<html>
  <head>
    <script
      src="https://production.deep-talk.ai/widget/widget.js"
      defer>
    </script>
  </head>
  <body>
    <marco
      id="my-marco-widget"
      api-base-url="https://production.deep-talk.ai/widget"
      app-id="YOUR_APP_ID">
    </marco>
  </body>
</html>

Replace YOUR_APP_ID with the identifier Marco gives you. Loading the script is enough: the widget appears automatically as a floating bubble.

Configuration lives in Marco's backend

Brand colours, behaviour and other settings are configured once with your Marco contact and stay tied to your appId. A change is applied on Marco's side and shows up on your page automatically, with no redeploy. The script URL is static too: product fixes arrive on their own.

Lifecycle and persistence

The session is created as soon as the widget finishes loading. Each browser keeps one continuous conversation, identified by its own session_id.

Anonymous identifier

The widget generates a random session identifier the first time it loads in a browser and stores it in localStorage. It is not tied to any real identity: it only allows the conversation to resume in that same browser, and serves as a correlation identifier for events and auditing.

Resuming

Reloading the page, navigating away and back, or closing and reopening the tab or browser (same profile): in all those cases the widget finds the stored identifier and, if the session is still within its inactivity timeout, resumes the conversation and publishes SESSION_RELOADED instead of SESSION_CREATED.

Inactivity timeout

A session idle beyond its configured limit closes automatically. The next time the widget loads in that browser there is no session left to resume, and it publishes SESSION_CREATED.

Explicit close

By default the session only ends through inactivity or a user action inside the widget. If you need to close it programmatically, use MarcoWidget.finish().

Optional API: MarcoWidget

For most integrations, the section above is all you need. If your application has to react to events or close the session explicitly, the script exposes an optional global namespace, MarcoWidget, on window.

javascript
MarcoWidget.on('READY', function () {
  console.log('Marco widget ready');
});

MarcoWidget.on('SESSION_CREATED', function (msg) {
  console.log('New session:', msg.session_id);
});

// Ends the session and clears the identifier from localStorage.
// The next load starts a fresh session.
MarcoWidget.finish();

MarcoWidget.on() subscribes a callback to state events and handles origin verification for you. In this flow the session is already active by the time the script loads, so MarcoWidget is only for listening to events and, optionally, closing the session.

Events

The widget publishes session state with postMessage, emitted from its iframe to the containing window. MarcoWidget.on() is a wrapper over that same stream.

State Published when
READY The script has finished loading. The session is still being created.
SESSION_CREATED A new session was created for this browser.
SESSION_RELOADED An existing active session was found and resumed in this browser.
STARTED The visitor sent a query and the interaction is under way.
RESPONSE_CREATED The agent produced a new response within the active session.
COMPLETED The session ended normally. Includes created_at and finished_at.
CANCELLED Ended early: visitor action, finish(), or inactivity.
ERROR The session or the call in progress ended because of a failure.

Terminal states

COMPLETED, CANCELLED and ERROR are terminal. None of them can be followed by further activity in the same session, and the next load of the widget will publish SESSION_CREATED.

Message shape

READY, STARTED, CANCELLED and ERROR share this base shape. session_id is absent on READY and present from SESSION_CREATED or SESSION_RELOADED onwards.

json
{
  "source": "marco-widget",
  "version": 1,
  "type": "STATE_CHANGE",
  "state": "READY",
  "session_id": "sess_9f2a1c",
  "timestamp": "2026-08-20T14:32:04Z"
}

COMPLETED adds two fields on top of the base: created_at and finished_at.

Errors

Errors arrive as an ERROR state event, in the same shape as above, with code and message fields.

Code Usual cause
unauthorized_origin The request came from an origin outside the allowlist for this appId.
invalid_app_id The appId matches no configured account, or was omitted.
rate_limited The request limit for this appId or IP address was exceeded.
network_error The request to Marco's backend failed: connectivity or timeout.

Security

Origin is this flow's primary trust signal. Every widget load is matched against the domains authorised for the given appId, and any request from an origin outside that list is rejected with unauthorized_origin. Marco also applies rate limits per appId and per IP address.

The script and its calls are served over HTTPS only, and your application must be served over HTTPS in every environment. The widget runs inside a sandboxed iframe with minimum permissions, managed automatically by the loader script.

If your application enforces a Content-Security-Policy, allow the widget domain in the three directives that govern script loading, iframe embedding and the widget's network calls.

http
Content-Security-Policy:
  script-src  https://production.deep-talk.ai;
  frame-src   https://production.deep-talk.ai;
  connect-src 'self' https://production.deep-talk.ai;

Common to all three flows

Data handling

Marco does not use conversation content or session metadata to train, fine-tune or improve any model.

Session content and audit records are retained for different periods, because they serve different purposes.

Data Purpose Retention
Session content metadata, transcript and summary. Serves the consultation itself. Deleted within 48 hours of the session reaching a terminal state.
Audit records Security monitoring and compliance evidence. session_id, external_id where it applies, timestamps and final state, with no clinical content. 6 years from creation, in line with the industry standard for healthcare access records.

session_id is the correlation identifier across all three flows, and links a session's events, its audit entries and the COMPLETED payload. In the private-page flow, external_id accompanies it where used. There is no separate correlation ID.

Stability of error codes

Across all three flows, code is a stable identifier and safe to use in business logic; new codes may be added over time, so always include a default case. message is for logs and debugging: its exact wording can change and should not be pattern-matched.

Common to all three flows

Environments

Marco offers two environments, with the same paths on both hosts.

Environment Host
Staging staging.deep-talk.ai
Production production.deep-talk.ai
text
// Public page and private page
Script:             https://<host>/widget/widget.js
API base URL:       https://<host>/widget

// Server to server
Session creation:   POST https://<host>/launch_token
Start chat:         GET  https://<host>/chat?launch_token=...

Isolation between environments

Each environment has its own credentials and its own list of authorised origins, and there are no cross-environment routes for any of them.

Flow Per-environment credential
Public page Its own appId. A staging appId is not valid against the production host.
Private page Its own shared secret. A signature computed with the staging secret is rejected in production.
Server to server Its own client_secret. A launch_token is only valid in the environment that issued it.

Store and rotate each environment's credentials separately.

Performance

Performance metrics and fixed thresholds, including time to first token and any other agreed latency guarantees, are defined in the SLA, a document separate from this guide.

Common to all three flows

Before going live

Before enabling any of the three flows, share the following with your Marco contact.

What we need What for Applies to
Origin(s) where the widget is embedded Origin verification. This is the list every load or call is matched against. All three flows
Primary and secondary brand colours Theming the interface to match your site. All three flows
Session duration Inactivity timeout and maximum lifetime, if the defaults do not suit you. All three flows
Shape of the metadata object on COMPLETED Agreeing the structured output your integration needs to receive when the session closes. Server to server

In return, Marco gives you each environment's credentials: the appId for the public-page flow, the shared secret for the private page, and the client_secret together with the app_id for server to server.

Not sure which flow is yours?

Write to sales@marco.care describing where your application lives and what context data you need to pass. We will confirm the flow and give you staging credentials so you can test before committing.

Update cookies preferences