# Tidio Webhooks

## When to Use This Skill

* How do I receive Tidio webhooks?
* How do I verify the `x-tidio-signature` header on a Tidio webhook?
* How do I handle `contact.created`, `ticket.created`, or `conversation.solved_by_lyro` events?
* Why is my Tidio webhook signature verification failing?
* How do I dedupe Tidio webhooks (`webhook_id`) and handle retries?

## Verification (core)

Tidio has no webhook SDK; verify manually. The header is
`x-tidio-signature: t=<unix seconds>,s=<hex>[,s=<hex>...]` (one `s` per active
webhook secret). The signature is **hex HMAC-SHA256** over the **raw body bytes**,
a literal `_`, then the `t` value, keyed with the secret's UTF-8 bytes. The
request is valid when **any** `s` matches.

```javascript
const crypto = require('crypto');

function verifyTidio(rawBody /* Buffer */, header, secret) {
  let t;
  const sigs = [];
  for (const item of (header || '').split(',')) {
    const i = item.indexOf('=');
    if (i === -1) continue;
    const key = item.slice(0, i), value = item.slice(i + 1);
    if (key === 't' && t === undefined) t = value;
    else if (key === 's') sigs.push(value);
  }
  if (!t || sigs.length === 0) return false;
  const signed = Buffer.concat([rawBody, Buffer.from('_'), Buffer.from(t, 'latin1')]);
  const expected = Buffer.from(
    crypto.createHmac('sha256', secret).update(signed).digest('hex'));
  return sigs.some((s) =>
    /^[0-9a-fA-F]{64}$/.test(s) &&
    crypto.timingSafeEqual(Buffer.from(s.toLowerCase()), expected));
}

```

Gotchas that cause most failures:

* Use the **raw body** (`express.raw({ type: '*/*' })`, `request.arrayBuffer()`, `await request.body()`). Never `JSON.parse` / `JSON.stringify` first.
* The body comes **first**, then `_`, then `t` (not `t.body`).
* Validate each `s` (exactly 64 hex characters) before comparing in constant time. Hex is case-insensitive.
* Missing header, missing `t` or `s`, malformed signature, or mismatch: return `401`.
* **No timestamp window by default.** Hookdeck retries and replays resend the original headers and timestamp, so a fixed window would reject legitimate deliveries. The examples offer an opt-in `TIDIO_WEBHOOK_TOLERANCE_SECONDS` (`t` is Unix seconds; unset or `0` = off).

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/tidio-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/tidio-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/tidio-webhooks/examples/fastapi/).

## Envelope and Event Types

Every delivery is an HTTP `POST` with JSON `{ created_at, project_public_key, topic, version, webhook_id, content }`.
Dispatch on `topic`; the event data is in `content`.

| `topic` | Triggered When |
| --- | --- |
| `contact.created` / `contact.updated` | A contact is created or updated |
| `conversation.solved_by_operator` | An operator marks a conversation as solved |
| `conversation.solved_by_lyro` | Lyro (AI) considers the conversation finished (`reason`: `handoff`, `taken_over_by_operator`, `inactive`) |
| `conversation.solved_automatically` | The Automatic Solve feature resolves a conversation |
| `conversation.operator_replied` | An operator message is persisted on the chat channel |
| `ticket.created` / `ticket.status_updated` | A ticket is created or its status changes |
| `ticket.operator_assigned` / `ticket.holder_changed` | A ticket gets a new operator or holder |
| `ticket.operator_replied` / `ticket.contact_replied` / `ticket.internal_note_added` / `ticket.replied` | Replies and notes on a ticket (`ticket.replied` covers all of them) |
| `customer_satisfaction.rated` | A customer satisfaction survey is submitted |

Changes made through the Tidio OpenAPI do **not** trigger webhooks. See
[references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/tidio-webhooks/references/overview.md) for payloads.

## Delivery Behaviour

* Respond `2xx` within **4 seconds**; process asynchronously.
* Non-2xx, timeout, or connection error: retried 9 times (5 s, 10 s, 30 s, 1 min, 3 min, 10 min, 30 min, 1 h, 3 h; about 4 h 45 min total).
* Ordering is **not guaranteed** (order by `created_at`); duplicates are possible, so dedupe on `webhook_id`.

## Environment Variables

```bash
TIDIO_WEBHOOK_SECRET=your_tidio_webhook_secret   # Settings > Developer > Webhooks
TIDIO_WEBHOOK_TOLERANCE_SECONDS=0                # optional opt-in replay window; 0 or unset = off

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 tidio --path /webhooks/tidio

```

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/tidio-webhooks/references/overview.md) - Tidio webhook concepts, envelope, and events
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/tidio-webhooks/references/setup.md) - Configure webhooks in the Tidio panel
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/tidio-webhooks/references/verification.md) - Signature verification details and gotchas