Webhooks: events to your server
Signed webhooks and REST hooks from reactive.chat – events, headers, body, signature verification with HMAC-SHA256, retries, timeouts, deactivation and verification examples in PHP, Node and Python.
Webhooks tell your server what happens in the chat: a new conversation, a new message, a handover to the team, a rating. There are two routes:
- Signed webhook – one target address per workspace, set up in your account, every delivery signed with HMAC-SHA256. The right route for your own systems.
- REST hooks – targets that an automation tool (Zapier, Make, n8n) subscribes and unsubscribes itself via the server API.
Events
- conversation.created – a conversation has been created. Channel in
channel(web,whatsapp,email…). - message.created – a message has been stored (from the visitor, the assistant, an agent or the system).
- handoff.requested – the conversation is to go to a human; with
reasonandtarget. - csat.submitted – the visitor has given a rating;
score(1or-1) andcomment(with masked personal data). - fehlerfall.erstellt – a new JavaScript error case has been detected on your website (see Manual).
Conversations from the website search (Smart Answer) only trigger events once they become visible as a chat.
Setting up the signed webhook
In your account under Settings → Integrations → Webhooks (permission: manage integrations):
- URL –
https://, at most 1000 characters, publicly reachable. One address per workspace. - Events – a selection; no selection means all.
- Signing secret – shown on the page (64 hex characters). Regenerate invalidates the previous one immediately; plan the switch so that your receiver briefly accepts both.
No test call is sent on saving. After switching it on, it can take up to about 15 seconds until events flow; events from before switching on are not delivered retroactively.
The delivery
POST /rc-webhook HTTP/1.1
Host: hooks.your-company.com
Content-Type: application/json; charset=utf-8
User-Agent: reactive.chat-webhook/1.0
X-RC-Event: csat.submitted
X-RC-Delivery: 01JAB3C4D5E6F7G8H9JKMNPQRS
X-RC-Timestamp: 1792503600
X-RC-Signature: sha256=3f0a1c...
{"id":"01JAB3C4D5E6F7G8H9JKMNPQRS","event":"csat.submitted","ts":1792503600,"site_key":"ABCDEFGHJKLMNPQRSTUVWX12","conversation_id":"01JAB2...","session_id":"s_...","visitor_id":"v_...","data":{"score":5,"comment":"Very helpful"}}
Headers:
- X-RC-Event – name of the event.
- X-RC-Delivery – ID of the delivery; stays the same on retries. Use it to detect duplicates.
- X-RC-Timestamp – Unix seconds.
- X-RC-Signature –
sha256=+hex(HMAC_SHA256(secret, X-RC-Timestamp + "." + raw_body)). The key is the displayed hex string itself.
Body:
- id (string) – same as
X-RC-Delivery. - event (string).
- ts (integer) – Unix seconds.
- site_key, conversation_id, session_id, visitor_id (string or
null). - data (object) – event-specific data. For
fehlerfall.erstellt:fall_id,project_id,signatur,meldung,datei,zeile,spalte,seite,browser,os,stack,erste_ms,url(link to the case in your account). Formessage.created:messagewithid,seq,kind,direction,author_type,author_name(only for agent and assistant),via,body(masked on the server),redacted,ts(milliseconds). Forhandoff.requested:reasonandtarget. Forcsat.submitted:scoreandcomment. Forconversation.created:channel.
Verifying the signature
Verify before any processing: timestamp at most 300 seconds old, signature over the raw body, constant-time comparison.
PHP:
<?php
$geheim = getenv('RC_WEBHOOK_SECRET');
$body = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_RC_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_RC_SIGNATURE'] ?? '';
if (!ctype_digit($ts) || abs(time() - (int) $ts) > 300) { http_response_code(401); exit; }
$soll = 'sha256=' . hash_hmac('sha256', $ts . '.' . $body, $geheim);
if (!hash_equals($soll, $sig)) { http_response_code(401); exit; }
$ereignis = json_decode($body, true);
// detect duplicates: remember $ereignis['id']
http_response_code(204);
Node.js (Express):
const crypto = require('crypto');
app.post('/rc-webhook', express.raw({ type: 'application/json' }), (req, res) => {
const ts = req.get('X-RC-Timestamp') || '';
const sig = req.get('X-RC-Signature') || '';
if (!/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(401);
const soll = 'sha256=' + crypto.createHmac('sha256', process.env.RC_WEBHOOK_SECRET)
.update(ts + '.' + req.body.toString('utf8')).digest('hex');
if (sig.length !== soll.length || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(soll))) {
return res.sendStatus(401);
}
const ereignis = JSON.parse(req.body.toString('utf8'));
res.sendStatus(204);
verarbeiteSpaeter(ereignis); // respond quickly, work afterwards
});
Python (Flask):
import hmac, hashlib, os, time
from flask import request, abort
@app.post("/rc-webhook")
def rc_webhook():
ts = request.headers.get("X-RC-Timestamp", "")
sig = request.headers.get("X-RC-Signature", "")
body = request.get_data() # raw bytes
if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
abort(401)
soll = "sha256=" + hmac.new(os.environ["RC_WEBHOOK_SECRET"].encode(),
ts.encode() + b"." + body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(soll, sig):
abort(401)
return "", 204
Responses, retries, deactivation
Respond quickly with a 2xx and process afterwards. Timeouts: 4 seconds for the connection setup, 10 seconds in total. Redirects are not followed.
- 2xx – delivered.
- 408, 429, 5xx, timeout, network error – retry after 1 s, 5 s, 25 s, 125 s and 625 s (up to six attempts in total), then discarded.
- 410 Gone – the integration is deactivated. Only use 410 if you really do not want any more webhooks.
- other 4xx – no retry.
Events older than one hour are no longer delivered. If errors pile up (5 in 60 seconds), delivery to your target pauses for 30 seconds; the jobs are retried afterwards. A chat never waits for your webhook.
The order of deliveries is not guaranteed. If needed, sort by ts or by the seq of the messages from the API.
REST hooks (Zapier, Make, n8n)
Automation tools subscribe their targets themselves via the server API. You can also use this directly:
curl -X POST https://reactive.chat/v1/hooks \
-H "Authorization: Bearer rc_live_..." \
-H "Content-Type: application/json" \
-d '{"event":"message.created","target_url":"https://hooks.your-company.com/rc","source":"n8n"}'
- POST /v1/hooks – a scope with write permission.
event(one of the events above),target_url(https://, at most 500 characters, public),source(zapier,makeorn8n, defaultzapier). Response 201 withid. The same combination of event and target is not created twice. At most 200 subscriptions per workspace. - GET /v1/hooks – list with
id,event,target_url,source,failures,created_at,last_ok_at. - DELETE /v1/hooks/{id} – unsubscribe.
Delivery of a REST hook:
POST https://hooks.your-company.com/rc
Content-Type: application/json
User-Agent: reactive.chat-n8n/1.0
X-RC-Event: message.created
X-RC-Delivery: 01JAB3...
{"event":"message.created","delivery_id":"01JAB3...","data":{"name":"message.created","ts":1792503600123,"site_key":"...","conversation_id":"...","session_id":"...","visitor_id":"...","message":{"id":"...","seq":7,"kind":"text","direction":"in","author_type":"bot","author_name":"Assistant","via":null,"body":"…","redacted":false,"ts":1792503600120}}}
- Not signed. Use a target address that is hard to guess, and check
X-RC-Deliveryfor duplicates. For your own systems, the signed webhook is the better route. datais the complete event: additionallyworkspace_id,delivery_idandattempt(attempt count).data.tsis in milliseconds here.dataofmessage.createdcontainsmessagewithid,seq,kind,direction,author_type,author_name(only for agent and assistant),via,body(masked on the server side),redacted,ts.handoff.requestedcarriesreasonandtarget,csat.submittedscoreandcomment,conversation.createdchannel.- A 410 only unsubscribes this one subscription. Hooks of a revoked API key are no longer served.
- As things stand, delivery requires the respective integration (Zapier, Make or n8n) to be switched on in the workspace under Integrations.
What does not exist
- No incoming webhook through which your server feeds visitor messages into a conversation. You send replies on behalf of your team via the server API or MCP.
- No transition window when renewing the signing secret.
- No event when a conversation is deleted.
Next: Server API
Pungkasan dianyari: