Handoff webhook
Receive assistant handoffs in another system and securely validate the HMAC signature sent by TAU.
Handoff webhook
The handoff webhook allows TAU to send conversation data to your system when the assistant transfers a conversation. You can connect this event to a CRM, help desk, n8n, or any other automation that accepts HTTP requests.
Configure it in TAU
- Go to Settings → Handoff.
- Under Redirect to, select Webhook (URL).
- Enter a public HTTPS URL that accepts
POSTrequests with JSON. - Under Webhook Authentication, select Generate New Webhook API Key.
- Copy the key when it is displayed and store it in a secret manager. You will use the same key to validate incoming requests.
Keep the key only on your server or in your automation tool's protected credential store. Never place it in browser code, public pages, or repositories.
How the signature works
When an API key is configured, each request includes:
| Item | Value |
|---|---|
| Method | POST |
| Content | UTF-8 JSON |
| Header | X-TAU-Signature |
| Algorithm | HMAC-SHA256 |
| Format | 64 hexadecimal characters, without a sha256= prefix |
| Signed message | Raw request body, exactly as received |
| Secret | The webhook API key generated in TAU |
Validation follows this rule:
expected_signature = HEX(HMAC_SHA256(api_key, raw_body))Then compare expected_signature with the value of X-TAU-Signature using a timing-safe comparison function.
Calculate the HMAC from the raw body. Do not use JSON.stringify, reorder fields, or format the JSON again. Spaces, line breaks, and field order change the signature.
If no API key is configured, the X-TAU-Signature header is not sent. For production integrations, keep authentication enabled and reject unsigned requests.
The signature confirms that the body was generated with the shared key and was not modified. It does not contain a timestamp. If an action must not be repeated, also add duplicate-processing protection in the destination system.
Node.js example with Express
The example below preserves the received bytes before Express parses the JSON:
const express = require('express');
const {
createHmac,
timingSafeEqual,
} = require('crypto');
const app = express();
const secret = process.env.TAU_WEBHOOK_SECRET;
if (!secret) {
throw new Error('TAU_WEBHOOK_SECRET is not configured');
}
app.use(express.json({
verify: (req, _res, buffer) => {
req.rawBody = Buffer.from(buffer);
},
}));
function isValidSignature(rawBody, receivedSignature) {
if (!/^[a-f0-9]{64}$/i.test(receivedSignature)) {
return false;
}
const expectedSignature = createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return timingSafeEqual(
Buffer.from(expectedSignature, 'hex'),
Buffer.from(receivedSignature, 'hex'),
);
}
app.post('/webhooks/tau', (req, res) => {
const receivedSignature = req.get('X-TAU-Signature') || '';
if (!isValidSignature(req.rawBody, receivedSignature)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// The signature is valid. Process req.body here.
return res.sendStatus(204);
});
app.listen(3000);Python example with FastAPI
import hashlib
import hmac
import json
import os
from fastapi import FastAPI, HTTPException, Request, Response
app = FastAPI()
secret = os.environ["TAU_WEBHOOK_SECRET"].encode("utf-8")
@app.post("/webhooks/tau")
async def receive_tau_webhook(request: Request) -> Response:
raw_body = await request.body()
received_signature = request.headers.get("X-TAU-Signature", "")
expected_signature = hmac.new(
secret,
raw_body,
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(expected_signature, received_signature):
raise HTTPException(status_code=401, detail="Invalid signature")
payload = json.loads(raw_body)
# The signature is valid. Process payload here.
return Response(status_code=204)PHP example
<?php
$secret = getenv('TAU_WEBHOOK_SECRET');
$rawBody = file_get_contents('php://input');
$receivedSignature = $_SERVER['HTTP_X_TAU_SIGNATURE'] ?? '';
if (!$secret) {
http_response_code(500);
exit('Webhook is not configured');
}
$expectedSignature = hash_hmac('sha256', $rawBody, $secret);
if (!hash_equals($expectedSignature, $receivedSignature)) {
http_response_code(401);
exit('Invalid signature');
}
$payload = json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR);
// The signature is valid. Process $payload here.
http_response_code(204);Configure it in n8n
In n8n, use the workflow below to preserve the original body, calculate the HMAC with a protected credential, and stop invalid requests:
Webhook → Crypto (Hmac) → Code (safe comparison) → If
├─ true → Respond to Webhook (204) → your automation
└─ false → Respond to Webhook (401)1. Webhook node
Configure:
| Field | Value |
|---|---|
| HTTP Method | POST |
| Authentication | None |
| Respond | Using 'Respond to Webhook' Node |
| Options → Raw Body | enabled |
Copy the test URL while validating the workflow. When finished, publish the workflow and use its production URL in TAU.
Raw Body is required. It preserves the received body in the data binary field. Using only $json.body can produce a different signature.
2. Credential and Crypto node
Create an Hmac Secret credential and paste the API key generated in TAU into it. Then add a Crypto node with:
| Field | Value |
|---|---|
| Action | Hmac |
| Credential | the Hmac Secret credential created above |
| Binary File | enabled |
| Binary Property Name | data |
| Type | SHA256 |
| Encoding | HEX |
| Property Name | calculatedSignature |
The secret remains in the n8n credential and does not need to appear in workflow code.
3. Code node for safe comparison
Add a Code node, choose JavaScript and Run Once for All Items, then paste:
const crypto = require('crypto');
const item = $input.first();
const expected = String(item.json.calculatedSignature || '').trim();
const received = String(
item.json.headers?.['x-tau-signature'] || '',
).trim();
let signatureValid = false;
if (
/^[a-f0-9]{64}$/i.test(expected) &&
/^[a-f0-9]{64}$/i.test(received)
) {
signatureValid = crypto.timingSafeEqual(
Buffer.from(expected, 'hex'),
Buffer.from(received, 'hex'),
);
}
return [{
json: {
...item.json,
signatureValid,
},
binary: item.binary,
}];On n8n Cloud, the crypto module is available in the Code node. In self-hosted installations, the administrator must allow this module with NODE_FUNCTION_ALLOW_BUILTIN=crypto before using the example.
n8n normally exposes HTTP header names in lowercase, so the example reads x-tau-signature.
4. If and Respond to Webhook nodes
- In the If node, check whether
{{ $json.signatureValid }}istrue. - Connect the true output to a Respond to Webhook node using status
204or another2xxstatus. - Connect that Respond to Webhook node's output to your automation. n8n sends the response and continues the workflow with the input data.
- Connect the false output to another Respond to Webhook node using status
401. - Never connect the invalid branch to nodes that create contacts, notify staff, or perform other actions.
TAU waits approximately 10 seconds for the endpoint response. Return a 2xx status as soon as a valid request is accepted, before starting longer processing.
Test vector
Use this example only to verify your HMAC implementation:
Secret: test_tau_webhook
UTF-8 body: {"summary":"Human support requested."}
Expected signature: ace49ec4a8f20e3ecf6cd85dee254a27229bb9856108d8006b9f15f3b49fc8e0Any additional space or line break in the body produces a different signature.
Production checklist
- Use HTTPS URLs only.
- Store the API key in an environment variable or protected credential.
- Validate the signature before processing data or performing any action.
- Reject missing or invalid signatures with
401or403. - Compare signatures with a safe function such as
timingSafeEqual,compare_digest, orhash_equals. - Return a
2xxstatus for accepted valid requests. - Never log the API key or the complete conversation body.