Documentation
Webhooks
Get notified the moment something happens in HelpMesh — HMAC-signed, retry-aware, auto-disabling on persistent failure.
DocsWebhooks
Quickstart
Webhooks are configured per-tenant in Settings → Developer → Webhooks. You provide an HTTPS endpoint and pick which events to subscribe to. We POST a JSON payload with HMAC-SHA256 signature in the X-HelpMesh-Signature header.
- Create a webhook in the dashboard. Copy the signing secret (shown ONCE).
- Pick events. Subscribe to all groups or pick specific events.
- POST endpoint receives signed payloads. Verify the signature, then handle.
- Return 2xx within 30 seconds. Non-2xx triggers retries; 10 failures in 24h auto-disables the webhook and emails the workspace OWNER.
Verifying the signature
Every webhook delivery includes X-HelpMesh-Signature: t=<timestamp>,v1=<hex-hmac>. Compute HMAC-SHA256 over {timestamp}.{raw-body} using your secret; compare with constant-time equality. Reject events older than 5 minutes (replay protection).
Node.js (Express)
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const SECRET = process.env.HELPMESH_WEBHOOK_SECRET;
// Use raw body — JSON parse AFTER signature check.
app.post('/webhooks/helpmesh', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.header('x-helpmesh-signature') ?? '';
const m = sig.match(/t=(\d+),v1=([0-9a-f]+)/);
if (!m) return res.status(400).send('bad signature');
const [, ts, hex] = m;
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
return res.status(400).send('stale event');
}
const expected = crypto
.createHmac('sha256', SECRET)
.update(`${ts}.${req.body.toString('utf8')}`)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected, 'hex'), Buffer.from(hex, 'hex'))) {
return res.status(401).send('invalid signature');
}
const event = JSON.parse(req.body.toString('utf8'));
// ... handle event ...
res.status(200).send('ok');
});Python (FastAPI)
import hmac
import hashlib
import time
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
SECRET = b"<your-helpmesh-webhook-secret>"
@app.post("/webhooks/helpmesh")
async def helpmesh_webhook(request: Request):
sig = request.headers.get("x-helpmesh-signature", "")
parts = dict(p.split("=", 1) for p in sig.split(",") if "=" in p)
ts, v1 = parts.get("t"), parts.get("v1")
if not ts or not v1:
raise HTTPException(400, "bad signature")
if abs(time.time() - int(ts)) > 300:
raise HTTPException(400, "stale event")
body = await request.body()
expected = hmac.new(SECRET, f"{ts}.{body.decode()}".encode(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, v1):
raise HTTPException(401, "invalid signature")
event = await request.json()
# ... handle event ...
return {"ok": True}Event reference
Every event has the same envelope: { id, eventType, tenantId, aggregateId, payload, createdAt }. The payload shape varies per event.
| Event | Fires when |
|---|---|
| thread.created | A new ticket is created via API, email, widget, or any channel |
| thread.updated | Thread status, priority, assignee, or labels change |
| thread.assigned | A thread is assigned to a different agent or team |
| thread.snoozed | Thread is snoozed (and again on un-snooze) |
| thread.resolved | Thread status changes to RESOLVED |
| message.created | Any message is added to a thread (customer reply, agent reply, internal note, system event) |
| meeting.scheduled | Agent schedules a Google Meet from inside a thread |
| meeting.updated | Meeting time, title, or description is edited |
| meeting.cancelled | Meeting is cancelled (calendar event withdrawn from customer's calendar) |
| meeting.attendance_marked | Agent marks meeting as ATTENDED or NO_SHOW after the meeting time |
| meeting.recording_shared | Agent pastes a recording link and sends it to the customer |
| sla.breached | A thread crosses an SLA threshold without a response |
| inbox.created | A new inbox is provisioned |
| inbox.updated | Inbox config changes (default flag, channel settings, etc.) |
Retries and auto-disable
- Retry schedule: exponential backoff at 30s, 2m, 8m, 30m, 2h, 6h, 24h. Up to 7 attempts.
- Auto-disable: 10 consecutive failures within 24 hours disables the webhook. The workspace OWNER receives an email with the failing URL and last error.
- Re-enable: Settings → Developer → Webhooks → click the disabled webhook → "Re-enable". Test the endpoint with the inline "Test delivery" button before re-enabling in production.
- Idempotency: retries deliver the same
event.id. De-duplicate at your end if your handler isn't idempotent.
Testing locally
Use ngrok or Cloudflare Tunnel to expose your local handler over HTTPS, then point the webhook URL at the tunnel. The dashboard's "Test delivery" button sends a synthetic webhook.test event you can use for verification without depending on real ticket activity.
Need help?
Email support@helpmesh.io with the webhook delivery event.id and we'll trace the delivery server-side.