# 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, …) |

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 with different labels and fields.

## Web search

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) in your knowledge base, not as a general open-web browse.

## Webhooks

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

### Setup

1. Open the chatbot widget → **Reply** → **What it can do** → **Add action** → **Webhook**.
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

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

Body:

```json
{
  "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": "alex@example.com",
    "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

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

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

```json
{
  "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.

### 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.
- For local testing, point the URL at a tunnel or your staging hook; keep HTTPS in production.
