Skip to Content

Website chat API

Connect your own form to the chat widget or build your own chat UI with the widget SDK, REST, typing, SSE and visitor activity.

Last updated on

You install the official widget in the app: see Putting the chat on your website. Snippet options are on Website chat settings. This page is for developers: the widget SDK for your own form and the REST layer for a custom chat UI. See OpenAPI, tag inbox.

Widget SDK

After the widget script has loaded, window.TillorInbox is ready. With it you pass on contact details from your own form (contact page, sales detail page, quote request). All methods wait until the widget config has loaded.

MethodDescription
ready()Promise that resolves as soon as the widget has loaded
getVisitor()Reads stored contact details from localStorage and filled-in widget fields; synchronous, no network
setVisitor({ name?, firstName?, lastName?, email?, phone?, message?, customFields? }, options?)Fills in chat fields without sending
open()Opens the chat window
close()Closes the chat window
toggle()Opens or closes the chat window
send({ body, name?, firstName?, lastName?, email?, phone?, message?, customFields?, open?, silent?, markSubmitted?, metadata? })Sends a message with contact details

getVisitor() returns this object:

FieldDescription
visitorIdID of this visitor in this browser (localStorage)
nameFull name (a single field in the widget)
firstName / lastNameDerived from name (first word / rest)
email, phoneContact details
customFieldsExtra fields from the widget settings
messageDraft in the message field
detailsSubmittedWhether the visitor form counts as filled in

Use getVisitor() to prefill your own form from localStorage (same browser, also after a page refresh).

Options for setVisitor (second argument):

OptionDefaultExplanation
openfalseOpen the chat window straight away
markSubmittedfalseHide the visitor form if all required fields are filled in
onlyEmptyfalseOnly overwrite empty fields
persistfalseKeep the details in localStorage (same browser, also after a new tab)

Options for send:

OptionDefaultExplanation
openfalseOpen the chat window before sending
silenttrue if open is falseNo sound, no error message in the widget; use the Promise for success or failure
markSubmittedtrueContact details do not need to be entered again in the widget afterwards
metadata-Optional hints for Tillie (not visible in the chat window); see metadata

With open: false (default) the chat button stays closed. The message does go to Tillor; your own form can say thanks or show an error. With open: true the chat window opens and the visitor sees the normal sending behaviour (sound, ticks, any widget errors).

Want to use your own button instead of the floating chat button? Set data-launcher="false" on the snippet and call TillorInbox.open().

metadata (booking form)

Use metadata when you have your own form on the website and Tillie should send only a confirmation, without follow-up questions (for example because the tab closes afterwards).

KeyTypeExplanation
forceHandoverbooleanConfirmation message and handover to a colleague, without intake questions. Also ignores the AI pause after a team message (see ignoreAiPause)
ignoreAiPausebooleanTillie also replies when she is paused in Automatic mode after a team message (30 minutes). Handy for silent form submission; combine with forceHandover or let Tillie do the intake as normal
handoverIntent"reservation" | "caravan_rental" | "sale" | "other"Optional: flow for the handover (default: derived automatically from the message)
sourcestringOptional integration label for your team (lowercase letters, digits, -, _; max. 64 characters)
await TillorInbox.send({
  name: "Jan Janssens",
  email: "jan@voorbeeld.be",
  phone: "+32 3 779 81 64",
  body: structuredBookingMessage,
  silent: true,
  metadata: {
    forceHandover: true,
    handoverIntent: "reservation",
    source: "website_booking_form",
  },
});

Replacing the contact form (silent sending)

Use this when you want to keep the existing contact form, but have messages go through Tillor without opening the chat window.

<form id="site-contact-form">
  <input name="name" type="text" required />
  <input name="email" type="email" required />
  <input name="phone" type="tel" />
  <textarea name="message" required></textarea>
  <button type="submit">Verstuur</button>
  <p id="contact-status" hidden></p>
</form>

<script>
  document.getElementById("site-contact-form").addEventListener("submit", async (event) => {
    event.preventDefault();
    const form = event.currentTarget;
    const status = document.getElementById("contact-status");
    const data = new FormData(form);

    try {
      await TillorInbox.send({
        name: String(data.get("name") ?? ""),
        email: String(data.get("email") ?? ""),
        phone: String(data.get("phone") ?? ""),
        body: String(data.get("message") ?? ""),
      });
      status.textContent = "Bedankt, we nemen snel contact op.";
      status.hidden = false;
      form.reset();
    } catch (error) {
      status.textContent = error instanceof Error ? error.message : "Versturen mislukt.";
      status.hidden = false;
    }
  });
</script>

