Advanced bulk

Beyond simple sends, bulk messaging supports per-recipient customization, mixed channels, scheduling, webhooks, and detailed status tracking and analytics. This page covers those advanced features and common errors. For the basics, start with the Bulk messaging overview.

POST/messages/bulk

Individual customization

Personalize each recipient's message with its own body and template_params while sharing a single template_id.

{
  "messages": [
    {
      "to": "+233593152134",
      "body": "Hi John, your order #12345 has been shipped!",
      "template_params": {
        "customer_name": "John",
        "order_number": "12345",
        "tracking_url": "https://track.com/12345"
      }
    },
    {
      "to": "+233593152135",
      "body": "Hi Sarah, your order #12346 has been shipped!",
      "template_params": {
        "customer_name": "Sarah",
        "order_number": "12346",
        "tracking_url": "https://track.com/12346"
      }
    }
  ],
  "template_id": "12345678912345",
  "preferred_channels": ["whatsapp", "sms"]
}

Mixed channels

Set preferred_channels per message to route recipients to different channels based on their preference. Combine with a top-level fallback_enabled for resilience.

{
  "messages": [
    {
      "to": "+233593152134",
      "body": "Your appointment reminder",
      "template_params": {
        "appointment_time": "2:00 PM",
        "location": "Main Office"
      },
      "preferred_channels": ["whatsapp"]
    },
    {
      "to": "+233593152135",
      "body": "Your appointment reminder",
      "template_params": {
        "appointment_time": "3:00 PM",
        "location": "Branch Office"
      },
      "preferred_channels": ["sms"]
    }
  ],
  "template_id": "12345678912345",
  "fallback_enabled": true
}

Scheduling

Schedule a bulk send for future delivery with scheduled_for (ISO 8601 timestamp).

{
  "messages": [
    {"to": "+233593152134", "body": "Happy Birthday! 🎉"},
    {"to": "+233593152135", "body": "Happy Birthday! 🎉"},
    {"to": "+233593152136", "body": "Happy Birthday! 🎉"}
  ],
  "preferred_channels": ["whatsapp", "sms"],
  "scheduled_for": "2024-01-15T09:00:00Z"
}

Webhooks

Receive delivery status updates for all messages in the batch by setting webhook_url. Zend posts status changes to that endpoint as they occur.

{
  "messages": [
    {"to": "+233593152134", "body": "System maintenance notification"},
    {"to": "+233593152135", "body": "System maintenance notification"},
    {"to": "+233593152136", "body": "System maintenance notification"}
  ],
  "preferred_channels": ["sms"],
  "webhook_url": "https://yourapp.com/webhooks/bulk-status"
}

Advanced request fields

scheduled_forstring
ISO 8601 timestamp for future delivery. Omit to send immediately.
webhook_urlstring
URL that receives delivery status updates for the batch.
preferred_channelsstring[]
Can be set per message to route recipients to different channels.
fallback_enabledboolean
Fall back to the next channel if the first fails.

Status tracking

Check bulk status

GET/messages?bulk_id={bulk_id}

Retrieve aggregate progress for a batch.

curl "https://api.tryzend.com/messages?bulk_id=bulk_1705312345678" \
  -H "x-api-key: YOUR_API_KEY"

Response

{
  "id": "bulk_1705312345678",
  "status": "processing",
  "total_recipients": 1000,
  "sent": 750,
  "delivered": 720,
  "failed": 30,
  "pending": 250,
  "total_cost": 15.20,
  "created_at": "2024-01-15T10:00:00Z",
  "estimated_completion": "2024-01-15T10:15:00Z"
}

Get individual message statuses

Page through the individual messages in a batch with limit and offset.

curl "https://api.tryzend.com/messages?bulk_id=bulk_1705312345678&limit=50&offset=0" \
  -H "x-api-key: YOUR_API_KEY"

Response

{
  "messages": [
    {
      "id": "6884da240f0e633b7b979bff",
      "status": "delivered",
      "channel_used": "whatsapp",
      "to": "+233593152134",
      "total_cost": 0.008,
      "created_at": "2024-01-15T10:00:05Z"
    }
  ],
  "total": 1000,
  "page": 1,
  "pages": 20
}

Batching & rate limiting

For large sends, split recipients into batches and space them out to stay within the 1,000 messages-per-hour rate limit.

// Optimal batch sizes
const optimalBatchSizes = {
  sms: 1000,      // SMS: 1000 per batch
  whatsapp: 500,  // WhatsApp: 500 per batch
  mixed: 750      // Mixed channels: 750 per batch
};

// Implement rate limiting for large batches
const sendBulkWithRateLimit = async (messages, batchSize = 500) => {
  const batches = [];
  for (let i = 0; i < messages.length; i += batchSize) {
    batches.push(messages.slice(i, i + batchSize));
  }

  for (const batch of batches) {
    await sendBulkMessage(batch);
    // Wait 1 minute between batches
    await new Promise(resolve => setTimeout(resolve, 60000));
  }
};

Progress monitoring

Poll the status endpoint until the batch completes.

const monitorBulkProgress = async (bulkId) => {
  const checkStatus = async () => {
    const response = await fetch(`https://api.tryzend.com/messages?bulk_id=${bulkId}`, {
      headers: { 'x-api-key': 'YOUR_API_KEY' }
    });
    const status = await response.json();

    console.log(`Progress: ${status.sent}/${status.total_recipients} sent`);

    if (status.status === 'completed') {
      console.log('Bulk send completed!');
      return;
    }

    // Check again in 30 seconds
    setTimeout(checkStatus, 30000);
  };

  checkStatus();
};

Analytics

Get bulk statistics

GET/analytics/overview

Retrieve delivery, channel, cost, and delivery-time breakdowns for a batch.

curl "https://api.tryzend.com/analytics/overview" \
  -H "x-api-key: YOUR_API_KEY"

Response

{
  "bulk_id": "bulk_1705312345678",
  "total_recipients": 1000,
  "delivery_stats": {
    "sent": 950,
    "delivered": 920,
    "failed": 30,
    "pending": 50
  },
  "channel_stats": {
    "whatsapp": {
      "sent": 600,
      "delivered": 580,
      "failed": 20
    },
    "sms": {
      "sent": 350,
      "delivered": 340,
      "failed": 10
    }
  },
  "cost_breakdown": {
    "whatsapp": 4.64,
    "sms": 6.80,
    "total": 11.44
  },
  "delivery_times": {
    "average": 45,
    "median": 32,
    "p95": 120
  }
}

Errors

Insufficient credits

{
  "statusCode": 400,
  "message": "Insufficient credits for bulk send. Required: 20.000, Available: 15.500. Please purchase more credits.",
  "error": "Bad Request"
}

Invalid recipients

{
  "statusCode": 400,
  "message": "Invalid phone numbers found: +233INVALID, +233INVALID2. Please fix and retry.",
  "error": "Bad Request"
}

Template errors

{
  "statusCode": 400,
  "message": "Template 'welcome_template' not found or not accessible",
  "error": "Bad Request"
}

Note

Validate recipient phone numbers and confirm you have enough credits before large sends. A single invalid number or a credit shortfall rejects the whole request.

Next steps