Skip to main content

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

{
  "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",
          "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",
          "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"
  }
}
When buffering is enabled, ALL messages are delivered in batch format, even single messages. The data array will contain just one message if only one was received during the buffer window. Always check the batch field or X-Webhook-Batch header.

Handling batched webhooks

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.

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

Handling retries in your code

Implement idempotency to handle retry attempts gracefully:
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

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'];

  // 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: req.body.event,
    data: 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

  • 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
  • Implement idempotency using X-Idempotency-Key header
  • Store processed keys in database or cache
  • Use timing-safe comparison when checking keys
  • 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
  • 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