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

# Webhooks API — Receive Real-Time Sahut Events

> Set up webhooks to receive real-time HTTP notifications when conversations, messages, and contacts change in your Sahut workspace.

Webhooks let Sahut send real-time HTTP POST notifications to your server when events occur — such as a new message arriving or a conversation being resolved. Use webhooks to trigger actions in your systems without polling the API.

## How webhooks work

1. You create a webhook endpoint on your server (a URL that accepts POST requests)
2. You register that URL in Sahut
3. When an event occurs, Sahut sends a POST request with a JSON payload to your URL
4. Your server responds with `200 OK` within 10 seconds

If your server doesn't respond with `2xx` within 10 seconds, Sahut retries delivery up to 5 times with exponential backoff (5s, 30s, 2m, 10m, 30m).

## Registering a webhook

### Via the dashboard

1. Go to **Settings → Integrations → Webhooks**
2. Click **Tambah Webhook** (Add Webhook)
3. Enter your **Endpoint URL**
4. Select the **Events** you want to receive
5. Click **Simpan** (Save)

### Via the API

```
POST /webhooks
```

#### Request body

<ParamField body="url" type="string" required>
  The HTTPS URL to receive webhook events.
</ParamField>

<ParamField body="events" type="array" required>
  List of event types to subscribe to. See event types below.
</ParamField>

#### Example request

```bash theme={null}
curl -X POST https://api.sahut.id/v1/webhooks \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://yourserver.com/sahut-webhook",
    "events": ["message.created", "conversation.resolved"]
  }'
```

#### Example response

```json theme={null}
{
  "data": {
    "id": "wh_01HX6G4P8R0T2U",
    "url": "https://yourserver.com/sahut-webhook",
    "events": ["message.created", "conversation.resolved"],
    "secret": "whsec_a1b2c3d4e5f6...",
    "created_at": "2024-03-15T10:00:00Z"
  }
}
```

<Warning>Copy the `secret` value from the creation response. Sahut only shows it once. You use this secret to verify webhook signatures.</Warning>

## Delete a webhook

```
DELETE /webhooks/{id}
```

<ParamField path="id" type="string" required>
  Webhook ID to delete.
</ParamField>

## Event types

| Event                   | Triggered when                                        |
| ----------------------- | ----------------------------------------------------- |
| `conversation.created`  | A new conversation is created                         |
| `conversation.updated`  | A conversation's status, assignee, or labels change   |
| `conversation.resolved` | A conversation is marked as resolved                  |
| `message.created`       | A new message is sent or received in any conversation |
| `contact.created`       | A new contact profile is created                      |
| `contact.updated`       | A contact's details are updated                       |

## Webhook payload format

All webhook events share the same envelope structure:

```json theme={null}
{
  "event": "message.created",
  "timestamp": "2024-03-15T08:30:00Z",
  "workspace_id": "ws_01HX0Y6H0J2K4L",
  "data": { }
}
```

### Example: message.created payload

```json theme={null}
{
  "event": "message.created",
  "timestamp": "2024-03-15T08:30:00Z",
  "workspace_id": "ws_01HX0Y6H0J2K4L",
  "data": {
    "message": {
      "id": "msg_01HX5E2N6P8R0S",
      "type": "incoming",
      "content": "Halo, saya ingin bertanya tentang produk Anda",
      "created_at": "2024-03-15T08:30:00Z"
    },
    "conversation": {
      "id": "conv_01HX2B9K3M5N7P",
      "status": "open"
    },
    "contact": {
      "id": "ct_01HX3C0L4N6P8Q",
      "name": "Budi Santoso",
      "phone": "+628123456789"
    },
    "channel": {
      "id": "ch_01HX1A8J2L4M6N",
      "type": "whatsapp"
    }
  }
}
```

## Verifying webhook signatures

Sahut signs every webhook request so you can verify it came from Sahut and wasn't tampered with.

Each request includes a `X-Sahut-Signature` header containing an HMAC-SHA256 signature of the raw request body, signed with your webhook's secret.

To verify the signature:

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

  function verifyWebhook(payload, signature, secret) {
    const expected = crypto
      .createHmac('sha256', secret)
      .update(payload, 'utf8')
      .digest('hex');
    return crypto.timingSafeEqual(
      Buffer.from(signature),
      Buffer.from(`sha256=${expected}`)
    );
  }

  // In your request handler:
  app.post('/sahut-webhook', (req, res) => {
    const rawBody = req.rawBody; // unparsed request body string
    const signature = req.headers['x-sahut-signature'];
    const secret = process.env.SAHUT_WEBHOOK_SECRET;

    if (!verifyWebhook(rawBody, signature, secret)) {
      return res.status(401).send('Invalid signature');
    }

    const event = JSON.parse(rawBody);
    // handle event...
    res.sendStatus(200);
  });
  ```

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

  def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
      expected = 'sha256=' + hmac.new(
          secret.encode('utf-8'),
          payload,
          hashlib.sha256
      ).hexdigest()
      return hmac.compare_digest(signature, expected)

  # In your request handler (Flask example):
  from flask import request, abort

  @app.route('/sahut-webhook', methods=['POST'])
  def webhook():
      payload = request.get_data()
      signature = request.headers.get('X-Sahut-Signature', '')
      secret = os.environ['SAHUT_WEBHOOK_SECRET']

      if not verify_webhook(payload, signature, secret):
          abort(401)

      event = request.get_json()
      # handle event...
      return '', 200
  ```
</CodeGroup>

<Note>Always verify webhook signatures in production to protect against spoofed requests.</Note>
