Claude API Rust Integration Example with reqwest
Integrating the Claude API into a Rust application comes down to three crates: reqwest for HTTP, serde/serde_json for payload handling, and tokio for async execution. There's no official Rust SDK from Anthropic, so most Rust developers either hand-roll a thin client against the Messages API or go through a gateway that normalizes the HTTP contract. Both approaches work — this article shows the raw integration first, then the gateway alternative for teams who don't want to maintain their own retry/streaming logic.
If you're searching for a Claude API Rust integration example because you're building a CLI tool, a backend service, or an embedded agent in a Rust binary, the core pattern is always the same: build a JSON request matching the Messages API schema, send it as a POST with your API key in a header, and deserialize the response into typed structs. Below is a complete, minimal example you can drop into a main.rs.
Setting Up the Project
Add the dependencies to Cargo.toml:
[dependencies]
reqwest = { version = "0.12", features = ["json", "stream"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }
futures-util = "0.3"
A Basic Non-Streaming Request
use serde::{Deserialize, Serialize};
#[derive(Serialize)]
struct Message {
role: String,
content: String,
}
#[derive(Serialize)]
struct MessageRequest {
model: String,
max_tokens: u32,
messages: Vec<Message>,
}
#[derive(Deserialize, Debug)]
struct ContentBlock {
text: String,
}
#[derive(Deserialize, Debug)]
struct MessageResponse {
content: Vec<ContentBlock>,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let api_key = std::env::var("CLAUDE_API_KEY")?;
let request_body = MessageRequest {
model: "claude-sonnet-4-5".to_string(),
max_tokens: 1024,
messages: vec![Message {
role: "user".to_string(),
content: "Explain ownership in Rust in two sentences.".to_string(),
}],
};
let client = reqwest::Client::new();
let response = client
.post("https://api.anthropic.com/v1/messages")
.header("x-api-key", api_key)
.header("anthropic-version", "2023-06-01")
.header("content-type", "application/json")
.json(&request_body)
.send()
.await?;
let parsed: MessageResponse = response.json().await?;
println!("{}", parsed.content[0].text);
Ok(())
}
This compiles, runs, and gives you a typed response. The structs only capture the fields you need — Rust's serde ignores unknown fields by default, so you don't have to model the full response schema upfront.
Handling Streaming Responses
Streaming is where Rust integrations get more interesting, since you're dealing with Server-Sent Events over a byte stream rather than a single JSON blob. Using futures-util::StreamExt:
use futures_util::StreamExt;
async fn stream_response(client: &reqwest::Client, api_key: &str) -> Result<(), Box<dyn std::error::Error>> {
let body = serde_json::json!({
"model": "claude-sonnet-4-5",
"max_tokens": 512,
"stream": true,
"messages": [{"role": "user", "content": "Write a haiku about async Rust."}]
});
let response = client
.post("https://api.anthropic.com/v1/messages")
.header("x-api-key", api_key)
.header("anthropic-version", "2023-06-01")
.json(&body)
.send()
.await?;
let mut stream = response.bytes_stream();
while let Some(chunk) = stream.next().await {
let bytes = chunk?;
let text = String::from_utf8_lossy(&bytes);
for line in text.lines() {
if let Some(data) = line.strip_prefix("data: ") {
if let Ok(json) = serde_json::from_str::<serde_json::Value>(data) {
if let Some(delta) = json.get("delta").and_then(|d| d.get("text")) {
print!("{}", delta.as_str().unwrap_or(""));
}
}
}
}
}
Ok(())
}
Parsing SSE manually like this works but is brittle: chunks don't always align with event boundaries, so production code needs a buffer that accumulates partial lines across reads. This is usually the first thing teams end up building a small internal library around.
Error Handling and Retries
Rust's Result type makes error handling explicit, which is useful here because Claude API responses include rate limits (429), overload errors (529), and transient 5xx failures that all warrant different retry strategies:
async fn send_with_retry(client: &reqwest::Client, body: &serde_json::Value, api_key: &str) -> Result<reqwest::Response, reqwest::Error> {
let mut attempts = 0;
loop {
let resp = client
.post("https://api.anthropic.com/v1/messages")
.header("x-api-key", api_key)
.header("anthropic-version", "2023-06-01")
.json(body)
.send()
.await?;
if resp.status().is_success() || attempts >= 3 {
return Ok(resp);
}
attempts += 1;
tokio::time::sleep(std::time::Duration::from_millis(500 * attempts as u64)).await;
}
}
Simplifying the Integration with a Gateway
Hand-rolling SSE parsing, retry logic, and key rotation in Rust is doable, but it's also exactly the kind of plumbing that's easy to get subtly wrong — especially around partial SSE frames and backoff jitter. If your goal is shipping a feature rather than maintaining an HTTP client, routing requests through SubToAPI keeps the same Messages-API-shaped request body and response format you'd write against Anthropic directly, just with a sub_live_... application key, team-level usage visibility, and streaming/tool-use support already handled server-side.
The Rust code barely changes — swap the base URL and header:
let response = client
.post("https://api.subtoapi.app/v1/messages")
.header("Authorization", format!("Bearer {}", std::env::var("SUBTOAPI_KEY")?))
.json(&request_body)
.send()
.await?;
That's useful when multiple services or teammates need their own scoped API keys without each person touching a shared Claude account — see the quickstart and messages endpoint docs for the full request/response reference, and the streaming guide if you're building the SSE path above. Plans start at Solo €9 with a free trial at signup, and pricing covers Team and Scale tiers for seat-based access.
Structuring a Reusable Client
For anything beyond a script, wrap the request logic in a struct so model name, max tokens, and headers aren't repeated everywhere:
struct ClaudeClient {
http: reqwest::Client,
api_key: String,
base_url: String,
}
impl ClaudeClient {
fn new(api_key: String) -> Self {
Self {
http: reqwest::Client::new(),
api_key,
base_url: "https://api.anthropic.com/v1".to_string(),
}
}
async fn send_message(&self, prompt: &str) -> Result<String, Box<dyn std::error::Error>> {
let body = serde_json::json!({
"model": "claude-sonnet-4-5",
"max_tokens": 1024,
"messages": [{"role": "user", "content": prompt}]
});
let resp: MessageResponse = self.http
.post(format!("{}/messages", self.base_url))
.header("x-api-key", &self.api_key)
.header("anthropic-version", "2023-06-01")
.json(&body)
.send()
.await?
.json()
.await?;
Ok(resp.content[0].text.clone())
}
}
This pattern scales to tool use too — you'd add a tools field to the JSON body and handle tool_use content blocks in the response the same way you handle text blocks. See tool use docs if you're routing through SubToAPI for that.
Questions
Does Anthropic provide an official Rust SDK? No. Anthropic maintains official SDKs for Python and TypeScript. Rust developers call the HTTP API directly with reqwest or use community crates, which vary in maintenance activity.
How do I handle streaming SSE responses in Rust without missing chunks? Buffer incoming bytes until you have complete \n\n-delimited events before parsing, since TCP reads don't align with SSE event boundaries. Don't assume each bytes_stream() item is a complete event.
Can I use tokio and async-std interchangeably for this? reqwest works with both, but you must pick one runtime for your whole binary. The examples above use tokio because it's the more common choice for HTTP-heavy services.