# Rust

Use the official [Lettermint Rust SDK](https://github.com/lettermint/lettermint-rust) to send email from a Rust application. The SDK also supports the Team API and webhook signature verification. See [SDKs and integrations](/sdks) for other packages.

## Requirements

- Rust 1.98 or newer
- Tokio or another async runtime that is compatible with `reqwest`
- A [Project API token](/platform/projects-and-routes/api-tokens)

## Install the SDK

Add the SDK and Tokio to `Cargo.toml`:

```toml
[dependencies]
lettermint = "1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
```

Run `cargo build` to install the dependencies. The package is also available on [crates.io](https://crates.io/crates/lettermint), and its API reference is on [docs.rs](https://docs.rs/lettermint).

## Send your first email

Store your Project API token in `LETTERMINT_TOKEN`. Then, create an email client and send a message:

```rust
use lettermint::Lettermint;

#[tokio::main]
async fn main() -> lettermint::Result<()> {
    let email = Lettermint::email(std::env::var("LETTERMINT_TOKEN").unwrap())?;

    let response = email
        .email()
        .from("John Doe <john@yourdomain.com>")
        .to("recipient@example.com")
        .subject("Hello from Lettermint")
        .text("This is a test email sent with the Lettermint Rust SDK.")
        .html("<p>This is a test email sent with the Lettermint Rust SDK.</p>")
        .send()
        .await?;

    println!("Email sent with ID: {}", response.message_id);

    Ok(())
}
```

API acceptance does not confirm delivery. Use [webhooks](/platform/webhooks/introduction) to receive delivery and bounce events.

## Add email options

Each call to `email.email()` starts a new message. Options from one message do not stay in the next message.

### Recipients and reply-to addresses

Call `to`, `cc`, `bcc`, or `reply_to` more than once to add multiple addresses:

```rust
let response = email
    .email()
    .from("support@yourdomain.com")
    .to("customer@example.com")
    .to("account-owner@example.com")
    .cc("manager@yourdomain.com")
    .bcc("archive@yourdomain.com")
    .reply_to("help@yourdomain.com")
    .subject("Your support request")
    .text("We received your support request.")
    .send()
    .await?;
```

### Metadata and headers

Use metadata to add application data to webhook payloads. Metadata does not add headers to the email:

```rust
let response = email
    .email()
    .from("orders@yourdomain.com")
    .to("customer@example.com")
    .subject("Order confirmation")
    .text("Your order is confirmed.")
    .metadata("order_id", "12345")
    .metadata("customer_id", "cust_789")
    .header("X-Priority", "1")
    .send()
    .await?;
```

### Tags

Use typed name and value tags to organize messages for filtering and analytics:

```rust
use lettermint::types::MessageTag;

let response = email
    .email()
    .from("notifications@yourdomain.com")
    .to("user@example.com")
    .subject("Your account is ready")
    .text("You can now sign in.")
    .tags([
        MessageTag::new("campaign", "welcome")?,
        MessageTag::new("customer", "new")?,
    ])
    .send()
    .await?;
```

You can add up to 20 typed tags. The `tag` method remains available for one legacy tag. You can add up to 19 typed tags when you also use a legacy tag. See [Tags](/platform/emails/tags) for the value rules.

### Route and tracking settings

Select a route and control open and click tracking for the message:

```rust
use lettermint::email::EmailSettings;
use lettermint::types::TlsPolicy;

let response = email
    .email()
    .from("notifications@yourdomain.com")
    .to("user@example.com")
    .subject("Security alert")
    .text("A new device signed in to your account.")
    .route("transactional")
    .settings(EmailSettings {
        track_opens: Some(false),
        track_clicks: Some(false),
        tls: Some(TlsPolicy::Enforced),
    })
    .send()
    .await?;
```

See [open tracking](/platform/emails/tracking/open-tracking), [click tracking](/platform/emails/tracking/click-tracking), and [TLS delivery](/platform/emails/tls) for the related behavior.

### Attachments and inline images

Attachment content must be Base64-encoded. Add the `base64` crate if your application does not already encode files:

```toml
[dependencies]
base64 = "0.22"
```

```rust
use base64::{Engine as _, engine::general_purpose::STANDARD};

let invoice = std::fs::read("invoice.pdf").expect("read invoice.pdf");

let response = email
    .email()
    .from("billing@yourdomain.com")
    .to("customer@example.com")
    .subject("Your invoice")
    .text("Your invoice is attached.")
    .attach_with_options(
        "invoice.pdf",
        STANDARD.encode(invoice),
        None,
        Some("application/pdf".to_string()),
    )
    .send()
    .await?;
```

For an inline image, set the content ID and use the same value in the HTML `cid:` URL:

```rust
let response = email
    .email()
    .from("welcome@yourdomain.com")
    .to("customer@example.com")
    .subject("Welcome")
    .html(r#"<p>Welcome!</p><img src="cid:logo" alt="Company logo">"#)
    .attach_with_options(
        "logo.png",
        encoded_logo,
        Some("logo".to_string()),
        Some("image/png".to_string()),
    )
    .send()
    .await?;
```

See [Attachments and inline images](/platform/emails/inline-images) for size and file type limits.

### Idempotency

Use the same idempotency key when you retry the same logical email:

```rust
let response = email
    .email()
    .from("orders@yourdomain.com")
    .to("customer@example.com")
    .subject("Order confirmation")
    .text("Your order is confirmed.")
    .idempotency_key("order-12345-confirmation")
    .send()
    .await?;
```

See [Idempotency](/platform/emails/idempotency) for key rules and the retention period.

### Schedule an email

Use an RFC 3339 UTC timestamp to schedule an email:

```rust
let response = email
    .email()
    .from("events@yourdomain.com")
    .to("attendee@example.com")
    .subject("Event reminder")
    .text("Your event starts tomorrow.")
    .scheduled_at("2026-10-01T09:00:00Z")
    .send()
    .await?;
```

See [Email scheduling](/platform/emails/scheduling) for scheduling limits and cancellation behavior.

## Handle errors

Match the SDK error variants to handle API, validation, network, and decoding failures:

```rust
use lettermint::{Error, Lettermint};

async fn send_email() -> lettermint::Result<()> {
    let email = Lettermint::email(std::env::var("LETTERMINT_TOKEN").unwrap())?;

    match email
        .email()
        .from("sender@yourdomain.com")
        .to("recipient@example.com")
        .subject("Test")
        .text("Hello!")
        .send()
        .await
    {
        Ok(response) => println!("Status: {:?}", response.status),
        Err(Error::Validation { error_type, body }) => {
            eprintln!("Validation error: {error_type}, details: {body:?}");
        }
        Err(Error::Http { status, message, .. }) => {
            eprintln!("API error {status}: {message}");
        }
        Err(Error::Request(error)) => eprintln!("Request failed: {error}"),
        Err(error) => eprintln!("Send failed: {error}"),
    }

    Ok(())
}
```

The SDK removes the active token from API error data before it returns the error.

## Use the Team API

Create a separate client with a Team API token. Store the token in `LETTERMINT_TEAM_TOKEN`:

```rust
use lettermint::Lettermint;

#[tokio::main]
async fn main() -> lettermint::Result<()> {
    let api = Lettermint::api(std::env::var("LETTERMINT_TEAM_TOKEN").unwrap())?;
    let domains = api.domains().list(&[("page[size]", "10")]).await?;

    for domain in domains.data {
        println!("{}", domain.domain);
    }

    Ok(())
}
```

The email client sends the Project API token in `X-Lettermint-Token`. The Team API client uses bearer authentication. Do not use a Project API token for Team API operations.

## Verify webhook signatures

Pass the raw request body and the exact Lettermint signature header to the verifier:

```rust
use lettermint::Webhook;

fn verify_webhook(
    payload: &str,
    signature: &str,
    delivery_timestamp: i64,
) -> lettermint::Result<()> {
    Webhook::new(std::env::var("LETTERMINT_WEBHOOK_SECRET").unwrap())
        .verify(payload, signature, Some(delivery_timestamp))?;

    Ok(())
}
```

The verifier checks the HMAC-SHA256 signature, the signature timestamp, and the optional delivery timestamp. The default timestamp tolerance is five minutes. Do not change the request body before verification. See [Webhook signing](/platform/webhooks/signing) for the required headers.

## Next steps

<CardGroup cols={2}>
    <Card title="Crate API reference" icon="book" href="https://docs.rs/lettermint">
        Review all public Rust types and methods.
    </Card>
    <Card title="Team API quickstart" icon="users" href="/platform/teams/team-api/quickstart">
        Manage domains, projects, routes, and team settings.
    </Card>
    <Card title="Email activity" icon="chart-line" href="/platform/emails/activity">
        Review message status and delivery events.
    </Card>
    <Card title="GitHub repository" icon="github" href="https://github.com/lettermint/lettermint-rust">
        View the source, report issues, or contribute.
    </Card>
</CardGroup>
