open-wa v5 is alpha. Use v4.76.0 for mature production systems unless you are validating v5.
The Client APIAPI ExplorerLicensing

Webhooks for Business

Connect WhatsApp events to your backend using webhooks. Plain language guide with source-backed v5 payload examples.

Webhook Wally

Webhooks for Business

Webhooks let WhatsApp events flow into your existing services without writing custom runtime code. When something public happens in WhatsApp, open-wa sends an HTTP POST to your endpoint.

Use this guide when you want to send events into a CRM, helpdesk, automation platform, or your own backend.

Enable webhooks

Config file

The source-backed v5 alpha path is the webhook integration plugin loaded from config. The CLI still parses --webhook, but current source warns that CLI webhook registration parity is not restored, so do not rely on that flag for delivery in this alpha.

// wa.config.js
export default {
  plugins: ['@open-wa/integration-webhook'],
  pluginConfig: {
    webhook: {
      url: 'https://your-app.example/webhooks/open-wa',
      events: ['message.received', 'session.state.changed'],
      headers: {
        'X-Webhook-Secret': process.env.OPEN_WA_WEBHOOK_SECRET,
      },
    },
  },
};

Current v5 envelope

The v5 webhook integration sends one JSON envelope for every delivery:

{
  "webhookId": "0f5d6a52-2ff2-43d5-9d0d-9c0c8dd5f9d1",
  "sessionId": "sales",
  "event": "message.received",
  "payload": {
    "ctx": {
      "correlationId": "evt_01",
      "ts": 1700000000000
    },
    "message": {
      "id": "false_1234567890@c.us_ABC123",
      "from": "1234567890@c.us",
      "to": "0987654321@c.us",
      "body": "Hello!",
      "caption": "",
      "type": "chat",
      "timestamp": 1700000000,
      "fromMe": false,
      "isGroupMsg": false,
      "isMedia": false
    }
  },
  "timestamp": 1700000000123
}

Copy these three fields first:

Field to storeCurrent v5 pathWhy it matters
Senderpayload.message.fromRoute the conversation to the right customer/contact.
Text or captionpayload.message.body or payload.message.captionGet the human-readable message content.
Message idpayload.message.idDeduplicate retries and avoid processing the same message twice.

Test receiver with raw-body logging

Start with a receiver that logs the raw request before transforming it. That gives you a ground-truth payload when event shapes change.

import express from 'express';

type WebhookEnvelope = {
  webhookId: string;
  sessionId: string;
  event: string;
  payload: unknown;
  timestamp: number;
};

type MessageFields = {
  id?: string;
  from?: string;
  body?: string;
  caption?: string;
};

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null;
}

function parseEnvelope(value: unknown): WebhookEnvelope | null {
  if (!isRecord(value)) return null;
  if (typeof value.webhookId !== 'string') return null;
  if (typeof value.sessionId !== 'string') return null;
  if (typeof value.event !== 'string') return null;
  if (typeof value.timestamp !== 'number') return null;

  return {
    webhookId: value.webhookId,
    sessionId: value.sessionId,
    event: value.event,
    payload: value.payload,
    timestamp: value.timestamp,
  };
}

function getMessageFields(envelope: WebhookEnvelope): MessageFields {
  if (!isRecord(envelope.payload)) return {};
  const message = envelope.payload.message;
  if (!isRecord(message)) return {};

  return {
    id: typeof message.id === 'string' ? message.id : undefined,
    from: typeof message.from === 'string' ? message.from : undefined,
    body: typeof message.body === 'string' ? message.body : undefined,
    caption: typeof message.caption === 'string' ? message.caption : undefined,
  };
}

const app = express();
const rawBodies = new WeakMap<object, string>();

app.use(express.json({
  limit: '2mb',
  verify: (req, _res, buf) => {
    rawBodies.set(req, buf.toString('utf8'));
  },
}));

app.post('/webhooks/open-wa', (req, res) => {
  console.log('raw webhook body:', rawBodies.get(req) ?? '(empty body)');

  if (req.header('X-Webhook-Secret') !== process.env.OPEN_WA_WEBHOOK_SECRET) {
    return res.status(401).send('Unauthorized');
  }

  const envelope = parseEnvelope(req.body);

  if (!envelope) {
    return res.status(400).send('Invalid open-wa webhook envelope');
  }

  const message = getMessageFields(envelope);

  if (envelope.event === 'message.received') {
    console.log(`[${envelope.sessionId}] message ${message.id ?? 'unknown-id'}`);
    console.log('sender:', message.from ?? 'unknown-sender');
    console.log('text:', message.body || message.caption || '(no text)');
  }

  res.sendStatus(204);
});

