BetaThe V4 API is in beta — endpoints and functionality may change.

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.