Features Guide

Learn about advanced features of the Zend including webhooks, scheduling, analytics, and more.

Webhooks

Receive real-time delivery status updates via webhooks.

Setting Up 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"
}

Webhook Payload Format

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
}

Webhook Security

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');
});

Scheduled Messages

Send messages at a specific time in the future.

Basic Scheduling

{
  "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"
}

Bulk Scheduled Messages

{
  "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"
}

Cancel Scheduled Message

PUT /messages/{id}/cancel

Response:

{
  "success": true,
  "message": "Message cancelled successfully"
}

Priority Levels

Set message priority for faster processing.

Priority Options

PriorityDescriptionProcessing Time
lowNon-urgent messages5-10 minutes
normalStandard messages1-2 minutes
highImportant messages30 seconds
urgentCritical messagesImmediate

Using Priority

{
  "to": "+233593152134",
  "body": "URGENT: System outage detected",
  "preferred_channels": ["sms", "whatsapp"],
  "priority": "urgent",
  "delivery_priority": "speed"
}

Message Status & Tracking

Check Message Status

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 Message History

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
}

Retry Failed Messages

PUT /messages/{id}/retry

Response:

{
  "id": "6884da240f0e633b7b979bff",
  "status": "pending",
  "estimated_cost": 0.02,
  "message": "Message queued for retry"
}

Error Handling

Common Error Responses

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"
}

Error Handling Best Practices

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));
      }
    }
  }
}

Rate Limits & Best Practices

Rate Limits

EndpointLimitWindow
SMS Messages100/minutePer minute
WhatsApp Messages50/minutePer minute
Bulk Messages1000/hourPer hour
Template Creation10/minutePer minute

Best Practices

1. Error Handling

// 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);
  }
}

2. Rate Limiting

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

3. Monitoring

// 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;
};

4. Cost Optimization

// 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'));
};