Send emails with Rust
Learn how to send transactional emails using PostStack and Rust.
Rust has no stdlib HTTP client, so every integration starts with picking one. `reqwest` is by far the most common — it wraps `hyper` with ergonomic async/await, JSON helpers, and connection pooling. For very small services that want to avoid Tokio, `ureq` is a sync alternative. The PostStack API surface is simple enough that either client is a few lines of code.
1. Install the SDK
# Cargo.toml
[dependencies]
reqwest = { version = "0.12", features = ["json"] }
serde_json = "1"
tokio = { version = "1", features = ["full"] }2. Initialize the client
use reqwest::Client;
let client = Client::new();
let api_key = std::env::var("POSTSTACK_API_KEY")?;3. Send an email
use reqwest::Client;
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let api_key = std::env::var("POSTSTACK_API_KEY")?;
let response = Client::new()
.post("https://api.poststack.dev/emails")
.header("Authorization", format!("Bearer {}", api_key))
.json(&json!({
"from": "hello@yourdomain.com",
"to": ["user@example.com"],
"subject": "Hello from Rust!",
"html": "<h1>Welcome!</h1>"
}))
.send()
.await?;
println!("{}", response.text().await?);
Ok(())
}4. Handle errors
Rust idioms for error handling, retries, and structured logging when calling the PostStack API.
use reqwest::{Client, StatusCode};
use serde_json::Value;
use std::time::Duration;
use tokio::time::sleep;
pub struct PoststackError {
pub status: u16,
pub body: String,
pub request_id: Option<String>,
}
pub async fn send_with_retry(
client: &Client,
api_key: &str,
payload: &Value,
) -> Result<Value, PoststackError> {
for attempt in 0..3 {
let res = client
.post("https://api.poststack.dev/emails")
.bearer_auth(api_key)
.json(payload)
.timeout(Duration::from_secs(10))
.send()
.await
.map_err(|e| PoststackError {
status: 0,
body: e.to_string(),
request_id: None,
})?;
let status = res.status();
let request_id = res
.headers()
.get("x-request-id")
.and_then(|v| v.to_str().ok())
.map(String::from);
if status.is_success() {
return res.json::<Value>().await.map_err(|e| PoststackError {
status: 0,
body: e.to_string(),
request_id,
});
}
if status == StatusCode::TOO_MANY_REQUESTS || status.is_server_error() {
sleep(Duration::from_secs(1u64 << attempt)).await;
continue;
}
let body = res.text().await.unwrap_or_default();
return Err(PoststackError {
status: status.as_u16(),
body,
request_id,
});
}
Err(PoststackError {
status: 0,
body: "retries exhausted".into(),
request_id: None,
})
}Framework integrations
Axum
Build a state struct holding the `reqwest::Client` and API key, attach it to the router with `Router::with_state`, and call `state.client.post(...).send().await` inside your handler. `Client` is `Clone`-cheap (Arc-wrapped under the hood) so sharing it is free.
Actix-web
Register the `Client` as a `web::Data<...>` resource at startup. Inject via `client: web::Data<Client>` in your handler signature. The same `Client` instance lives for the lifetime of the server.
Tokio tasks / background workers
Use `tokio::spawn` for fire-and-forget sends, or a channel-based worker pool for batch processing. The `Client` is `Send + Sync` so passing it across task boundaries is free.
Sync usage (ureq)
For CLI tools or small sync services, `ureq::post(...).set("authorization", ...).send_json(payload)?` is a complete integration in three lines. No async runtime needed.
Common pitfalls
Recreating the Client per request
`reqwest::Client::new()` allocates a new connection pool every time. Build one at startup and clone it (Arc internally, cheap) into each handler.
Missing TLS feature on `reqwest`
`reqwest = { version = "0.12" }` without features defaults to native-TLS on some platforms. Use `features = ["json", "rustls-tls"]` for predictable cross-platform behaviour.
Awaiting outside an async runtime
Calling `.await` from a non-async context panics with "future not awaited inside a runtime". Wrap async code with `#[tokio::main]` or use the sync `ureq` client instead.
Notes
- Uses reqwest for HTTP requests — the most popular Rust HTTP client
FAQ
Why no official Rust SDK?
The PostStack API surface is small enough that adding an SDK would mostly add maintenance overhead. The reqwest example in this guide is the complete integration. We may ship an official SDK if demand grows.
reqwest or ureq?
reqwest if you are already in an async runtime (Axum, Actix, Tokio). ureq if you want to avoid pulling in Tokio for a small CLI or sync service.
How do I integrate with Axum?
Build a state struct, attach with `Router::with_state`, and inject into handlers via `State<...>`. See the integrations section for a worked example.
Related guides
Ready to send emails with Rust?
Create a free account and get your API key in under a minute.