zeroclaw-labs/zeroclaw · error · anyhow::Error

API error: {status}

Error message

API error: {status}

What it means

Linq channel (api.linqapp.com partner API v3): send() first POSTs to /chats/{recipient}/messages treating the recipient as a chat_id; on 404 it falls back to creating a new chat (POST /chats with from_phone and the recipient), and this error means that chat-creation call itself returned non-2xx. The bail message carries only the HTTP status — the detailed error body is recorded as the error_body attribute of the zeroclaw log event on the line above, so check the logs for the real reason. A sibling bail at linq.rs:369 covers non-404 send failures.

Source

Thrown at crates/zeroclaw-channels/src/linq.rs:352

                        "value": message.content
                    }]
                }
            });

            let create_resp = self
                .client
                .post(format!("{LINQ_API_BASE}/chats"))
                .bearer_auth(&self.api_token)
                .header("Content-Type", "application/json")
                .json(&new_chat_body)
                .send()
                .await?;

            if !create_resp.status().is_success() {
                let status = create_resp.status();
                let error_body = create_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})), "create chat failed:");
                anyhow::bail!("API error: {status}");
            }

            return Ok(());
        }

        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})
                ),
            "send failed:"
        );
        anyhow::bail!("API error: {status}");
    }

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Open the zeroclaw log for the matching 'create chat failed:' event and read attrs.error_body — it contains Linq's actual error message.
  2. 401 -> re-issue the partner api_token and update the linq channel config.
  3. 400 -> verify from_phone is a valid registered number and the recipient is a well-formed E.164 phone or existing chat id.
  4. Improve the error surface: include error_body in the bail message so callers see the cause without log access.

Example fix

// before (crates/zeroclaw-channels/src/linq.rs:350-352)
let error_body = create_resp.text().await.unwrap_or_default();
::zeroclaw_log::record!(ERROR, /* ... */, "create chat failed:");
anyhow::bail!("API error: {status}");

// after: surface the upstream reason to the caller
let error_body = create_resp.text().await.unwrap_or_default();
::zeroclaw_log::record!(ERROR, /* ... */, "create chat failed:");
anyhow::bail!("create chat failed: status={status}, body={error_body}");
Defensive patterns

Strategy: try-catch

Validate before calling

// Cheap credential probe before relying on the linq channel
async fn linq_token_ok(client: &reqwest::Client, token: &str) -> bool {
    client.get("https://api.linqapp.com/api/partner/v3/phonenumbers")
        .bearer_auth(token).send().await
        .map(|r| r.status().is_success()).unwrap_or(false)
}

Type guard

fn is_linq_api_status_error(err: &anyhow::Error) -> bool {
    err.to_string().trim_start_matches("Caused by:").contains("API error: ")
}

Try / catch

if let Err(e) = linq_channel.send(msg).await {
    if is_linq_api_status_error(&e) {
        // the bail omits the body: pull attrs.error_body from the zeroclaw log event first
        tracing::error!(error = %e, "linq send failed; consult error_body in log attrs");
        if e.to_string().contains("API error: 401") { alert("linq api_token expired"); }
        return Ok(()); // treat as channel-level degradation, keep conversation alive
    }
    return Err(e);
}

Prevention

When it happens

Trigger: 401 with an invalid or expired api_token; 400 when from_phone is missing/unverified or the recipient phone number is malformed; 429/5xx from Linq rate limits or incidents — all hit after the initial chat send already 404'd (typical when the recipient is a phone number with no existing chat).

Common situations: Expired partner API token; from_phone not registered/verified with the Linq account; recipient numbers not in E.164; wrong token for the environment; the recipient string being a phone number (by design) so every first send goes through the create-chat path.

Related errors


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