> ## Documentation Index
> Fetch the complete documentation index at: https://docs.waply.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Register webhooks for real-time Waply events

> Register HTTPS endpoints to receive real-time event notifications from Waply for messages, conversations, contacts, and broadcast completions.

Webhooks let you receive real-time notifications when events happen in your Waply account. Instead of polling the API, you register an HTTPS endpoint and Waply sends a POST request to it whenever a subscribed event occurs. This is the recommended way to react to inbound messages, conversation updates, and broadcast completions.

## Register a webhook

Send a `POST` request to `/webhooks` with the URL you want to receive events at and the list of event types you want to subscribe to.

### POST /webhooks

<ParamField body="url" type="string" required>
  The HTTPS URL Waply will send event payloads to. Must use `https://`. Plain `http://` URLs are rejected.
</ParamField>

<ParamField body="events" type="string[]" required>
  Array of event type strings to subscribe to. See [Event types](#event-types) for the full list.
</ParamField>

<CodeGroup>
  ```bash cURL theme={null}
  curl --request POST \
    --url https://api.waply.io/v1/webhooks \
    --header 'Authorization: Bearer YOUR_API_KEY' \
    --header 'Content-Type: application/json' \
    --data '{
      "url": "https://yourapp.com/waply/webhook",
      "events": ["message.received", "conversation.resolved"]
    }'
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch('https://api.waply.io/v1/webhooks', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.WAPLY_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      url: 'https://yourapp.com/waply/webhook',
      events: ['message.received', 'conversation.resolved'],
    }),
  });

  const webhook = await response.json();
  console.log(webhook.secret); // store this securely
  ```

  ```python Python theme={null}
  import requests, os

  response = requests.post(
      'https://api.waply.io/v1/webhooks',
      headers={'Authorization': f'Bearer {os.environ["WAPLY_API_KEY"]}'},
      json={
          'url': 'https://yourapp.com/waply/webhook',
          'events': ['message.received', 'conversation.resolved'],
      },
  )

  webhook = response.json()
  print(webhook['secret'])  # store this securely
  ```
</CodeGroup>

The response includes a `secret` field. Store this secret securely — you will use it to verify that incoming webhook requests genuinely come from Waply.

```json theme={null}
{
  "id": "wh_01HXYZ",
  "url": "https://yourapp.com/waply/webhook",
  "events": ["message.received", "conversation.resolved"],
  "secret": "whsec_a4b8c16d...",
  "created_at": "2026-04-16T10:00:00Z"
}
```

<Warning>
  The `secret` is only returned in the response to the initial `POST /webhooks` request. It is not retrievable afterwards. If you lose it, delete the webhook and create a new one.
</Warning>

## List webhooks

### GET /webhooks

Returns all webhooks registered on your account.

<CodeGroup>
  ```bash cURL theme={null}
  curl --request GET \
    --url https://api.waply.io/v1/webhooks \
    --header 'Authorization: Bearer YOUR_API_KEY'
  ```

  ```javascript JavaScript theme={null}
  const response = await fetch('https://api.waply.io/v1/webhooks', {
    headers: { 'Authorization': `Bearer ${process.env.WAPLY_API_KEY}` },
  });
  const { data } = await response.json();
  ```
</CodeGroup>

## Event types

Subscribe to any combination of the following event types when registering a webhook:

| Event type              | Triggered when                                    |
| ----------------------- | ------------------------------------------------- |
| `message.received`      | A new inbound message arrives on any channel      |
| `message.delivered`     | An outbound message is delivered to the recipient |
| `message.read`          | An outbound message is read by the recipient      |
| `conversation.assigned` | A conversation is assigned to an agent or team    |
| `conversation.resolved` | A conversation is marked as resolved              |
| `contact.created`       | A new contact is created                          |
| `broadcast.completed`   | A broadcast finishes sending to all recipients    |

## Webhook payload

Every event Waply sends to your endpoint has the same envelope structure:

```json theme={null}
{
  "event": "message.received",
  "timestamp": "2026-04-16T10:05:23Z",
  "data": {
    "id": "msg_01HABC",
    "conversation_id": "conv_01HDEF",
    "direction": "inbound",
    "type": "text",
    "content": "Hello, I need help with my order.",
    "status": "delivered",
    "created_at": "2026-04-16T10:05:23Z"
  }
}
```

The shape of the `data` object varies by event type and matches the corresponding API object (Message, Conversation, Contact, or Broadcast).

## Verify the webhook signature

Waply signs every webhook request with an HMAC-SHA256 signature so you can confirm it came from Waply and not a third party. The signature is sent in the `X-Waply-Signature` header as a hex string.

To verify the signature, compute the HMAC-SHA256 of the raw request body using your webhook secret and compare it to the value in the header.

<CodeGroup>
  ```javascript Node.js theme={null}
  const crypto = require('crypto');

  function verifySignature(rawBody, secret, signatureHeader) {
    const expected = crypto
      .createHmac('sha256', secret)
      .update(rawBody)
      .digest('hex');

    // Use timingSafeEqual to prevent timing attacks
    const expectedBuf = Buffer.from(expected, 'hex');
    const receivedBuf = Buffer.from(signatureHeader, 'hex');

    if (expectedBuf.length !== receivedBuf.length) return false;
    return crypto.timingSafeEqual(expectedBuf, receivedBuf);
  }

  // Express example
  app.post('/waply/webhook', express.raw({ type: 'application/json' }), (req, res) => {
    const signature = req.headers['x-waply-signature'];

    if (!verifySignature(req.body, process.env.WAPLY_WEBHOOK_SECRET, signature)) {
      return res.status(401).send('Invalid signature');
    }

    const event = JSON.parse(req.body);
    console.log('Verified event:', event.event);
    res.sendStatus(200);
  });
  ```

  ```python Python theme={null}
  import hashlib
  import hmac
  import os

  def verify_signature(raw_body: bytes, secret: str, signature_header: str) -> bool:
      expected = hmac.new(
          secret.encode('utf-8'),
          raw_body,
          hashlib.sha256,
      ).hexdigest()
      return hmac.compare_digest(expected, signature_header)

  # Flask example
  from flask import Flask, request, abort
  import json

  app = Flask(__name__)

  @app.route('/waply/webhook', methods=['POST'])
  def webhook():
      signature = request.headers.get('X-Waply-Signature', '')
      if not verify_signature(request.data, os.environ['WAPLY_WEBHOOK_SECRET'], signature):
          abort(401)

      event = request.get_json()
      print('Verified event:', event['event'])
      return '', 200
  ```
</CodeGroup>

<Note>
  Always use your raw request body (before JSON parsing) when computing the signature. Parsing and re-serialising the JSON may change whitespace or key ordering, which will cause the comparison to fail.
</Note>

## Delete a webhook

### DELETE /webhooks/{id}

Unregisters a webhook. Waply immediately stops sending events to the associated URL.

<ParamField path="id" type="string" required>
  The ID of the webhook to delete.
</ParamField>

<CodeGroup>
  ```bash cURL theme={null}
  curl --request DELETE \
    --url https://api.waply.io/v1/webhooks/wh_01HXYZ \
    --header 'Authorization: Bearer YOUR_API_KEY'
  ```

  ```javascript JavaScript theme={null}
  await fetch('https://api.waply.io/v1/webhooks/wh_01HXYZ', {
    method: 'DELETE',
    headers: { 'Authorization': `Bearer ${process.env.WAPLY_API_KEY}` },
  });
  ```
</CodeGroup>

```json theme={null}
{
  "success": true
}
```
