Call the Anthropic Claude API from Rust using reqwest and tokio. Complete working examples: minimal request, system prompt, streaming SSE, retry logic, and async patterns.
Rust has no official Anthropic SDK, so you call the Claude API directly over HTTP using reqwest and tokio. This guide shows every pattern you need: minimal request, streaming, retry logic, and async best practices.
[dependencies]
reqwest = { version = "0.12", features = ["json", "stream"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
dotenvy = "0.15"
use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
use serde_json::{json, Value};
use std::env;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
dotenvy::dotenv().ok();
let api_key = env::var("ANTHROPIC_API_KEY")?;
let mut headers = HeaderMap::new();
headers.insert("x-api-key", HeaderValue::from_str(&api_key)?);
headers.insert("anthropic-version", HeaderValue::from_static("2023-06-01"));
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
let client = reqwest::Client::builder()
.default_headers(headers)
.build()?;
let body = json!({
"model": "claude-sonnet-4-6",
"max_tokens": 1024,
"messages": [{"role": "user", "content": "Explain Rust ownership in one paragraph."}]
});
let resp: Value = client
.post("https://api.anthropic.com/v1/messages")
.json(&body)
.send()
.await?
.json()
.await?;
println!("{}", resp["content"][0]["text"].as_str().unwrap_or(""));
Ok(())
}
use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
use serde_json::{json, Value};
use std::env;
pub struct ClaudeClient {
http: reqwest::Client,
base_url: &'static str,
}
impl ClaudeClient {
pub fn new() -> Self {
dotenvy::dotenv().ok();
let api_key = env::var("ANTHROPIC_API_KEY").expect("ANTHROPIC_API_KEY not set");
let mut headers = HeaderMap::new();
headers.insert("x-api-key", HeaderValue::from_str(&api_key).unwrap());
headers.insert("anthropic-version", HeaderValue::from_static("2023-06-01"));
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
Self {
http: reqwest::Client::builder()
.default_headers(headers)
.build()
.unwrap(),
base_url: "https://api.anthropic.com/v1",
}
}
pub async fn message(
&self,
model: &str,
system: Option<&str>,
user_msg: &str,
max_tokens: u32,
) -> Result<String, Box<dyn std::error::Error>> {
let mut body = json!({
"model": model,
"max_tokens": max_tokens,
"messages": [{"role": "user", "content": user_msg}]
});
if let Some(sys) = system {
body["system"] = json!(sys);
}
let resp: Value = self.http
.post(format!("{}/messages", self.base_url))
.json(&body)
.send()
.await?
.json()
.await?;
Ok(resp["content"][0]["text"].as_str().unwrap_or("").to_string())
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = ClaudeClient::new();
let text = client
.message("claude-sonnet-4-6", Some("You are a Rust expert."), "What is the borrow checker?", 512)
.await?;
println!("{text}");
Ok(())
}
use futures_util::StreamExt;
use reqwest::header::{HeaderMap, HeaderValue, CONTENT_TYPE};
use serde_json::Value;
use std::env;
// Add futures-util = "0.3" to Cargo.toml
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
dotenvy::dotenv().ok();
let api_key = env::var("ANTHROPIC_API_KEY")?;
let mut headers = HeaderMap::new();
headers.insert("x-api-key", HeaderValue::from_str(&api_key)?);
headers.insert("anthropic-version", HeaderValue::from_static("2023-06-01"));
headers.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
let client = reqwest::Client::builder().default_headers(headers).build()?;
let body = serde_json::json!({
"model": "claude-sonnet-4-6",
"max_tokens": 1024,
"stream": true,
"messages": [{"role": "user", "content": "Write a Rust async example with tokio."}]
});
let mut stream = client
.post("https://api.anthropic.com/v1/messages")
.json(&body)
.send()
.await?
.bytes_stream();
let mut buf = String::new();
while let Some(chunk) = stream.next().await {
let chunk = chunk?;
buf.push_str(&String::from_utf8_lossy(&chunk));
// Process complete lines
while let Some(pos) = buf.find('
') {
let line = buf[..pos].trim().to_string();
buf = buf[pos + 1..].to_string();
if let Some(data) = line.strip_prefix("data: ") {
if data == "[DONE]" { break; }
if let Ok(val) = serde_json::from_str::<Value>(data) {
if val["type"] == "content_block_delta" {
if let Some(text) = val["delta"]["text"].as_str() {
print!("{text}");
}
}
}
}
}
}
println!();
Ok(())
}
use std::time::Duration;
use tokio::time::sleep;
async fn call_with_retry(
client: &reqwest::Client,
body: &serde_json::Value,
max_retries: u32,
) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
let mut attempt = 0u32;
loop {
let resp = client
.post("https://api.anthropic.com/v1/messages")
.json(body)
.send()
.await?;
let status = resp.status();
if status == 429 || status == 529 {
if attempt >= max_retries {
return Err(format!("Max retries exceeded (status {status})").into());
}
let delay_secs = std::cmp::min(2u64.pow(attempt), 60);
eprintln!("Rate limited ({status}), retrying in {delay_secs}s…");
sleep(Duration::from_secs(delay_secs)).await;
attempt += 1;
continue;
}
return Ok(resp.json().await?);
}
}
| Language | Official SDK | HTTP client | Streaming | Best for |
|---|---|---|---|---|
| Python | Yes (anthropic) | httpx | Built-in | ML pipelines, scripting |
| Rust | No | reqwest | bytes_stream() | Systems, WASM, high-throughput backends |
| Go | No | net/http | bufio.Scanner | Cloud-native services |
| C# | No | HttpClient | IAsyncEnumerable | Enterprise / .NET |
Estimate token costs before scaling with the Claude API Cost Calculator. For more low-level REST patterns, see the cURL example or the Go example.