millionsend
Official Rust SDK for MillionSend — a self-hostable, Resend-compatible email API on AWS SES.
The HTTP API is wire-compatible with Resend, and this crate mirrors the shape of
resend-rs, so migrating is mostly a
find-and-replace: swap the crate, the client type, and point the base URL at
your instance.
Async (tokio + reqwest). Every fallible call returns Result<T, Error>.
Install
[]
= "0.1"
= { = "1", = ["macros", "rt-multi-thread"] }
Quickstart
use ;
async
to, cc, bcc, and reply_to accept a single address ("a@b.dev".into()) or
many (vec!["a@b.dev".to_string(), "c@d.dev".to_string()].into()).
Configuration
use MillionSend;
// Explicit base URL.
let ms = with_base_url;
// Key only; base URL falls back to MILLIONSEND_BASE_URL, then http://localhost:3001.
let ms = new;
// Both from the environment (MILLIONSEND_API_KEY + optional MILLIONSEND_BASE_URL).
let ms = from_env?;
MillionSend is self-hosted, so there is no cloud default — set the base URL to
your deployment in production. Every request carries
Authorization: Bearer <api_key> and a millionsend-rust/<version> User-Agent.
Error handling
Fallible calls return Result<T, millionsend::Error>:
Error::Api(ApiError { status_code, name, message })— a non-2xx response.nameis a stable snake_case code you can match on (validation_error,not_found,restricted_api_key,sending_paused, …).Error::Http(_)— a transport failure that never reached the API;err.status_code()isNone.Error::Parse(_)— a 2xx body that failed to deserialize.
match ms.emails.get.await
Resources
Emails
use SendEmailOptions;
ms.emails.send.await?; // POST /emails
ms.emails.send_with_idempotency_key.await?; // + Idempotency-Key
ms.emails.get.await?; // GET /emails/:id
ms.emails.cancel.await?; // POST /emails/:id/cancel
// Batch: 1–100 in one call.
ms.batch.send.await?; // POST /emails/batch
ms.batch.send_with_idempotency_key.await?;
Audiences & contacts
use ;
let audience = ms.audiences.create.await?;
ms.audiences.list.await?;
ms.audiences.get.await?;
ms.audiences.delete.await?;
ms.contacts.create.await?;
// Address by id (a bare &str) or email; email wins if both are set.
ms.contacts.get.await?;
ms.contacts.get.await?;
// null clears a field, omitted leaves it unchanged.
ms.contacts.update.await?;
ms.contacts.delete.await?;
ms.contacts.list.await?; // or list(None, None) for top-level
Topic subscriptions (granular unsubscribe):
use ;
ms.contacts.topics.update.await?;
Topics
use ;
ms.topics.create.await?;
ms.topics.get.await?;
ms.topics.list.await?; // bare { data } — topics are unpaginated
ms.topics.delete.await?;
Broadcasts
use ;
let broadcast = ms.broadcasts.create.await?;
ms.broadcasts.list.await?;
ms.broadcasts.get.await?;
ms.broadcasts.update.await?; // draft only
ms.broadcasts.send.await?; // None = send now
ms.broadcasts.cancel.await?; // scheduled only
ms.broadcasts.delete.await?; // draft only
Segments (MillionSend extension)
Dynamic segments are a saved filter over an audience's contacts — a MillionSend
superset with no Resend equivalent (served at /segments2).
use ;
ms.segments.create.await?;
ms.segments.get.await?; // includes a live contact_count
ms.segments.list.await?;
ms.segments.update.await?;
ms.segments.delete.await?;
Migrating from Resend
- use resend_rs::{Resend, types::CreateEmailBaseOptions};
- let resend = Resend::new("re_123");
+ use millionsend::{MillionSend, SendEmailOptions};
+ let ms = MillionSend::with_base_url("ms_123", "https://mail.acme.dev");
Method names and nesting match. Notes:
- Domains and API keys are managed in the MillionSend dashboard, not via the
API — there are no
domains/api_keysresources here. - Resend's
segmentsis an alias of audiences; MillionSend'ssegmentsis the distinct dynamic-filter feature. Useaudiencesfor a straight port.
License
MIT