Datalumo
Docs · Chat actions
View markdown

Chat actions

Chat widgets can do more than answer from your sources. Under Reply → What it can do you can add optional actions:

Action What it does
Web search Lets the chatbot look up live pages on hosts you allowlist (max 5)
Webhook Sends structured details to your HTTPS endpoint (tickets, CRM, n8n, Make, …)
Act on the page Proposes a page action the visitor confirms in chat. Your page (or the WordPress plugin) listens and does the work.

Knowledge search stays always on. Secrets never appear in the public widget config API.

You can add up to 5 actions per chatbot, including multiple webhooks or page actions with different labels and fields.

Configure one or more hostnames (for example status.example.com). The model may only search those hosts. It is meant for live content that is not (yet) or cannot be in your knowledge base.

#Webhooks

When a visitor clearly wants the action (book a demo, open a ticket, leave details), the chatbot collects the fields you configured and POSTs JSON to your URL. It should ask for missing required values and must not invent placeholders.

#Setup

  1. Open the chatbot widget → ReplyWhat it can doAdd actionWebhook.
  2. Set a clear label (the model sees this) and an optional description of when to use it.
  3. Paste an HTTPS URL.
  4. Generate the signing secret (dl_wh_…) and store it on your receiver. Datalumo encrypts it at rest and only shows it once.
  5. Add the fields the model should collect (text, email, choice, or yes/no). At least one field is required; max 8.

#Request

POST https://your-app.example/hooks/datalumo
Content-Type: application/json
X-Datalumo-Timestamp: 1735689600
X-Datalumo-Signature: v1=…

Body:

{
  "tool_id": "01h…",
  "tool_label": "Create support ticket",
  "conversation_id": "…",
  "answer_id": "…",
  "page_url": "https://example.com/pricing",
  "user": { "id": "user-42" },
  "context": { "title": "Pricing" },
  "payload": {
    "email": "[email protected]",
    "message": "Billing failed on invoice 1042"
  }
}
Field Description
tool_id Stable id for this action on the widget
tool_label Label from the dashboard
conversation_id Chat thread id when available
answer_id Id for this assistant turn
page_url Page the visitor was on, when known
user { "id": "…" } when the visitor is identified; otherwise null
context Page context from the embed, or null
payload Values for the fields you configured (keys match your field keys)

Timeouts are short (a few seconds). Soft rate limit: about 20 calls per minute per widget action.

Datalumo also deduplicates successful deliveries for the same answer_id + tool_id for about a day, so a model retry does not POST twice. Still treat your receiver as idempotent on those ids.

#Verify the signature

Always check the signature before trusting the body. Compute HMAC-SHA256 of {timestamp}.{rawBody} with your signing secret, hex-encode it, and prefix v1=. Compare to X-Datalumo-Signature with a constant-time equals.

Reject requests if the timestamp is too old (for example more than five minutes) to limit replay.

PHP

$timestamp = $_SERVER['HTTP_X_DATALUMO_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_DATALUMO_SIGNATURE'] ?? '';
$rawBody = file_get_contents('php://input');

$expected = 'v1='.hash_hmac('sha256', $timestamp.'.'.$rawBody, $signingSecret);

if (! hash_equals($expected, trim($signature))) {
    http_response_code(401);
    exit;
}

if (abs(time() - (int) $timestamp) > 300) {
    http_response_code(401);
    exit;
}

$event = json_decode($rawBody, true);

Node.js

import crypto from 'node:crypto';

