DMWire Docs
Early access

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:

Request
curl https://api.dmwire.ai/v1/webhooks/events \
  -H "X-API-Key: $DMWIRE_API_KEY"
Response (HTTP 200, captured 2026-10-17)
{
  "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:

Request
curl https://api.dmwire.ai/v1/webhooks \
  -H "X-API-Key: $DMWIRE_API_KEY"
Response (HTTP 200, captured 2026-10-17)
{
  "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

Request
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"}'
Response (HTTP 200, captured 2026-10-17)
{
  "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

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.

message.delivered
{
  "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

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

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

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

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

Webhook headers of a real delivery (standard HTTP headers such as Host left out)
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
HeaderMeaning
X-Webhook-EventThe event name
X-Webhook-DeliveryA unique id per delivery. Use it to ignore duplicates.
X-Webhook-Attempt1 for the first try, higher for retries
X-Webhook-TimestampUnix seconds when the delivery was signed
X-Webhook-Signaturev1= 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:

Formula
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.

Python
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)
Node.js
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.

Request
curl https://api.dmwire.ai/v1/webhooks/0/health \
  -H "X-API-Key: $DMWIRE_API_KEY"
Response (HTTP 200, captured 2026-10-17)
{
  "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.