Webhooks
Webhooks push every event to an https address of yours, as it happens. You only need them if you run your own server or an automation tool. An agent can listen with wait_for_messages instead.
Events
The live list, with descriptions:
curl https://api.dmwire.ai/v1/webhooks/events \
-H "X-API-Key: $DMWIRE_API_KEY"{
"success": true,
"events": [
"message.received",
"message.sent",
"message.delivered",
"message.read",
"message.failed",
"account.connected",
"account.disconnected",
"account.restricted",
"account.limit_reached"
],
"descriptions": {
"message.received": "A contact sent a message to one of your connected accounts.",
"message.sent": "A message you sent left your account.",
"message.delivered": "A message you sent reached the contact's phone.",
"message.read": "The contact read a message you sent.",
"message.failed": "A message you sent could not be delivered.",
"account.connected": "A WhatsApp or Telegram account finished connecting.",
"account.disconnected": "A connected account logged out or lost its session.",
"account.restricted": "The channel or our safety limits stopped an account from sending.",
"account.limit_reached": "You hit a daily limit of your plan; sends resume tomorrow or after an upgrade."
}
}Create a subscription
Ask your agent ("send my WhatsApp messages to https://example.com/hook"), or call POST /webhooks with url (https, publicly reachable) and subscribed_to (a list of the events above). Your subscriptions, as stored:
curl https://api.dmwire.ai/v1/webhooks \
-H "X-API-Key: $DMWIRE_API_KEY"{
"success": true,
"webhooks": [
{
"id": "0",
"name": "Webhook 2026-10-17T02:16:54.885Z",
"url": "https://your-server.example/dmwire-webhook",
"subscribed_to": [
"message.received",
"message.sent",
"message.failed",
"account.connected",
"account.disconnected"
],
"created_at": "2026-10-17T02:16:54.885Z",
"signing_enabled": true,
"signing_secret_created_at": "2026-10-17T02:17:40.181Z",
"retries_enabled": false,
"enabled": true
}
]
}Turn on signing with rotate_webhook_signing_secret (POST /webhooks/{webhookId}/signing-secret). It returns the secret once; keep it on your server.
Send a test event
curl \
-X POST https://api.dmwire.ai/v1/webhooks/0/test \
-H "X-API-Key: $DMWIRE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"event": "message.received"}'{
"success": true,
"webhook_id": "0",
"delivered": true
}Payloads
Each delivery is a JSON POST. These are real deliveries our test receiver got from DMWire (test events use sample data).
message.received
{
"event": "message.received",
"created_at": "2026-10-17T02:43:29.214Z",
"account_id": "Pn4kT7wQ2zX9cV1bN6mL",
"channel": "whatsapp_web",
"message": {
"id": "aB3dE5fG7hI9jK1lM2nO",
"direction": "inbound",
"text": "Hi, is the blue one still available?",
"media": null,
"status": "received",
"error": null
},
"contact": {
"id": "jk213msdfj1234asmf",
"phone": "+1234567890",
"name": "John Doe"
}
}message.sent, message.delivered, message.read
Same shape as message.received, with direction outbound and status sent, delivered or read.
{
"event": "message.delivered",
"created_at": "2026-10-17T02:43:29.550Z",
"account_id": "Pn4kT7wQ2zX9cV1bN6mL",
"channel": "whatsapp_web",
"message": {
"id": "aB3dE5fG7hI9jK1lM2nO",
"direction": "outbound",
"text": "Yes, it is. Want me to reserve it?",
"media": null,
"status": "delivered",
"error": null
},
"contact": {
"id": "jk213msdfj1234asmf",
"phone": "+1234567890",
"name": "John Doe"
}
}message.failed
{
"event": "message.failed",
"created_at": "2026-10-17T02:43:29.891Z",
"account_id": "Pn4kT7wQ2zX9cV1bN6mL",
"channel": "whatsapp_web",
"message": {
"id": "aB3dE5fG7hI9jK1lM2nO",
"direction": "outbound",
"text": "Yes, it is. Want me to reserve it?",
"media": null,
"status": "failed",
"error": "The recipient is not on WhatsApp"
},
"contact": {
"id": "jk213msdfj1234asmf",
"phone": "+1234567890",
"name": "John Doe"
}
}account.connected and account.disconnected
{
"event": "account.disconnected",
"created_at": "2026-10-17T02:43:30.232Z",
"account_id": "Pn4kT7wQ2zX9cV1bN6mL",
"channel": "whatsapp_web",
"account": {
"id": "Pn4kT7wQ2zX9cV1bN6mL",
"channel": "whatsapp_web",
"identifier": "+1234567890",
"status": "disconnected",
"reason": "logged_out"
}
}account.restricted
{
"event": "account.restricted",
"created_at": "2026-10-17T02:43:30.409Z",
"account_id": "Pn4kT7wQ2zX9cV1bN6mL",
"channel": "whatsapp_web",
"account": {
"id": "Pn4kT7wQ2zX9cV1bN6mL",
"channel": "whatsapp_web",
"identifier": "+1234567890",
"status": "restricted",
"reason": "blocked_by_whatsapp"
}
}account.limit_reached
{
"event": "account.limit_reached",
"created_at": "2026-10-17T02:43:30.569Z",
"account_id": null,
"channel": null,
"account": {
"id": null,
"channel": null,
"identifier": null,
"status": "limited",
"reason": "daily_send_cap"
}
}Headers
Content-Type: application/json
User-Agent: Webhook-Delivery/1.0
X-Webhook-Event: message.received
X-Webhook-Delivery: 972060ad-7869-4623-8037-9448a03db77c
X-Webhook-Attempt: 1
X-Webhook-Timestamp: 1792205009
X-Webhook-Signature: v1=a369567cad7193906d0a50282c3a5660c2ed2dd5ae25f8c6a7edf2b79fb5dedc| Header | Meaning |
|---|---|
X-Webhook-Event | The event name |
X-Webhook-Delivery | A unique id per delivery. Use it to ignore duplicates. |
X-Webhook-Attempt | 1 for the first try, higher for retries |
X-Webhook-Timestamp | Unix seconds when the delivery was signed |
X-Webhook-Signature | v1= followed by the signature, when signing is on |
Verify the signature
The signature is an HMAC SHA-256, in hex, of the timestamp, a dot, and the raw request body, keyed with your signing secret:
v1=hex( HMAC_SHA256( secret, X-Webhook-Timestamp + "." + raw_body ) )Use the raw body exactly as received, before any JSON parsing. Reject old timestamps (for example more than 5 minutes) to stop replays. Both snippets below were run against real DMWire deliveries and accepted them.
import hashlib, hmac, time
def verify(secret: str, headers: dict, raw_body: bytes) -> bool:
ts = headers.get("X-Webhook-Timestamp", "")
sig = headers.get("X-Webhook-Signature", "")
if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
return False
expected = "v1=" + hmac.new(secret.encode(), ts.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig)const crypto = require("crypto");
function verify(secret, headers, rawBody) {
const ts = headers["x-webhook-timestamp"] || "";
const sig = headers["x-webhook-signature"] || "";
if (!/^\d+$/.test(ts) || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const expected = "v1=" + crypto.createHmac("sha256", secret).update(ts + "." + rawBody).digest("hex");
return expected.length === sig.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
}Delivery health
Answer with any 2xx status quickly. A URL that keeps failing is paused automatically; get_webhook_health shows why, and reenable_webhook turns it back on.
curl https://api.dmwire.ai/v1/webhooks/0/health \
-H "X-API-Key: $DMWIRE_API_KEY"{
"success": true,
"webhook_id": "0",
"url": "https://your-server.example/dmwire-webhook",
"health": {
"consecutive_failures": 0,
"total_failures": 0,
"total_successes": 1,
"is_disabled": false,
"disabled_at": null,
"disabled_reason": null,
"last_failure": null,
"last_success_at": "2026-10-17T02:20:46.493Z",
"created_at": "2026-10-17T02:20:46.493Z",
"updated_at": "2026-10-17T03:24:37.837Z"
}
}Last checked against the live service on 2026-10-17.