app.listen(3000, () => {
  console.log('Webhook receiver listening on http://localhost:3000');
});

Do not remove the raw log until your receiver is stable.

For a one-request local test before connecting WhatsApp, send the current envelope shape to your receiver:

curl -i -X POST http://localhost:3000/webhooks/open-wa \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Secret: $OPEN_WA_WEBHOOK_SECRET" \
  -d '{"webhookId":"local-test","sessionId":"sales","event":"message.received","payload":{"message":{"id":"false_1234567890@c.us_ABC123","from":"1234567890@c.us","body":"Hello!"}},"timestamp":1700000000123}'

Expected local test output, illustrative and abbreviated:

Webhook receiver listening on http://localhost:3000
raw webhook body: {"webhookId":"...","sessionId":"sales","event":"message.received","payload":{...}}
[sales] message false_1234567890@c.us_ABC123
sender: 1234567890@c.us
text: Hello!

The local curl request should return HTTP/1.1 204 No Content. If your receiver returns 401, fix the shared secret header. If it returns any other non-2xx status when open-wa delivers a real event, open-wa treats that as a failure and retries according to the reliability rules below. After the one-request test passes, configure open-wa with your receiver URL and send one real WhatsApp message to confirm the production path.

Platform mappings

  1. Create a Zap with Webhooks by Zapier as the trigger.
  2. Choose Catch Hook.
  3. Copy the Custom Webhook URL.
  4. Configure open-wa with that URL.
  5. In later Zap steps, read fields from payload.message, not data.

Useful mappings:

Zapier fieldv5 webhook path
Contact / senderpayload.message.from
Message textpayload.message.body
Media caption fallbackpayload.message.caption
Deduplication keypayload.message.id
  1. Create a scenario.
  2. Add Webhooks > Custom webhook.
  3. Copy the webhook URL.
  4. Configure open-wa with that URL.
  5. Use the parsed JSON module output paths under payload.message.

For message automations, map payload.message.from, payload.message.body, and payload.message.id first.

  1. Create a workflow.
  2. Add a Webhook node with method POST.
  3. Copy the production webhook URL.
  4. Configure open-wa with that URL.
  5. Add a Set or Code node that reads payload.message.

Example n8n Code node expression:

return [{
  json: {
    sessionId: $json.sessionId,
    event: $json.event,
    sender: $json.payload?.message?.from,
    text: $json.payload?.message?.body || $json.payload?.message?.caption,
    messageId: $json.payload?.message?.id,
  },
}];

Common event names

The webhook plugin receives public runtime event names. Common current v5 names include:

EventPayload focus
message.receivedpayload.message contains the inbound message.
message.anypayload.message contains inbound or outbound message data.
message.deletedpayload.messageId, payload.chatId, and optional payload.by.
ack.changedpayload.ack contains acknowledgment details.
session.state.changedpayload.details.prev and payload.details.next describe state changes.
group.participants.changed.globalpayload.change contains the group participant change.

Reliability

open-wa treats a 2xx response as successful delivery. Network errors, timeouts, and non-2xx responses are failures.

By default, the webhook integration attempts the first delivery plus 3 retries. The retry delay starts at 1000ms and uses exponential backoff.

AttemptDelay before attempt
First deliverynone
Retry 11000ms
Retry 22000ms
Retry 34000ms

Webhook delivery is at least once. Store payload.message.id for message deduplication and include sessionId in your dedupe key when you run more than one account.

Next step: keep returning a quick 2xx, store the raw event or dedupe key first, then process slower CRM or automation work asynchronously.

Security

Send a shared secret header with every webhook:

// wa.config.js
export default {
  plugins: ['@open-wa/integration-webhook'],
  pluginConfig: {
    webhook: {
      url: 'https://your-app.example/webhooks/open-wa',
      headers: {
        'X-Webhook-Secret': process.env.OPEN_WA_WEBHOOK_SECRET,
      },
    },
  },
};

Verify that header before processing the body, and keep webhook URLs on HTTPS. Message payloads can include names, phone-number-based chat IDs, message text, and media metadata.

Wally the Walrus typing

Was this helpful?

Wally and his cute companion coffee mug are coding day and night to keep this up-to-date!

On this page