# Guide to tawk.to Webhooks: Features and Best Practices

tawk.to webhooks tell your systems what is happening in your live chat and help desk: a visitor starts a conversation, a chat ends, a transcript is ready, or a new ticket comes in. If you run tawk.to on your website, webhooks are how those conversations reach your CRM, ticketing, analytics or alerting without anyone copying them across by hand.

This guide covers how tawk.to webhooks are configured and signed, the four events and their payloads, the retry policy, and the practices that keep a tawk.to receiver reliable.

## What are tawk.to webhooks?

A tawk.to webhook has three parts: a URL, one or more events to send to it, and a secret key used to prove that a request came from tawk.to. When one of the chosen events happens, tawk.to sends a `POST` with a JSON payload specific to that event.

Every request is signed. tawk.to computes an HMAC-SHA1 of the raw request body with the webhook's secret key and sends it in the `X-Tawk-Signature` header as a hex string. It also sends an `X-Hook-Event-Id` header that identifies the event and stays the same when a delivery is retried.

HMAC-SHA1 is tawk.to's only option. SHA-1 is no longer recommended for new hashing designs, but as an HMAC keyed with a secret it is still fit for authenticating a message, and it is what you have to implement to verify tawk.to.

## tawk.to webhook features

| Feature | Details |
| --- | --- |
| Configuration | The **Admin** section of the tawk.to dashboard |
| Events | `chat:start`, `chat:end`, `chat:transcript_created`, `ticket:create` |
| Event identity | The `event` field in the body |
| Signature header | `X-Tawk-Signature` |
| Scheme | HMAC-SHA1, lowercase hex, no prefix |
| Signed content | The raw request body, nothing else |
| Secret | A secret key per webhook |
| Event ID header | `X-Hook-Event-Id`, unchanged across retries |
| Replay window | None. No timestamp is sent or signed |
| Response window | 30 seconds |
| Retries | Any non-2xx response or timeout, for up to 12 hours |

tawk.to publishes no source IP list for webhooks, and does not say how far apart its retries are.

## Common events

tawk.to documents four events:

| Event | Fires when |
| --- | --- |
| `chat:start` | The first message in a chat is sent by a visitor or agent. Whispers and system notifications such as triggers don't count |
| `chat:end` | A chat ends |
| `chat:transcript_created` | The full transcript of a chat session is generated after it has ended |
| `ticket:create` | A new ticket is created |

Every payload has `event`, `time` (an ISO 8601 date-time) and a `property` object with the `id` and `name` of the tawk.to property the event belongs to. The rest depends on the event:

* `chat:start` adds `chatId`, `domain`, `referrer`, a `visitor` object (`name`, `email`, `city`, `country`) and the first `message`, whose `type` is `msg`, `file` or `webrtc-call` and whose `sender.type` is `agent`, `visitor` or `system`.
* `chat:end` has the same fields without `message`.
* `chat:transcript_created` has `domain`, `referrer` and a `chat` object with the chat `id`, the `visitor` and a `messages` array. Each message has `type`, `msg`, `time`, a `sender` with a one-letter `t` (`a` agent, `v` visitor, `s` system), plus the sender's name `n` and `id` for agents, and optional `attchs` for files.
* `ticket:create` has a `requester` (`name`, `email`) and a `ticket` with `id`, a numeric `humanId`, `subject` and `message`.

Here is the `chat:start` example from tawk.to's documentation, written out as JSON:

```json
{
  "event": "chat:start",
  "chatId": "70fe3290-99ad-11e9-a30a-51567162179f",
  "time": "2019-06-28T14:03:04.646Z",
  "domain": "tawk.to",
  "referrer": "https://www.google.com/",
  "message": {
    "text": "Sample message",
    "type": "msg",
    "sender": { "type": "visitor" }
  },
  "visitor": {
    "name": "V1561719148780935",
    "email": "hello@test.com",
    "city": "jelgava",
    "country": "LV"
  },
  "property": {
    "id": "58ca8453b8a7e060cd3b1ecb",
    "name": "Bobs Burgers"
  }
}

```

tawk.to's own examples are JavaScript object literals, with unquoted keys and single-quoted strings, so they won't parse as JSON if you paste them into a test. The real deliveries are JSON. The visitor's `email` is documented as optional, so expect it to be missing for anonymous visitors.

## Setting up tawk.to webhooks

1. In the tawk.to dashboard, open the **Admin** section and go to the webhooks settings.
2. Create a webhook with your HTTPS endpoint URL and choose the events to send.
3. Copy the webhook's secret key and store it server-side, for example as `TAWK_WEBHOOK_SECRET`.
4. Start a test chat on your site, or create a ticket, to trigger an event.

tawk.to's documentation only says that webhooks are managed in the Admin section, so menu labels may differ slightly from the steps above.

If you use Hookdeck, create a tawk.to source, set the secret key as its **Webhook Signing Secret**, and use the source URL as the webhook URL in tawk.to. tawk.to signs every request, so turn verification on.

## Securing tawk.to webhooks

Compute an HMAC-SHA1 of the raw request body with the webhook's secret key, encode it as hex, and compare it with `X-Tawk-Signature`. tawk.to's samples compare with `===`, which leaks timing information. Use a constant-time comparison instead.

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

function verifyTawkSignature(rawBody, signatureHeader, secret) {
  if (!signatureHeader || !secret) return false;

  const expected = crypto.createHmac('sha1', secret).update(rawBody).digest('hex');

  const received = Buffer.from(signatureHeader.trim().toLowerCase(), 'utf8');
  const computed = Buffer.from(expected, 'utf8');
  return received.length === computed.length && crypto.timingSafeEqual(received, computed);
}

