zeroclaw-labs/zeroclaw · error

WhatsApp API error: {status}

Error message

WhatsApp API error: {status}

What it means

`post_to_meta` posts interactive (button/list) messages to the Meta Graph API and bails whenever the response status is not 2xx. Only the HTTP status code reaches the returned error; Meta's detailed error body is recorded to the zeroclaw log on the same event (attrs `status` and `error_body`, module whatsapp), so the log is where the real cause lives. Common statuses: 401 bad token, 400 payload contract violation, 429 rate limit, 5xx Meta incidents.

Source

Thrown at crates/zeroclaw-channels/src/whatsapp.rs:689

            }
        });
        self.post_to_meta(&url, &body).await
    }

    async fn post_to_meta(&self, url: &str, body: &serde_json::Value) -> anyhow::Result<()> {
        let resp = self
            .http_client()
            .post(url)
            .bearer_auth(&self.access_token)
            .header("Content-Type", "application/json")
            .json(body)
            .send()
            .await?;
        if !resp.status().is_success() {
            let status = resp.status();
            let error_body = resp.text().await.unwrap_or_default();
            ::zeroclaw_log::record!(ERROR, ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Fail).with_outcome(::zeroclaw_log::EventOutcome::Failure).with_attrs(::serde_json::json!({"status": status.to_string(), "error_body": error_body})), "WhatsApp interactive send failed:");
            anyhow::bail!("WhatsApp API error: {status}");
        }
        Ok(())
    }
}

/// One section in an interactive list message. Sections group related
/// rows under a header.
#[derive(Debug, Clone)]
pub struct InteractiveListSection {
    /// Section header (Meta caps at 24 chars; we truncate).
    pub title: String,
    /// Rows in this section. Up to 10 per Meta's limit.
    pub rows: Vec<InteractiveListRow>,
}

/// One row in an interactive list message.
#[derive(Debug, Clone)]
pub struct InteractiveListRow {

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Inspect the zeroclaw log entry for this send — the `error_body` attr contains Meta's error code and message, which names the exact field or limit at fault.
  2. On 401: refresh the WhatsApp access token; prefer a permanent token from a system user in Meta Business Manager.
  3. On 429: slow down sends and add exponential backoff between batches.
  4. On 5xx: retry with backoff and check Meta's platform status page.

Example fix

// before
channel.send_interactive_buttons(to, "Proceed?", &buttons).await?;

// after: retry transient statuses, surface contract/auth errors
let mut backoff = std::time::Duration::from_secs(2);
for _ in 0..3 {
    match channel.send_interactive_buttons(to, "Proceed?", &buttons).await {
        Ok(()) => break,
        Err(e) => {
            let msg = e.to_string();
            if msg.contains("429") || msg.contains("WhatsApp API error: 5") {
                tokio::time::sleep(backoff).await;
                backoff *= 2;
            } else {
                return Err(e); // 400/401: fix payload or token, do not retry
            }
        }
    }
}
Defensive patterns

Strategy: retry

Validate before calling

// Preflight credentials before an interactive burst
if !channel.health_check().await {
    anyhow::bail!("WhatsApp Cloud API preflight failed: check access token / phone-number id");
}

Try / catch

match channel.send_interactive_buttons(to, body, &buttons).await {
    Ok(()) => Ok(()),
    Err(e) => {
        let msg = e.to_string();
        // real cause (Meta error body) is in the zeroclaw log's error_body attr
        if msg.contains("WhatsApp API error: 429") || msg.contains("WhatsApp API error: 5") {
            // transient: back off and retry
            retry_with_backoff(|| channel.send_interactive_buttons(to, body, &buttons)).await
        } else {
            Err(e) // 4xx contract/auth failure: fix payload or token
        }
    }
}

Prevention

When it happens

Trigger: Any non-success Graph API response during `send_interactive_buttons` or `send_interactive_list`: expired or invalid access token (401), payload that violates Meta's limits such as over-long button titles or invalid characters (400), messaging rate limit after bulk sends (429), or a Meta-side incident (5xx).

Common situations: Temporary WhatsApp access tokens expiring after 24h instead of using a permanent system-user token; button/row titles that break Meta's length or character rules; burst sends tripping rate limits; Graph API version deprecation.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/2df68aa822b3e99c. Report an issue: GitHub.