Error handling & retries
Handle common errors, rate limits, and implement safe retries
Error schema
All errors follow a standard shape: { error, message, status }. See Errors.
Common cases
- 401 Unauthorized: missing/invalid token
- 402 Payment Required: trial limit reached
- 403 Forbidden: action not allowed
- 409 Conflict: duplicate idempotency key or non-cancellable message
- 429 Too Many Requests: conversation or configured outbound limit reached
- 503 Service Unavailable: no active number available
Messaging-safety 403 and 429 responses are not transient failures. Branch on
the response body's code and follow
Messaging safety limits and recovery instead of
retrying until the request succeeds.
Retry strategy
Use an Idempotency-Key with exponential backoff for transient errors (5xx).
Do not apply this retry loop to messaging-safety 403 or 429 responses.
async function sendWithRetry(chatId, body, { maxAttempts = 5 } = {}) {
const encodedChatId = encodeURIComponent(chatId);
let attempt = 0;
while (attempt < maxAttempts) {
attempt++;
const idempotencyKey = `retry-${Date.now()}-${attempt}`;
const res = await fetch(
`https://api.blooio.com/v2/api/chats/${encodedChatId}/messages`,
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.BLOOIO_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify(body),
},
);
if (res.ok) return res.json();
if (res.status < 500) throw new Error(`Non-retryable: ${res.status}`);
await new Promise((r) =>
setTimeout(r, Math.min(1000 * 2 ** attempt, 10000)),
);
}
throw new Error("Max retry attempts reached");
}Persist keys client-side to dedupe across restarts.