function verify(rawBody, timestamp, signatureHeader, secret) {
  const expected =
    'v1=' +
    crypto.createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');

  const a = Buffer.from(expected);
  const b = Buffer.from(String(signatureHeader || '').trim());

  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Express-style: use the raw body string, not a re-serialised object.
if (!verify(rawBody, req.get('X-Datalumo-Timestamp'), req.get('X-Datalumo-Signature'), secret)) {
  return res.status(401).end();
}

Use the exact raw request body bytes. Re-encoding JSON can change whitespace and break the signature.

#Optional reply

Respond with HTTP 2xx. You may return a small JSON body (max 2 KB) so the chatbot can confirm with a reference:

{
  "ok": true,
  "reference": "TICKET-1842",
  "message": "We opened ticket TICKET-1842."
}
Field Limits Description
ok bool Defaults to true if omitted
reference string, max 80 Short id the assistant can quote
message string, max 280 Optional visitor-safe confirmation

Non-JSON or oversized bodies are treated as plain success (no reference). Keep replies visitor-safe; do not return secrets or stack traces.

#Act on the page

Use this when the write has to happen in the visitor's browser: their cart, a draft, anything tied to a page session. Datalumo does not POST to your server. The model collects the same field types as a webhook. The reply includes an actions list. The official embed shows a confirm card with a way to stop (Not now, or Cancel while it runs). A confirm click is what fires the event.

#Setup

  1. Open the chatbot widget → ReplyWhat it can do.
  2. Open Plugin actions and pick a preset from an official integration (WordPress today). That fills the event name and fields.
  3. Or Add actionAct on the page and set a custom event name. That is the name your page listens for.
  4. Add fields only when the action needs values (max 8). Navigation actions can have none.

#Event

After the visitor confirms:

window.addEventListener('datalumo:action', (event) => {
  const { name, widget, conversation_id, answer_id, tool_id, run, payload, respond } = event.detail;

  if (name !== 'add_to_cart') {
    return;
  }

  event.preventDefault();

  // Do the write in this page (the visitor's session).
  respond({ ok: true, message: 'Added to your cart.' });
});
Field Description
name Event slug from the dashboard
widget Widget key (org/widget)
conversation_id Chat thread id when available
answer_id Id for this assistant turn
tool_id Stable id for this action on the widget
run Incremented on each confirm. Send it back on datalumo:action-result
payload Values for the fields you configured
respond Call with { ok, message } when you finish, or { status: "choices", message, choices } if the visitor must pick first. Only counts after preventDefault(), or from chat.on('action') after the window event. First successful call wins.

When the write needs a pick (a product variant, a time slot), return choices instead of a failure:

respond({
  ok: false,
  status: 'choices',
  message: 'Which option would you like?',
  layout: 'options',
  choices: [
    { id: 'v_1843', label: 'Small / Blue' },
    { id: 'v_1844', label: 'Medium / White' },
  ],
  url: 'https://store.example/product/v-neck',
});

The official embed shows the prompt on a card. A plain option list fires on click. Choices with image render as thumbnails; description or meta render as a list, then the visitor confirms with the action label. Not now stops the action without firing. A pick re-dispatches the same event with payload.choice set to that id. Do not invent a new event name.

A choice may include its own choices array for the next question (colour, then that colour's sizes). The embed walks those steps in the bubble and only re-fires when the visitor picks a leaf. After more than one step, payload.choice is the path:

respond({
  status: 'choices',
  message: 'Which Color?',
  choices: [
    {
      id: 'attribute_pa_color:blue',
      label: 'Blue',
      message: 'Which Size?',
      choices: [
        { id: 'attribute_pa_size:small', label: 'Small' },
        { id: 'attribute_pa_size:large', label: 'Large' },
      ],
    },
  ],
});

// leaf pick:
payload.choice === ['attribute_pa_color:blue', 'attribute_pa_size:small']

If you only know the next question, keep returning a flat list. The embed writes encoded ids (attribute_pa_color:blue) onto payload and keeps the last sent payload when you return another choices list.

Field Description
status choices means not done, not an error
message Prompt above the chips
layout options (default) or cards. V1 always draws option chips. Extra fields on a choice (description, image, meta) are kept for a later card layout
choices Objects with id (what you get back) and label (what the visitor sees). A choice may nest choices plus message for the next step. Max 8 per step. If you have more, omit choices and send url
url Optional http(s) link (product page, overflow). Shown next to the prompt

A choice list is always objects. Do not send ["Small", "Medium"].

You can fire a later result when you cannot call respond() yet. Include run from the event (the embed only applies a result to the in-flight confirm, never a pending card):

window.dispatchEvent(new CustomEvent('datalumo:action-result', {
  detail: { answer_id, tool_id, run, ok: true, message: 'Added to your cart.' },
}));

If you call respond(), stop there. The first respond after the event is claimed (preventDefault) wins. Do not also fire datalumo:action-result for the same confirm.

url on a result or choice must be an absolute http:// or https:// link. Protocol-relative (//…) and relative paths are ignored.

If nothing handles the event, the chip says this site did not handle that action.

A page can fake this event. Treat a confirm like a user click. Fine for add to cart. Do not use it for checkout or changing an email.

Chat handles also support chat.on('action', handler) / off. Same detail object.

Headless replies include actions next to text and sources. Dispatch the event yourself, or mount the official chat so the chip does it.

#Tips

  • Prefer a clear label and description so the model knows when to call the action.
  • Collect only fields you need. Required fields are enforced before the POST or the proposal.
  • For local testing, point a webhook URL at a tunnel or your staging hook; keep HTTPS in production.
  • For page actions, put real product or post ids on the synced page (external_id) so the model can copy them.