Claude API Rust SDK Example: A Working Guide
Is there an official Claude API Rust SDK?
No. Anthropic ships official SDKs for Python and TypeScript, but not for Rust. There are a handful of community crates on crates.io that wrap the Messages API, but they vary in maintenance quality, lag behind new features (extended thinking, tool use, prompt caching), and some are abandoned after a version or two.
The practical answer for most Rust projects is to skip the hunt for a "complete" SDK and call the REST API directly with reqwest and serde. The Messages API is a plain JSON-over-HTTPS interface, so a minimal client is maybe 60 lines of Rust, fully under your control, and easy to extend as the API changes. Below is a working example you can drop into a project today, followed by notes on streaming, tool use, and error handling.
Setting up the project
# 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"
This gives you an async HTTP client, JSON (de)serialization, and the tooling needed to consume a server-sent events stream later on.
A minimal Claude API client in Rust
use serde::{Deserialize, Serialize};
use std::env;
#[derive(Serialize)]
struct Message {
role: String,
content: String,
}
#[derive(Serialize)]
struct MessagesRequest {
model: String,
max_tokens: u32,
messages: Vec<Message>,
}
#[derive(Deserialize, Debug)]
struct ContentBlock {
#[serde(rename = "type")]
block_type: String,
text: Option<String>,
}
#[derive(Deserialize, Debug)]
struct MessagesResponse {
content: Vec<ContentBlock>,
usage: Usage,
}
#[derive(Deserialize, Debug)]
struct Usage {
input_tokens: u32,
output_tokens: u32,
}
async fn send_message(prompt: &str) -> Result<MessagesResponse, reqwest::Error> {
let api_key = env::var("ANTHROPIC_API_KEY").expect("missing API key");
let client = reqwest::Client::new();
let body = MessagesRequest {
model: "claude-sonnet-4-5".to_string(),
max_tokens: 1024,
messages: vec![Message {
role: "user".to_string(),
content: prompt.to_string(),
}],
};
let resp = 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(&body)
.send()
.await?
.json::<MessagesResponse>()
.await?;
Ok(resp)
}
#[tokio::main]
async fn main() {
let result = send_message("Explain ownership in Rust in two sentences.")
.await
.expect("request failed");
for block in &result.content {
if block.block_type == "text" {
println!("{}", block.text.clone().unwrap_or_default());
}
}
println!("tokens in/out: {}/{}", result.usage.input_tokens, result.usage.output_tokens);
}
This compiles as-is once you add a real model name and key. The important part is the shape: a typed request struct, a typed response struct, and a plain reqwest::Client doing the HTTP work. No macro magic, no hidden retry logic — which makes it easy to reason about and extend.
Streaming responses in Rust
Streaming with the raw API means consuming server-sent events line by line. With reqwest's stream feature you can read the response body as a byte stream and parse data: lines yourself:
use futures_util::StreamExt;
async fn stream_message(prompt: &str) -> Result<(), reqwest::Error> {
let api_key = std::env::var("ANTHROPIC_API_KEY").unwrap();
let client = reqwest::Client::new();
let body = serde_json::json!({
"model": "claude-sonnet-4-5",
"max_tokens": 1024,
"stream": true,
"messages": [{"role": "user", "content": prompt}]
});
let mut stream = client
.post("https://api.anthropic.com/v1/messages")
.header("x-api-key", api_key)
.header("anthropic-version", "2023-06-01")
.json(&body)
.send()
.await?
.bytes_stream();
while let Some(chunk) = stream.next().await {
let chunk = chunk?;
let text = String::from_utf8_lossy(&chunk);
for line in text.lines() {
if line.starts_with("data:") {
println!("{}", line.trim_start_matches("data:").trim());
}
}
}
Ok(())
}
This is bare-bones — production code should parse the JSON payload of each event (content_block_delta, message_stop, etc.) rather than printing raw lines, but the stream handling pattern is correct and reusable.
Adding tool use
Tool calls work the same way: you add a tools array to the request body describing the function schema, and when Claude wants to call one, the response contains a tool_use content block with a name and JSON input instead of plain text. Your Rust code checks block_type == "tool_use", executes the matching function, and sends the result back in a follow-up request with a tool_result block. The request/response structs above extend naturally — add a tools: Option<Vec<ToolSchema>> field and an input: Option<serde_json::Value> field on ContentBlock.
Using SubToAPI instead of rolling your own key management
If you're building a Rust service that needs Claude access but you don't want to manage Anthropic billing, rate limits, and raw API keys directly, SubToAPI turns your existing Claude subscription into a standard HTTPS API with its own application keys (sub_live_...). The Rust client above works unchanged — you just point it at a different base URL and header:
.post("https://api.subtoapi.app/v1/messages")
.header("authorization", format!("Bearer {}", std::env::var("SUBTOAPI_KEY").unwrap()))
That gets you streaming, tool use, and usage metadata through the same request shape, plus a dashboard for team seats and per-key usage without writing any extra Rust. See the quickstart and the messages and streaming docs for the exact payloads, and pricing for plan details. There's a free trial at signup if you want to test it against your own client code first.
Error handling and retries
A few things worth building into any Rust Claude client from day one:
- Match on HTTP status before deserializing. A 429 or 529 response won't match your success struct and will panic a naive
.json::<T>()call. - Retry with backoff on 429/5xx, not on 400/401 — those are permanent until you fix the request or the key.
- Set a request timeout on the
reqwest::Client(.timeout(Duration::from_secs(60))) since long generations can otherwise hang indefinitely on a dropped connection. - Log
usagefrom every response if you're billing per-call or tracking costs; it's the only place token counts are reported.
Questions
Is there a crates.io package I should use instead of writing this myself? You can, but check the commit history first — several Claude-related crates haven't been updated for current API versions. For most projects, a 60-line reqwest wrapper like the one above is easier to maintain than depending on an unmaintained crate.
Does the raw Messages API support async/await in Rust? Yes, through reqwest's async client and tokio as the runtime, as shown in the examples above. There's no blocking-only limitation.
Can I use this same Rust code with SubToAPI instead of Anthropic directly? Yes — the request and response JSON shapes match, so you only change the base URL and swap the x-api-key header for a Bearer token as shown in the SubToAPI section above.