Skip to main content
This guide shows you how to handle errors from the Chirp API in your application. For a full reference of the error format, see Errors. For details on specific error codes, see Error Codes.

API Error Handling

Basic Error Handling

Every error response from the Chirp API has the same structure. Start by checking the error.type field to determine the category of the problem.
const response = await fetch("https://api.buildwithchirp.com/v1/sms", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${apiKey}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    from: "+15551234567",
    to: ["+15559876543"],
    text: "Hello from Chirp!",
  }),
});

if (!response.ok) {
  const { error } = await response.json();

  switch (error.type) {
    case "invalid_request_error":
      // Fix the request and try again
      console.error(`Bad request: ${error.message}`);
      break;
    case "authentication_error":
      // Check your API key
      console.error(`Auth failed: ${error.message}`);
      break;
    case "provider_error":
      // The request was valid but the provider rejected it
      console.error(`Provider error: ${error.message}`);
      break;
    case "api_error":
      // Something went wrong on Chirp's end - retry later
      console.error(`Server error: ${error.message}`);
      break;
  }
}

Handling Specific Error Codes

When an error includes a code field, you can use it to branch your logic and take specific actions.
Handling specific error codes
async function sendWhatsAppMessage(to, text) {
  const response = await fetch("https://api.buildwithchirp.com/v1/whatsapp/messages", {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${apiKey}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ to, type: "text", text: { body: text } }),
  });

  if (!response.ok) {
    const { error } = await response.json();

    switch (error.code) {
      case "conversation_window_expired":
        // Fall back to sending a template message
        return sendTemplateMessage(to);

      case "recipient_not_on_whatsapp":
        // Fall back to SMS
        return sendSms(to, text);

      case "message_undeliverable":
        // Mark recipient as unreachable
        await markUndeliverable(to, error.provider?.code);
        break;

      case "rate_limit_exceeded":
      case "whatsapp_rate_limited":
        // Retry with backoff
        return retryWithBackoff(() => sendWhatsAppMessage(to, text));

      default:
        throw new Error(`Unexpected error: ${error.message}`);
    }
  }

  return response.json();
}

Handling Provider Errors

When an error has a provider object, you can access the raw error from Meta or Telnyx for additional debugging context.
Handling provider errors
if (!response.ok) {
  const { error } = await response.json();

  if (error.provider) {
    console.error("Provider error details:", {
      source: error.provider.source,
      code: error.provider.code,
      message: error.provider.message,
    });

    // If it's a Meta error, save the trace ID for support
    if (error.provider.fbtrace_id) {
      await saveTraceId(messageId, error.provider.fbtrace_id);
    }
  }
}

Handling Validation Errors

When the param field is present, it tells you which request parameter caused the error. Use this to provide targeted feedback in your UI.
Handling validation errors
if (!response.ok) {
  const { error } = await response.json();

  if (error.type === "invalid_request_error" && error.param) {
    // Highlight the specific form field that has an error
    setFieldError(error.param, error.message);
  }
}

Webhook Error Handling

When a message fails to deliver, Chirp sends a webhook event with the error details in the same format as API errors.

Receiving Failed Events

Failed delivery events include an error object in the data payload.
Webhook error handling
app.post("/webhooks/chirp", (req, res) => {
  const { event, data } = req.body;

  if (event === "messages.whatsapp.failed") {
    const { error, message } = data;

    console.error(`Message ${message.id} failed:`, {
      type: error.type,
      code: error.code,
      message: error.message,
      provider: error.provider,
    });

    // Handle specific failure reasons
    switch (error.code) {
      case "conversation_window_expired":
        // Queue a template message instead
        queueTemplateMessage(message.to);
        break;

      case "account_restricted":
        // Alert operations team
        alertOpsTeam("WhatsApp account restricted", error);
        break;

      case "message_undeliverable":
        // Update delivery status in your database
        markMessageFailed(message.id, error.code);
        break;
    }
  }

  if (event === "messages.sms.failed") {
    const { error, message } = data;

    console.error(`SMS ${message.id} failed:`, {
      code: error.code,
      message: error.message,
      provider: error.provider,
    });
  }

  res.sendStatus(200);
});

Webhook Failure Payload

Here is an example of a failed WhatsApp message webhook:
WhatsApp failure webhook
{
  "event": "messages.whatsapp.failed",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "data": {
    "message": {
      "id": "msg_abc123",
      "from": "+15551234567",
      "to": "+15559876543",
      "type": "text"
    },
    "app": {
      "id": "app_xyz"
    },
    "error": {
      "type": "provider_error",
      "code": "message_undeliverable",
      "message": "Unable to deliver message. The recipient may have blocked your number.",
      "provider": {
        "source": "meta",
        "code": 131026,
        "message": "Unable to deliver message"
      }
    }
  }
}

Retry Strategies

Not all errors should be retried. Use the following table to determine whether an error is retryable.

When to Retry

Error Type / CodeRetryableStrategy
api_error (any)YesExponential backoff
meta_api_unavailableYesExponential backoff, max 5 retries
telnyx_api_unavailableYesExponential backoff, max 5 retries
meta_api_errorMaybeRetry once; if it fails again, investigate
telnyx_api_errorMaybeRetry once; if it fails again, investigate
rate_limit_exceededYesWait for Retry-After header, then retry
whatsapp_rate_limitedYesWait and retry with backoff
invalid_request_error (any)NoFix the request parameters
authentication_error (any)NoFix the API key
message_undeliverableNoPermanent failure; do not retry
conversation_window_expiredNoSend a template message instead
account_restrictedNoResolve the restriction with Meta
Never retry invalid_request_error or authentication_error automatically. These indicate a problem with your request that will not resolve on its own. Retrying will waste your rate limit budget.

Exponential Backoff

For retryable errors, use exponential backoff to avoid overwhelming the API.
Exponential backoff
async function retryWithBackoff(fn, maxRetries = 5) {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fn();
    } catch (err) {
      const { error } = err;

      // Don't retry non-retryable errors
      if (
        error?.type === "invalid_request_error" ||
        error?.type === "authentication_error"
      ) {
        throw err;
      }

      // Last attempt - give up
      if (attempt === maxRetries - 1) {
        throw err;
      }

      // Calculate delay: 1s, 2s, 4s, 8s, 16s (with jitter)
      const baseDelay = Math.pow(2, attempt) * 1000;
      const jitter = Math.random() * 1000;
      const delay = baseDelay + jitter;

      console.log(`Retry ${attempt + 1}/${maxRetries} in ${Math.round(delay)}ms`);
      await new Promise((resolve) => setTimeout(resolve, delay));
    }
  }
}
Add random jitter to your retry delays to prevent multiple clients from retrying at the same time (thundering herd problem).

Logging Best Practices

Log error details to help your team debug issues in production.
Error logging
function logApiError(context, error) {
  const logEntry = {
    timestamp: new Date().toISOString(),
    context,
    error: {
      type: error.type,
      code: error.code,
      message: error.message,
      param: error.param,
    },
  };

  // Include provider details for upstream errors
  if (error.provider) {
    logEntry.provider = {
      source: error.provider.source,
      code: error.provider.code,
      message: error.provider.message,
    };

    // fbtrace_id is critical for Meta support tickets
    if (error.provider.fbtrace_id) {
      logEntry.provider.fbtrace_id = error.provider.fbtrace_id;
    }
  }

  console.error(JSON.stringify(logEntry));
}
When contacting Meta support about a WhatsApp issue, always include the fbtrace_id from the error response. This allows Meta to look up the exact request in their systems.