You do not need to set open and silent: without open: true the window stays closed and Tillor treats the call as silent sending.

Linking the contact form (opening the chat)

Pass on fields and open the chat window, so that the visitor sees the conversation:

<form id="site-contact-form">
  <input name="name" type="text" required />
  <input name="email" type="email" required />
  <input name="phone" type="tel" />
  <textarea name="message" required></textarea>
  <button type="submit">Verstuur</button>
</form>

<script>
  document.getElementById("site-contact-form").addEventListener("submit", (event) => {
    event.preventDefault();
    const form = event.currentTarget;
    const data = new FormData(form);
    void TillorInbox.send({
      name: String(data.get("name") ?? ""),
      email: String(data.get("email") ?? ""),
      phone: String(data.get("phone") ?? ""),
      body: String(data.get("message") ?? ""),
      open: true,
    });
  });
</script>

Or only fill in fields and open the window, without sending straight away:

void TillorInbox.setVisitor(
  {
    name: "Jan Janssens",
    email: "jan@voorbeeld.be",
    phone: "+32 3 779 81 64",
    message: "Ik heb een vraag over de stacaravan te koop.",
  },
  { markSubmitted: true, open: true },
);

A sales page with immediate sending and opening the chat:

void TillorInbox.send({
  name: "Jan Janssens",
  email: "jan@voorbeeld.be",
  phone: "+32 3 779 81 64",
  body: "Ik heb interesse in deze stacaravan.",
  open: true,
});

Syncing live while someone is typing:

const form = document.getElementById("site-contact-form");
form.addEventListener("input", () => {
  void TillorInbox.setVisitor({
    name: form.name.value,
    email: form.email.value,
    phone: form.phone.value,
    message: form.message.value,
  });
});

Calling early (before the widget script)

If you call a method before the script has loaded, put the call in the queue:

<script>
  window.TillorInbox = window.TillorInbox || { q: [] };
  window.TillorInbox.q.push([
    "setVisitor",
    { name: "Jan", email: "jan@voorbeeld.be" },
    { open: true },
  ]);
</script>
<script src="https://tillor.eu/inbox-widget.js" data-org-id="org_abc123" data-widget-key="iwk_xxx" async defer></script>

Custom chat UI

If you build your own chat UI instead of the widget, you use the REST layer below.

Authentication: header X-Tillor-Inbox-Widget-Key (value from Settings > Integrations > Website chat).

Typing

POST /api/orgs/{orgId}/inbox/website/typing
X-Tillor-Inbox-Widget-Key: iwk_xxx
Content-Type: application/json

{
  "visitorId": "vis_…",
  "active": true,
  "visitorName": "Jan",
  "visitorEmail": "jan@voorbeeld.nl",
  "visitorPhone": "+32470123456"
}
  • active: true when the visitor is typing; false when the field is empty or the visitor stops (the widget sends false after about 4 seconds without input)
  • visitorName / visitorEmail / visitorPhone are optional
  • Use the same visitorId as with POST …/inbox/website/messages and GET …/inbox/website/messages

Realtime (SSE)

GET /api/orgs/{orgId}/inbox/website/subscribe?visitorId=vis_…
X-Tillor-Inbox-Widget-Key: iwk_xxx
Accept: text/event-stream
EventWhen
inbox:website:message-createdNew team or AI message
inbox:website:message-updatedTeam or AI message changed or deleted
inbox:website:typing-changeddata.staffTyping or data.staffOutboundSenderType changes

The payload shape is the same as org SSE (event, data, timestamp). Reconnect after a drop; there is no resumption.

The official widget uses SSE as its primary layer and fetches GET …/messages again every 5 seconds as a fallback (every 1 second with the panel open when SSE briefly drops). That way replies are marked as delivered as long as the visitor is on the page.

Visitor activity

POST /api/orgs/{orgId}/inbox/website/events
X-Tillor-Inbox-Widget-Key: iwk_xxx
Content-Type: application/json

event: chat_closed, tab_hidden or navigation (with navigation also url). Your team sees these events as grey info text in the conversation, see Conversations via Website chat.

Limits

POST …/inbox/website/messages and POST …/inbox/website/attachments/upload answer 429 when a visitor or a network sends too much. The body carries data.reason = INBOX_WIDGET_RATE_LIMITED and data.retryAfterSeconds. Limits per organization:

RoutePer visitor (visitorId)Per network (IP address)
messages10 per minute, 200 per day30 per minute, 600 per day
attachments/upload10 per hour50 per hour

Wait retryAfterSeconds before you try again, and show the visitor a message instead of retrying on your own.

See also

Was this page helpful?

On this page