const app = express();

app.post('/webhooks/tawk-to', express.raw({ type: '*/*' }), (req, res) => {
  const secret = process.env.TAWK_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500); // fail closed

  if (!verifyTawkSignature(req.body, req.get('x-tawk-signature'), secret)) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(req.body.toString('utf8'));
  const event_id = req.get('x-hook-event-id'); // stable across retries: dedupe on it
  // Route on event.event. Persist, then process async.
  res.sendStatus(200);
});

```

tawk.to's Node sample only keeps the raw body for `application/json` requests. `express.raw({ type: '*/*' })` keeps it whatever the content type, so a change of header can't silently break verification.

The same check in Python:

```python
import hashlib
import hmac

def verify_tawk_signature(raw_body: bytes, signature_header, secret: str) -> bool:
    if not signature_header or not secret:
        return False
    expected = hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha1).hexdigest()
    received = signature_header.strip().lower().encode("utf-8")
    return hmac.compare_digest(received, expected.encode("utf-8"))

# In FastAPI: raw_body = await request.body()
# verify_tawk_signature(raw_body, request.headers.get("x-tawk-signature"),
#                       os.environ["TAWK_WEBHOOK_SECRET"])

```

To check your verifier, use the secret `tawk-test-secret` and this exact body:

```
{"event":"chat:start","chatId":"70fe3290-99ad-11e9-a30a-51567162179f","time":"2019-06-28T14:03:04.646Z"}

```

The signature should be:

```
e3390d44732bf3413e039ab6d11134cab6902b67

```

> **Make tawk.to webhooks production-ready.** [Hookdeck Event Gateway](/event-gateway) verifies the `X-Tawk-Signature` header, answers tawk.to well inside its 30-second window, and durably queues every chat and ticket event before your service sees it.

## tawk.to webhook limitations and pain points

### Twelve hours of retries, then nothing

**The Problem:** Events from a long outage are gone, and there is no documented way to ask for them again.

**Why It Happens:** tawk.to retries a failed delivery for up to 12 hours. A non-2xx response and a response slower than 30 seconds both count as failures. After that window, the documentation describes no further attempts and no redelivery feature.

**Workarounds:**

* Respond within 30 seconds by storing the event and processing it later.
* Monitor the endpoint so outages are fixed well inside 12 hours.
* Subscribe to `chat:transcript_created` as well as the chat events, so a missed `chat:end` can be recovered from the transcript.

**How Hookdeck Can Help:** Hookdeck accepts each event as soon as tawk.to sends it and stores it, then retries delivery to your service on a schedule you control and lets you replay anything that failed, so the 12-hour limit applies to Hookdeck's uptime rather than yours.

### No timestamp means no replay protection

**The Problem:** A captured request can be sent again later and still verify.

**Why It Happens:** The signature covers only the body. tawk.to sends no timestamp header and signs nothing else.

**Workarounds:**

* Deduplicate on `X-Hook-Event-Id`, which tawk.to keeps the same across retries.
* Keep the endpoint on HTTPS so requests can't be captured in transit.

**How Hookdeck Can Help:** Hookdeck's deduplication can drop repeated deliveries based on `X-Hook-Event-Id` before they reach your service.

### Examples that aren't JSON

**The Problem:** Tests built from tawk.to's documentation fail to parse.

**Why It Happens:** The documented examples are JavaScript object literals with unquoted keys, single quotes and, in one case, a trailing comma. Some also differ slightly from the field tables, such as the `type` field on the example ticket `requester`.

**Workarounds:**

* Convert the examples to JSON before using them as fixtures.
* Capture real deliveries from a test property and use those instead.
* Treat fields that the tables don't list as optional.

**How Hookdeck Can Help:** Hookdeck records every real delivery with its headers and body, so you can copy genuine tawk.to payloads into your tests and replay them against your endpoint.

## Best practices

### Verify against the raw body

The HMAC covers the exact bytes tawk.to sent. In Express, use `express.raw({ type: '*/*' })` and parse after verification. In Next.js, read `await request.text()`. In FastAPI, read `await request.body()`.

### Route on `event`

The event name is the top-level `event` field, and there is no event-type header. Switch on it, and log and acknowledge anything you don't recognise.

### Deduplicate on `X-Hook-Event-Id`

The header is stable across retries, which makes it the natural idempotency key. Store it before you process the event. See our [guide to webhook idempotency](/webhooks/guides/implement-webhook-idempotency).

### Expect missing visitor details

Visitor email is optional, and the generated visitor names (such as `V1561719148780935`) aren't real names. Match visitors to records in your CRM by email only when one is present.

### Acknowledge fast and process asynchronously

tawk.to treats a response after 30 seconds as a failure and retries it. Verify, persist the event, return `200`, and do the real work afterwards. See [why to process webhooks asynchronously](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

## Conclusion

tawk.to sends four events, `chat:start`, `chat:end`, `chat:transcript_created` and `ticket:create`, each signed with a hex HMAC-SHA1 of the raw body in `X-Tawk-Signature`. Verify against the raw bytes with a constant-time comparison, route on the `event` field, deduplicate on `X-Hook-Event-Id`, and answer within 30 seconds so the 12-hour retry window never runs out on you.

[Hookdeck Event Gateway](https://hookdeck.com) sits in front of your receiver, checks the tawk.to signature, and holds each event until your service has processed it.

[Get started with Hookdeck](https://dashboard.hookdeck.com/signup) for free and handle tawk.to webhooks reliably in minutes.