Docs
Errors & rate limits
Every error uses the same JSON envelope, and every response tells you how much rate-limit budget remains.
Error envelope
Non-2xx responses carry an HTTP status and a body with a stable machine-readable code, a human message, optional details and a request_id to quote to support.
{
"error": {
"code": "validation_failed",
"message": "Request validation failed",
"details": {
"to": "must be a valid phone number in E.164 format",
"text.body": "must be at most 4096 characters"
},
"request_id": "8c3f2a91b7d84e0f9a1c2b3d4e5f6a7b"
}
}Error codes
Rate limits
Each API key has a request budget per second based on your plan (Starter 20, Growth 100, Business 300 rps). Every response includes the standard headers:
Message sending is additionally paced per WhatsApp number according to Meta's throughput tier. That pacing happens inside Engage's queue: you receive 202 immediately and messages are delivered as fast as WhatsApp allows.
Retrying safely
Retry 429, 500, 502 and network errors with exponential backoff and jitter. Always send an Idempotency-Key so a retried POST never creates a duplicate.
async function engage(path, body, key = crypto.randomUUID()) {
for (let attempt = 0; attempt < 5; attempt++) {
const res = await fetch(`https://api.engage.swastik.ai/v1${path}`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.ENGAGE_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": key, // same key on every retry
},
body: JSON.stringify(body),
});
if (res.ok) return res.json();
if (![429, 500, 502, 503].includes(res.status)) {
const { error } = await res.json();
throw new Error(`${error.code}: ${error.message}`);
}
const wait = Number(res.headers.get("Retry-After")) * 1000 || 2 ** attempt * 500;
await new Promise((r) => setTimeout(r, wait + Math.random() * 250));
}
throw new Error("Engage request failed after retries");
}