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

# Delivery

> Message buffering, ordering, retry policy, and automatic pausing

## Message buffering

Message buffering allows you to receive multiple `whatsapp.message.received` events in a single batched webhook, reducing load during high-volume conversations.

### How it works

1. **Debounce pattern** - Messages are collected until the configured time window expires
2. **Automatic batching** - Multiple messages from the same conversation are grouped
3. **Immediate delivery** - Batches are sent when max size is reached or window expires
4. **Per-conversation** - Each conversation has its own independent buffer

### Configuration

When creating or editing a webhook, enable message buffering for the `whatsapp.message.received` event:

* **Buffer window**: Time to wait before sending (1-60 seconds, default: 5)
* **Maximum batch size**: Max messages per batch (1-100, default: 50)

### Batched webhook format

```json theme={null}
{
  "type": "whatsapp.message.received",
  "batch": true,
  "data": [
    {
      "message": {
        "id": "wamid.111",
        "timestamp": "1730092801",
        "type": "text",
        "text": { "body": "First in batch" },
        "kapso": {
          "direction": "inbound",
          "status": "received",
          "processing_status": "pending",
          "origin": "cloud_api",
          "phone_number": "+15551234567",
          "phone_number_id": "123456789012345"
        }
      },
      "conversation": {
        "id": "conv_123",
        "phone_number": "+15551234567",
        "status": "active",
        "last_active_at": "2025-10-28T14:26:01Z",
        "created_at": "2025-10-28T13:40:00Z",
        "updated_at": "2025-10-28T14:26:01Z",
        "metadata": {},
        "phone_number_id": "123456789012345"
      },
      "is_new_conversation": false,
      "phone_number_id": "123456789012345"
    },
    {
      "message": {
        "id": "wamid.112",
        "timestamp": "1730092802",
        "type": "text",
        "text": { "body": "Second in batch" },
        "kapso": {
          "direction": "inbound",
          "status": "received",
          "processing_status": "pending",
          "origin": "cloud_api",
          "phone_number": "+15551234567",
          "phone_number_id": "123456789012345"
        }
      },
      "conversation": {
        "id": "conv_123",
        "phone_number": "+15551234567",
        "status": "active",
        "last_active_at": "2025-10-28T14:26:02Z",
        "created_at": "2025-10-28T13:40:00Z",
        "updated_at": "2025-10-28T14:26:02Z",
        "metadata": {},
        "phone_number_id": "123456789012345"
      },
      "is_new_conversation": false,
      "phone_number_id": "123456789012345"
    }
  ],
  "batch_info": {
    "size": 2,
    "window_ms": 5000,
    "first_sequence": 101,
    "last_sequence": 102,
    "conversation_id": "conv_123"
  }
}
```

<Note>
  With buffering on, every delivery uses batch format, even a single message.
</Note>

The `data` array contains one message if only one arrived during the buffer window. Always check the `batch` field or the `X-Webhook-Batch` header rather than assuming the shape.

### Handling batched webhooks

```javascript theme={null}
app.post('/webhooks', (req, res) => {
  const isBatch = req.headers['x-webhook-batch'] === 'true';

  if (isBatch) {
    const { data, batch_info } = req.body;

    console.log(`Processing ${batch_info.size} messages`);

    data.forEach(event => {
      processMessage(event.message, event.conversation);
    });
  } else {
    // Single event
    const { message, conversation } = req.body;
    processMessage(message, conversation);
  }

  res.status(200).send('OK');
});
```

## Message ordering

Kapso ensures messages are delivered in the correct order within each conversation.

### How it works

* **Sequence-based ordering** - Each webhook delivery gets a sequence number
* **Automatic queuing** - Messages are queued if earlier messages haven't been delivered
* **Ordering timeout** - After 30 seconds, messages are delivered regardless to prevent delays
* **Per conversation** - Ordering is maintained independently per conversation
* **Applies to** - Message received and message sent events

This ensures your endpoint receives messages in the same order they were sent/received.

## Message origin

The `message.kapso.origin` field tells you how the message entered the system:

* **cloud\_api** - Sent via Kapso API (outbound jobs, flow actions, API calls)
* **business\_app** - Sent from WhatsApp Business App (manual messages your team sends using the Business App)
* **history\_sync** - Backfilled during message history import (only present if your project ran a sync)

Use this to filter out messages you don't want to process. For example, skip `business_app` messages to avoid processing manual messages sent by your team.

## Retry policy

If Kapso doesn't receive a 200 response, webhooks are automatically retried.

### Retry schedule

Each webhook is attempted based on this schedule:

* **Immediately** (initial attempt)
* **10 seconds** after first failure
* **40 seconds** after second failure
* **90 seconds** after third failure

**Total time to failure**: \~2.5 minutes across 3 retry attempts.

### What happens after retries fail?

After all retries are exhausted:

