Errors & rate limits
Zend uses standard HTTP status codes and returns a consistent JSON error shape. This page covers the error response format, the most common errors, rate limits per endpoint, and best practices for handling both.
Error response shape
Every error response is JSON with three fields: an HTTP statusCode, a human-readable message, and an error label.
{
"statusCode": 400,
"message": "Insufficient credits. Required: 0.020, Available: 0.015. Please purchase more credits.",
"error": "Bad Request"
}
Response fields
400, 429, 500.Bad Request, Too Many Requests.Common errors
Insufficient credits — 400
{
"statusCode": 400,
"message": "Insufficient credits. Required: 0.020, Available: 0.015. Please purchase more credits.",
"error": "Bad Request"
}
Invalid template — 400
{
"statusCode": 400,
"message": "Template rendering failed: Required variable 'customer_name' is missing",
"error": "Bad Request"
}
Rate limit exceeded — 429
{
"statusCode": 429,
"message": "Rate limit exceeded. Please wait 60 seconds before sending more messages.",
"error": "Too Many Requests"
}
Invalid phone number — 400
{
"statusCode": 400,
"message": "Invalid phone number format. Please use international format (e.g., +233593152134)",
"error": "Bad Request"
}
Handling errors
Inspect the HTTP status and message to branch on the failure type. The example below throws targeted errors and retries transient failures with exponential backoff.
class MessageAPI {
constructor(apiKey) {
this.apiKey = apiKey;
this.baseUrl = 'https://api.tryzend.com';
}
async sendMessage(messageData) {
try {
const response = await fetch(`${this.baseUrl}/messages`, {
method: 'POST',
headers: {
'x-api-key': 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}`);
}
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: 'PUT',
headers: {
'x-api-key': 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
Rate limits are enforced per endpoint. Exceeding a limit returns a 429 response.
| 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 |
Best practices
Handle errors explicitly
Branch on the error message so each failure type has a clear recovery path.
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);
}
}
Throttle to stay under limits
Track requests within each window and pause before you hit a limit rather than reacting to 429s.
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
Note
cost.