Developers
DM Handled calls your endpoint when something happens.
POST over https and answers with any 2xx status within
8 seconds. Do the slow work after answering.POST /api/v1/webhooks. Store the
signing secret (whsec_…) shown once.| Event | When |
|---|---|
contact.confirmed | A customer confirmed their email address (opened the link) or WhatsApp number (sent their code from it) in the chat |
consent.granted | A customer gave a consent in the chat |
consent.withdrawn | A consent was withdrawn — the customer replied STOP on WhatsApp, or your team pressed Withdraw |
handover.created | A customer needs a person; carries how to reach them, if they said |
conversation.needs_human | A conversation was handed to a person |
message.received | A person sent a message |
message.sent | The assistant replied |
POST /your/endpoint
Content-Type: application/json
X-DMH-Event: consent.granted
X-DMH-Event-Id: 57
X-DMH-Timestamp: 1790600000
X-DMH-Signature: sha256=5f2c…
{
"id": 57,
"event": "consent.granted",
"at": "2026-09-28T10:05:12.345678+00:00",
"data": {
"contact": {"ref": "1042", "dmh_id": 311, "name": "Ana García", "email": "ana@example.com",
"email_confirmed": true, "phone": "+34600111222", "phone_confirmed": false,
"lang": "es", "created_at": "…", "updated_at": "…"},
"consent": {"purpose": "reply_whatsapp", "granted": true,
"text": "Do you agree that Flamingo Real Estate may contact you on WhatsApp at +34600111222 about your enquiry, also after today? …",
"lang": "en", "given_at": "…", "source": "chat", "withdrawn_at": null,
"withdrawn_by": null, "origin": "chat", "active": true},
"conversation": {"id": 812, "channel": "web", "url": "https://app.dmhandled.com/app/c/812/"}
}
}contact.ref is your id when the chat knows the customer; otherwise it is null
and dmh_id identifies them — send dmh_id with your first
PUT /contacts/{ref} to attach your id.
{
"id": 58,
"event": "handover.created",
"at": "2026-09-28T10:06:40+00:00",
"data": {
"contact": {"ref": null, "dmh_id": 312, "name": "", "email": "ana@example.com",
"email_confirmed": true, "phone": null, "phone_confirmed": false, "lang": "es", …},
"reach": {"method": "email", "email": "ana@example.com", "email_verified": true,
"whatsapp": "", "whatsapp_verified": false, "consent": false},
"message": "Is Villa Mar still available in May?",
"reason": "nobody_answered",
"conversation": {"id": 813, "channel": "web", "url": "https://app.dmhandled.com/app/c/813/"}
}
}reach.method is email, whatsapp or empty when the customer did not say.
message is what they wrote that led to the hand-over.
The signature is an HMAC-SHA256, keyed with your secret, over {timestamp}.{raw body}.
Compute it on the raw bytes before parsing, compare in constant time, and refuse a timestamp
more than 5 minutes old.
import hashlib, hmac, time
def verified(secret: str, headers, raw_body: bytes) -> bool:
ts = headers["X-DMH-Timestamp"]
expected = "sha256=" + hmac.new(secret.encode(), f"{ts}.".encode() + raw_body,
hashlib.sha256).hexdigest()
fresh = abs(time.time() - int(ts)) < 300
return fresh and hmac.compare_digest(expected, headers["X-DMH-Signature"])const crypto = require("crypto");
function verified(secret, headers, rawBody /* Buffer */) {
const ts = headers["x-dmh-timestamp"];
const expected = "sha256=" + crypto.createHmac("sha256", secret)
.update(Buffer.concat([Buffer.from(ts + "."), rawBody])).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
const a = Buffer.from(expected), b = Buffer.from(headers["x-dmh-signature"] || "");
return fresh && a.length === b.length && crypto.timingSafeEqual(a, b);
}Anything but a 2xx within 8 seconds is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours,
6 hours and 12 hours, with the same id. Skip an id you have already processed.
Missed events can be fetched with GET /api/v1/events?after=….