* The webhook is marked as failed
* Batched messages fall back to individual delivery
* You can check failed deliveries in the Kapso dashboard
* If the failure rate is high enough, the webhook is automatically paused (see below)

## Automatic pausing

Kapso automatically pauses a webhook when it detects a persistently failing endpoint. This protects your system from retry storms and prevents queue buildup.

### Pause thresholds

A webhook is paused when **all** of the following are met within a 15-minute window:

| Condition                 | Value |
| ------------------------- | ----- |
| Minimum total deliveries  | 20    |
| Minimum failed deliveries | 10    |
| Minimum failure rate      | 85%   |

### What happens when a webhook is paused

* The webhook's `active` field is set to `false`
* Pending deliveries are marked as `failed` with reason `"Webhook inactive; delivery skipped"`
* All project members receive an email with failure details and a link to settings
* No new deliveries are attempted until you re-enable the webhook

### Re-enabling a paused webhook

1. Fix the issue with your endpoint
2. Open **Integrations → Webhooks** in the Kapso dashboard
3. Toggle the webhook back to active

<Warning>
  Webhooks that are re-enabled will resume delivery. Make sure your endpoint is healthy before re-enabling, or it may be paused again.
</Warning>

### Handling retries in your code

Implement idempotency to handle retry attempts gracefully:

```javascript theme={null}
app.post('/webhooks', async (req, res) => {
  const idempotencyKey = req.headers['x-idempotency-key'];

  // Check if already processed
  const existing = await db.webhookEvents.findOne({
    idempotency_key: idempotencyKey
  });

  if (existing) {
    // Already processed this webhook
    return res.status(200).send('Already processed');
  }

  try {
    // Process webhook
    await processEvent(req.body);

    // Store idempotency key
    await db.webhookEvents.create({
      idempotency_key: idempotencyKey,
      processed_at: new Date()
    });

    res.status(200).send('OK');
  } catch (error) {
    // Return 500 to trigger retry
    console.error('Webhook processing failed:', error);
    res.status(500).send('Processing failed');
  }
});
```

## Best practices

### Performance

1. **Respond quickly** - Return 200 within 10 seconds
2. **Process asynchronously** - Use background jobs for heavy processing
3. **Scale horizontally** - Use load balancers to handle high volume
4. **Enable buffering** - Reduce webhook volume during busy periods

### Reliability

1. **Implement idempotency** - Use `X-Idempotency-Key` to prevent duplicate processing
2. **Handle all event types** - Even if you don't need them now
3. **Log everything** - Track webhook deliveries and failures
4. **Set up monitoring** - Alert on high failure rates

### Example production setup

```javascript theme={null}
const queue = require('bull'); // Background job processor
const webhookQueue = new queue('webhooks');

app.post('/webhooks', async (req, res) => {
  const idempotencyKey = req.headers['x-idempotency-key'];
  const event = req.headers['x-webhook-event'];

  // Check if already processed
  if (await isProcessed(idempotencyKey)) {
    return res.status(200).send('Already processed');
  }

  // Add to background queue
  await webhookQueue.add({
    idempotency_key: idempotencyKey,
    event,
    data: req.body.batch === true ? req.body.data : req.body,
    headers: {
      signature: req.headers['x-webhook-signature'],
      batch: req.headers['x-webhook-batch']
    }
  });

  // Respond immediately
  res.status(200).send('OK');
});

// Process webhooks in background
webhookQueue.process(async (job) => {
  const { idempotency_key, event, data, headers } = job.data;

  // Verify signature
  if (!verifySignature(data, headers.signature)) {
    throw new Error('Invalid signature');
  }

  // Process event
  await processEvent(event, data);

  // Mark as processed
  await markProcessed(idempotency_key);
});
```

## Troubleshooting

<AccordionGroup>
  <Accordion title="Webhooks not being received">
    * Verify your endpoint is publicly accessible via HTTPS
    * Check firewall rules allow incoming requests from Kapso
    * Ensure you're returning 200 status within 10 seconds
    * Check webhook is enabled in dashboard
  </Accordion>

  <Accordion title="Duplicate webhooks">
    * Implement idempotency using `X-Idempotency-Key` header
    * Store processed keys in database or cache
    * Use timing-safe comparison when checking keys
  </Accordion>

  <Accordion title="Webhooks out of order">
    * Check sequence numbers in `batch_info`
    * Implement ordering logic in your application if needed
    * Note: 30-second timeout allows out-of-order delivery to prevent indefinite delays
  </Accordion>

  <Accordion title="High failure rate">
    * Verify signature verification logic is correct
    * Check endpoint response time (must be under 10 seconds)
    * Review error logs for exceptions in your code
    * Ensure database/external services aren't timing out
    * If Kapso auto-paused your webhook, fix the endpoint and re-enable it from **Integrations → Webhooks**
  </Accordion>
</AccordionGroup>
