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

# Webhooks

> Configure webhooks to receive inbound messages and delivery updates

Webhooks allow your application to receive real-time notifications when events occur, such as incoming messages, delivery status changes, or partner invitation updates.

Zavu supports two types of webhooks:

* **Sender Webhooks**: Receive events for a specific sender (messages, conversations)
* **Project Webhooks**: Receive project-level events (partner invitations)

## Project Webhooks

Project webhooks are ideal for receiving notifications about project-level events, such as when a partner invitation is completed. This is particularly useful if you're using the Partner Invitations API to onboard clients.

### Via Dashboard

1. Go to [Dashboard](https://dashboard.zavu.dev) and select your project
2. Navigate to **Webhooks** in the sidebar
3. In the "Project Webhook" section, click **Configure**
4. Enter your webhook URL and select the events you want to receive
5. Save - you'll receive a signing secret

### Via API

Configure a project webhook using the invitations API:

```bash cURL theme={null}
curl -X POST https://api.zavu.dev/v1/invitations/webhook \
  -H "Authorization: Bearer zv_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.com/webhooks/zavu",
    "events": ["invitation.status_changed"]
  }'
```

Response:

```json theme={null}
{
  "webhook": {
    "url": "https://your-app.com/webhooks/zavu",
    "events": ["invitation.status_changed"],
    "secret": "whsec_abc123def456...",
    "active": true
  }
}
```

### Managing Project Webhooks

**Get current configuration:**

```bash cURL theme={null}
curl https://api.zavu.dev/v1/invitations/webhook \
  -H "Authorization: Bearer zv_live_xxx"
```

**Update webhook:**

```bash cURL theme={null}
curl -X PATCH https://api.zavu.dev/v1/invitations/webhook \
  -H "Authorization: Bearer zv_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://new-url.com/webhooks",
    "active": true
  }'
```

**Remove webhook:**

```bash cURL theme={null}
curl -X DELETE https://api.zavu.dev/v1/invitations/webhook \
  -H "Authorization: Bearer zv_live_xxx"
```

### Project Webhook Events

| Event                       | Description                                                                       |
| --------------------------- | --------------------------------------------------------------------------------- |
| `invitation.status_changed` | A partner invitation status changed (pending, in\_progress, completed, cancelled) |

### Project Webhook Payload

```json theme={null}
{
  "id": "evt_1234567890_abc123",
  "type": "invitation.status_changed",
  "timestamp": 1705312200000,
  "projectId": "prj_xyz789",
  "data": {
    "invitationId": "inv_abc123",
    "clientName": "John Doe",
    "clientEmail": "john@example.com",
    "currentStatus": "completed",
    "newSenderId": "snd_new123",
    "wabaAccountId": "waba_123456"
  }
}
```

<Note>
  Project webhooks are independent of sender webhooks. You can have both configured simultaneously.
</Note>

***

## Sender Webhooks

Each sender can have one webhook configured. Webhooks are managed as part of the sender resource.

### Via Dashboard

1. Go to [Dashboard](https://dashboard.zavu.dev) and select your project
2. Navigate to your Sender's settings
3. Click **Add Webhook**
4. Enter your webhook URL and select the events you want to receive
5. Save - you'll receive a signing secret

### Via API

Configure a webhook when creating a new sender:

```bash cURL theme={null}
curl -X POST https://api.zavu.dev/v1/senders \
  -H "Authorization: Bearer zv_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "My Sender",
    "phoneNumber": "+15551234567",
    "webhookUrl": "https://your-app.com/webhooks/zavu",
    "webhookEvents": ["message.inbound", "message.delivered", "message.failed"]
  }'
```

Response:

```json theme={null}
{
  "id": "snd_abc123",
  "name": "My Sender",
  "phoneNumber": "+15551234567",
  "isDefault": false,
  "webhook": {
    "url": "https://your-app.com/webhooks/zavu",
    "events": ["message.inbound", "message.delivered", "message.failed"],
    "secret": "whsec_abc123def456...",
    "active": true
  },
  "createdAt": "2024-01-15T10:30:00.000Z"
}
```

<Warning>
  Store your webhook secret securely. It's only shown once when you create the sender with a webhook. You'll need it to verify webhook signatures.
</Warning>

## Available Events

### Inbound (Receiving Messages)

| Event              | Description                                                 |
| ------------------ | ----------------------------------------------------------- |
| `conversation.new` | First message from a new contact (new conversation started) |
| `message.inbound`  | A customer sent you a message                               |

### Outbound (Delivery Tracking)

| Event               | Description                                            |
| ------------------- | ------------------------------------------------------ |
| `message.queued`    | Your message was queued for delivery                   |
| `message.sent`      | Your message was sent to the carrier                   |
| `message.delivered` | Your message was delivered to the recipient            |
| `message.read`      | Your message was read by the recipient (WhatsApp only) |
| `message.failed`    | Message delivery failed                                |

### Templates

| Event                     | Description                                                             |
| ------------------------- | ----------------------------------------------------------------------- |
| `template.status_changed` | A WhatsApp template status changed (draft, pending, approved, rejected) |

### Partner Invitations (Project Webhook)

| Event                       | Description                                                                       |
| --------------------------- | --------------------------------------------------------------------------------- |
| `invitation.status_changed` | A partner invitation status changed (pending, in\_progress, completed, cancelled) |

<Info>
  The `invitation.status_changed` event is delivered via **Project Webhooks**, not Sender Webhooks. See the [Project Webhooks](#project-webhooks) section above for configuration.
</Info>

<Tip>
  Subscribe to `message.inbound` to receive all customer messages. Add `conversation.new` to get notified when a new contact messages you for the first time (useful for lead tracking). The outbound events are optional and used for tracking delivery status.
</Tip>

## Webhook Payload

All webhook payloads follow the same structure:

```json theme={null}
{
  "id": "evt_1234567890_abc123",
  "type": "message.inbound",
  "timestamp": 1705312200000,
  "senderId": "snd_abc123",
  "projectId": "prj_xyz789",
  "data": {
    // Event-specific data
  }
}
```

| Field       | Description                          |
| ----------- | ------------------------------------ |
| `id`        | Unique event identifier              |
| `type`      | Event type (e.g., `message.inbound`) |
| `timestamp` | Unix timestamp in milliseconds       |
| `senderId`  | The Sender that received this event  |
| `projectId` | Your project ID                      |
| `data`      | Event-specific payload               |

## Handling Webhooks

Your webhook endpoint must:

1. **Respond with 2xx status** within 30 seconds
2. **Verify the signature** to ensure the request is from Zavu
3. **Process asynchronously** for long-running tasks

<CodeGroup>
  ```typescript TypeScript (Express) theme={null}
  import express from "express";
  const app = express();

  app.post("/webhooks/zavu", express.json(), (req, res) => {
    // 1. Verify signature (see Security guide)
    const signature = req.headers["x-zavu-signature"];
    if (!verifySignature(signature, req.body, process.env.WEBHOOK_SECRET)) {
      return res.status(401).send("Invalid signature");
    }

    // 2. Acknowledge receipt immediately
    res.status(200).send("OK");

    // 3. Process the event asynchronously
    const event = req.body;
    processEvent(event).catch(console.error);
  });

  async function processEvent(event) {
    switch (event.type) {
      case "message.inbound":
        await handleInboundMessage(event.data);
        break;
      case "message.delivered":
        await handleDeliveryConfirmation(event.data);
        break;
      case "message.failed":
        await handleDeliveryFailure(event.data);
        break;
    }
  }
  ```

  ```python Python (Flask) theme={null}
  from flask import Flask, request
  import threading

  app = Flask(__name__)

  @app.route("/webhooks/zavu", methods=["POST"])
  def handle_webhook():
      # 1. Verify signature (see Security guide)
      signature = request.headers.get("X-Zavu-Signature")
      if not verify_signature(signature, request.data, WEBHOOK_SECRET):
          return "Invalid signature", 401

      # 2. Acknowledge receipt immediately
      event = request.json

      # 3. Process the event asynchronously
      thread = threading.Thread(target=process_event, args=(event,))
      thread.start()

      return "OK", 200

  def process_event(event):
      if event["type"] == "message.inbound":
          handle_inbound_message(event["data"])
      elif event["type"] == "message.delivered":
          handle_delivery_confirmation(event["data"])
      elif event["type"] == "message.failed":
          handle_delivery_failure(event["data"])
  ```

  ```ruby Ruby (Sinatra) theme={null}
  require "sinatra"
  require "json"

  post "/webhooks/zavu" do
    # 1. Verify signature (see Security guide)
    signature = request.env["HTTP_X_ZAVU_SIGNATURE"]
    raw_body = request.body.read
    unless verify_signature(signature, raw_body, ENV["WEBHOOK_SECRET"])
      halt 401, "Invalid signature"
    end

    # 2. Acknowledge receipt immediately
    event = JSON.parse(raw_body)

    # 3. Process the event asynchronously
    Thread.new { process_event(event) }

    status 200
    "OK"
  end

  def process_event(event)
    case event["type"]
    when "message.inbound"
      handle_inbound_message(event["data"])
    when "message.delivered"
      handle_delivery_confirmation(event["data"])
    when "message.failed"
      handle_delivery_failure(event["data"])
    end
  end
  ```

  ```go Go theme={null}
  package main

  import (
  	"encoding/json"
  	"io"
  	"net/http"
  )

  func webhookHandler(w http.ResponseWriter, r *http.Request) {
  	// 1. Verify signature (see Security guide)
  	signature := r.Header.Get("X-Zavu-Signature")
  	body, _ := io.ReadAll(r.Body)
  	if !verifySignature(signature, body, webhookSecret) {
  		http.Error(w, "Invalid signature", http.StatusUnauthorized)
  		return
  	}

  	// 2. Acknowledge receipt immediately
  	w.WriteHeader(http.StatusOK)
  	w.Write([]byte("OK"))

  	// 3. Process the event asynchronously
  	var event map[string]interface{}
  	json.Unmarshal(body, &event)
  	go processEvent(event)
  }

  func processEvent(event map[string]interface{}) {
  	switch event["type"] {
  	case "message.inbound":
  		handleInboundMessage(event["data"])
  	case "message.delivered":
  		handleDeliveryConfirmation(event["data"])
  	case "message.failed":
  		handleDeliveryFailure(event["data"])
  	}
  }
  ```

  ```php PHP theme={null}
  <?php
  // 1. Verify signature (see Security guide)
  $signature = $_SERVER['HTTP_X_ZAVU_SIGNATURE'] ?? '';
  $rawBody = file_get_contents('php://input');
  if (!verifySignature($signature, $rawBody, getenv('WEBHOOK_SECRET'))) {
      http_response_code(401);
      echo "Invalid signature";
      exit;
  }

  // 2. Acknowledge receipt
  $event = json_decode($rawBody, true);

  // 3. Process the event
  switch ($event['type']) {
      case 'message.inbound':
          handleInboundMessage($event['data']);
          break;
      case 'message.delivered':
          handleDeliveryConfirmation($event['data']);
          break;
      case 'message.failed':
          handleDeliveryFailure($event['data']);
          break;
  }

  http_response_code(200);
  echo "OK";
  ```
</CodeGroup>

## Retry Policy

If your endpoint returns an error or doesn't respond within 30 seconds, Zavu will retry the delivery:

| Attempt   | Delay      |
| --------- | ---------- |
| 1st retry | 1 minute   |
| 2nd retry | 5 minutes  |
| 3rd retry | 15 minutes |
| 4th retry | 1 hour     |
| 5th retry | 4 hours    |

After 5 failed attempts, the webhook delivery is marked as failed. You can view failed deliveries in the Dashboard.

<Tip>
  Use a service like [webhook.site](https://webhook.site) or [ngrok](https://ngrok.com) for testing webhooks during development.
</Tip>

## Managing Webhooks

### Update Webhook Configuration

Update your sender's webhook settings:

```bash cURL theme={null}
curl -X PATCH https://api.zavu.dev/v1/senders/snd_abc123 \
  -H "Authorization: Bearer zv_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "webhookUrl": "https://new-url.com/webhooks",
    "webhookEvents": ["message.inbound"],
    "webhookActive": true
  }'
```

### Disable Webhook

```bash cURL theme={null}
curl -X PATCH https://api.zavu.dev/v1/senders/snd_abc123 \
  -H "Authorization: Bearer zv_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "webhookActive": false
  }'
```

### Remove Webhook

Set `webhookUrl` to `null` to remove the webhook:

```bash cURL theme={null}
curl -X PATCH https://api.zavu.dev/v1/senders/snd_abc123 \
  -H "Authorization: Bearer zv_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "webhookUrl": null
  }'
```

### Regenerate Secret

If your webhook secret is compromised:

```bash cURL theme={null}
curl -X POST https://api.zavu.dev/v1/senders/snd_abc123/webhook/secret \
  -H "Authorization: Bearer zv_live_xxx"
```

Response:

```json theme={null}
{
  "secret": "whsec_new_secret_here..."
}
```

## Next Steps

* [Event Types](/guides/receiving-messages/events) - Detailed payload for each event
* [Security](/guides/receiving-messages/security) - Verify webhook signatures
