Learn about advanced features of the Zend including webhooks, scheduling, analytics, and more.
Receive real-time delivery status updates via webhooks.
Include a webhook URL in your message request:
{
"to": "+233593152134",
"body": "Your order has been shipped!",
"preferred_channels": ["whatsapp"],
"template_id": "12345678912345",
"template_params": {
"order_number": "ORD-2024-001"
},
"webhook_url": "https://yourapp.com/webhooks/message-status"
}
Message Sent:
{
"event": "message.sent",
"message_id": "6884da240f0e633b7b979bff",
"status": "sent",
"channel": "whatsapp",
"recipient": "+233593152134",
"timestamp": "2024-01-15T10:30:00Z",
"cost": 0.008
}
Message Delivered:
{
"event": "message.delivered",
"message_id": "6884da240f0e633b7b979bff",
"status": "delivered",
"channel": "whatsapp",
"recipient": "+233593152134",
"timestamp": "2024-01-15T10:30:05Z",
"cost": 0.008
}
Message Failed:
{
"event": "message.failed",
"message_id": "6884da240f0e633b7b979bff",
"status": "failed",
"channel": "whatsapp",
"recipient": "+233593152134",
"timestamp": "2024-01-15T10:30:00Z",
"error": "Recipient not available on WhatsApp",
"cost": 0.008
}
Verify webhook authenticity:
const crypto = require('crypto');
function verifyWebhook(payload, signature, secret) {
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Usage
app.post('/webhooks/message-status', (req, res) => {
const signature = req.headers['x-zend-signature'];
const payload = req.body;
if (!verifyWebhook(payload, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
// Process webhook
console.log('Webhook received:', payload);
res.status(200).send('OK');
});
Send messages at a specific time in the future.
{
"to": "+233593152134",
"body": "Reminder: Your appointment is in 1 hour",
"preferred_channels": ["whatsapp"],
"template_id": "12345678912345",
"template_params": {
"appointment_time": "2:00 PM",
"location": "Main Office"
},
"scheduled_for": "2024-01-15T13:00:00Z"
}
{
"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"
}
PUT /messages/{id}/cancel
Response:
{
"success": true,
"message": "Message cancelled successfully"
}
Set message priority for faster processing.
Priority | Description | Processing Time |
---|---|---|
low | Non-urgent messages | 5-10 minutes |
normal | Standard messages | 1-2 minutes |
high | Important messages | 30 seconds |
urgent | Critical messages | Immediate |
{
"to": "+233593152134",
"body": "URGENT: System outage detected",
"preferred_channels": ["sms", "whatsapp"],
"priority": "urgent",
"delivery_priority": "speed"
}
GET /messages/{id}
Response:
{
"id": "6884da240f0e633b7b979bff",
"status": "delivered",
"channel_used": "whatsapp",
"to": "+233593152134",
"body": "Your verification code is: 123456",
"total_cost": 0.008,
"delivery_attempts": [
{
"channel": "whatsapp",
"status": "delivered",
"attempted_at": "2024-01-15T10:30:00Z",
"cost": 0.008
}
],
"created_at": "2024-01-15T10:29:55Z",
"sent_at": "2024-01-15T10:30:00Z",
"delivered_at": "2024-01-15T10:30:05Z"
}
GET /messages?limit=50&offset=0&status=delivered&channel=whatsapp
Response:
{
"messages": [
{
"id": "6884da240f0e633b7b979bff",
"status": "delivered",
"channel_used": "whatsapp",
"to": "+233593152134",
"total_cost": 0.008,
"created_at": "2024-01-15T10:29:55Z"
}
],
"total": 150,
"page": 1,
"pages": 3
}
PUT /messages/{id}/retry
Response:
{
"id": "6884da240f0e633b7b979bff",
"status": "pending",
"estimated_cost": 0.02,
"message": "Message queued for retry"
}
Insufficient Credits:
{
"statusCode": 400,
"message": "Insufficient credits. Required: 0.020, Available: 0.015. Please purchase more credits.",
"error": "Bad Request"
}
Invalid Template:
{
"statusCode": 400,
"message": "Template rendering failed: Required variable 'customer_name' is missing",
"error": "Bad Request"
}
Rate Limit Exceeded:
{
"statusCode": 429,
"message": "Rate limit exceeded. Please wait 60 seconds before sending more messages.",
"error": "Too Many Requests"
}
Invalid Phone Number:
{
"statusCode": 400,
"message": "Invalid phone number format. Please use international format (e.g., +233593152134)",
"error": "Bad Request"
}
class MessageAPI {
constructor(apiKey) {
this.apiKey = apiKey;
this.baseUrl = 'https://api.zend.com';
}
async sendMessage(messageData) {
try {
const response = await fetch(`${this.baseUrl}/messages`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(messageData)
});
if (!response.ok) {
const error = await response.json();
// Handle specific error types
switch (response.status) {
case 400:
if (error.message.includes('credits')) {
throw new Error('Insufficient credits - please top up your account');
} else if (error.message.includes('template')) {
throw new Error('Template error - check your template parameters');
} else {
throw new Error(`Bad request: ${error.message}`);
}
break;
case 429:
throw new Error('Rate limit exceeded - please wait before sending more messages');
case 500:
throw new Error('Server error - please try again later');
default:
throw new Error(`API error: ${error.message}`);
}
}
return await response.json();
} catch (error) {
console.error('Message send failed:', error.message);
throw error;
}
}
async retryWithBackoff(messageId, maxRetries = 3) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(`${this.baseUrl}/messages/${messageId}/retry`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`
}
});
if (response.ok) {
return await response.json();
}
} catch (error) {
console.error(`Retry attempt ${attempt} failed:`, error.message);
if (attempt === maxRetries) {
throw new Error('Max retry attempts reached');
}
// Exponential backoff
await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));
}
}
}
}
Endpoint | Limit | Window |
---|---|---|
SMS Messages | 100/minute | Per minute |
WhatsApp Messages | 50/minute | Per minute |
Bulk Messages | 1000/hour | Per hour |
Template Creation | 10/minute | Per minute |
// Always implement proper error handling
try {
const result = await messageAPI.sendMessage(messageData);
console.log('Message sent:', result.id);
} catch (error) {
if (error.message.includes('credits')) {
// Handle insufficient credits
await topUpCredits();
} else if (error.message.includes('rate limit')) {
// Implement retry with backoff
await retryWithBackoff(messageData);
} else {
// Log and handle other errors
console.error('Unexpected error:', error);
}
}
class RateLimiter {
constructor(limit, window) {
this.limit = limit;
this.window = window;
this.requests = [];
}
async checkLimit() {
const now = Date.now();
this.requests = this.requests.filter(time => now - time < this.window);
if (this.requests.length >= this.limit) {
const oldestRequest = this.requests[0];
const waitTime = this.window - (now - oldestRequest);
await new Promise(resolve => setTimeout(resolve, waitTime));
}
this.requests.push(now);
}
}
const smsLimiter = new RateLimiter(100, 60000); // 100 per minute
const whatsappLimiter = new RateLimiter(50, 60000); // 50 per minute
// Monitor message delivery rates
const monitorDelivery = async () => {
const analytics = await fetch('/analytics/overview?from_date=2024-01-14T00:00:00.000Z&to_date=2024-01-15T00:00:00.000Z');
const data = await analytics.json();
const deliveryRate = (data.summary.delivered / data.summary.sent) * 100;
if (deliveryRate < 90) {
console.warn(`Low delivery rate: ${deliveryRate.toFixed(1)}%`);
// Send alert to team
}
return deliveryRate;
};
// Choose most cost-effective channel
const optimizeForCost = (messageData) => {
const channels = messageData.preferred_channels || ['whatsapp', 'sms'];
// WhatsApp is cheaper for template messages
if (messageData.template_id) {
return channels.filter(ch => ch === 'whatsapp').concat(channels.filter(ch => ch !== 'whatsapp'));
}
// SMS is cheaper for simple messages
return channels.filter(ch => ch === 'sms').concat(channels.filter(ch => ch !== 'sms'));
};