Claude API Rust Example

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.

💥 50p impulse-buy: Power Prompts PDF (first 10 buyers) 30 battle-tested Claude Code prompts · 8-page PDF · paste into CLAUDE.md and never re-type a prompt again · 50p impulse-buy, no commitment

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.

Cargo.toml dependencies

[dependencies]
reqwest = { version = "0.12", features = ["json", "stream"] }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
dotenvy = "0.15"

Minimal request

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(())
}

Reusable ClaudeClient struct

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(())
}

Streaming SSE

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(())
}

Exponential-backoff retry

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?);
    }
}

Rust vs other languages for Claude API

LanguageOfficial SDKHTTP clientStreamingBest for
PythonYes (anthropic)httpxBuilt-inML pipelines, scripting
RustNoreqwestbytes_stream()Systems, WASM, high-throughput backends
GoNonet/httpbufio.ScannerCloud-native services
C#NoHttpClientIAsyncEnumerableEnterprise / .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.

Frequently asked questions

Is there an official Anthropic Rust SDK?
No. As of mid-2026, Anthropic does not publish an official Rust crate. The canonical approach is to use `reqwest` for HTTP and `serde_json` for JSON serialization, both of which are idiomatic Rust choices for REST API clients.
Which crates do I need for the Claude API in Rust?
Add `reqwest` (with the `json` and `stream` features), `tokio` (with `full` feature for async runtime), `serde`, `serde_json`, and optionally `dotenvy` for reading `.env` files. All are available on crates.io.
How do I handle streaming SSE in Rust?
Use `reqwest`'s `Response::bytes_stream()` to get an async byte stream, then split on newlines and parse lines beginning with `data:`. Each data line is a JSON object with `type: content_block_delta` containing the text delta.
How do I set the API key securely in Rust?
Read it from an environment variable with `std::env::var("ANTHROPIC_API_KEY")`. Use the `dotenvy` crate to load a `.env` file during development. Never hardcode API keys in source.
What is the rate limit behavior I should handle in Rust?
The API returns HTTP 429 for rate limits and 529 for overload. Implement exponential backoff: on these status codes, wait `min(initial_delay * 2^attempt, 60)` seconds and retry. Use `reqwest`'s `StatusCode` to detect these.

Free tools

Cost Calculator → API Cookbook → Diff Summarizer → Skills Browser →

More examples

Claude API Python QuickstartClaude API Node.js / TypeScript QuickstartClaude API Streaming in PythonClaude API Streaming in Node.js / TypeScriptClaude API Tool Use in PythonClaude API Tool Use in Node.js / TypeScript