Bento Rust Guide

Install the crate via Cargo, configure `bento::Client` with your Site UUID plus keys, and call the async helpers inside Axum, Actix, or any Tokio runtime.

Bring Bento’s event, subscriber, and transactional email APIs into any async Rust service using the official crate.

Getting Started

Step 1

Install the Bento Rust SDK

Install the SDK with your framework’s package manager and keep it alongside your other HTTP clients so configuration, logging, and credentials stay centralized.

Using Cargo

toml
[dependencies]
bento = "0.1.0"

Step 2

Configure the client

Store your Site UUID, publishable key, and secret key in environment variables or a secrets manager. Register the Bento services during your app’s bootstrap so every controller, job, or command reuses the same client.

basic_setup

rust
use bento::{Client, Config, ConfigBuilder};
use std::time::Duration;
use tokio;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = ConfigBuilder::new()
        .publishable_key("YOUR_PUBLISHABLE_KEY")
        .secret_key("YOUR_SECRET_KEY")
        .site_uuid("YOUR_SITE_UUID")
        .timeout(Duration::from_secs(10)) // Optional, defaults to 30s
        .build()?;

    let client = Client::new(config)?;
    
    Ok(())
}

Beginner Guide

Tracking your first event

Events are the fastest way to add subscribers, kick off automations, and capture context in a single API call. Track onboarding milestones, purchases, or page views, then personalize with Liquid.

  • Pair every event with subscriber metadata so flows can branch without extra imports.
  • Include transactional context (cart items, amounts, IDs) for downstream personalization.
  • Prefer events over bespoke subscriber mutations—automations can add tags or update fields later.
Detail Liquid tag
Product details {{ event.details.product.size }}
Purchase amount {{ event.details.value.amount }}
Transaction ID {{ event.details.unique.key }}
Payment method {{ event.details.value.payment_method }}

Track a simple page view

rust
use bento::EventData;
use std::collections::HashMap;
use serde_json::json;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let mut details = HashMap::new();
    details.insert("url".to_string(), json!("/home"));
    details.insert("title".to_string(), json!("Home Page"));

    let event = EventData {
        event_type: "$pageView".to_string(),
        email: "user@example.com".to_string(),
        fields: None,
        details: Some(details),
    };

    let client = bento::Client::new();
    client.track_events(vec![event]).await?;

    Ok(())
}

Track a form submission

rust
use bento::{Client, EventData};
use std::collections::HashMap;
use serde_json::json;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut details = HashMap::new();
    details.insert("formName".to_string(), json!("Newsletter Signup"));
    details.insert("source".to_string(), json!("Homepage"));

    let event = EventData {
        event_type: "$formSubmitted".to_string(),
        email: "user@example.com".to_string(),
        fields: None,
        details: Some(details),
    };

    let client = Client::new("your-api-key-here");

    client.track_events(vec![event]).await?;

    Ok(())
}

Managing subscribers

You can still create, tag, or unsubscribe subscribers directly when you need to correct data or build admin tooling. Keep these calls reserved for operational work—events remain the preferred entry point.

Create a new subscriber

rust
let subscriber = client.create_subscriber("new@example.com").await?;

Tag a subscriber

rust
use bento::{CommandData, CommandType};

let command = CommandData {
    command: CommandType::AddTag,
    email: "user@example.com".to_string(),
    query: "Newsletter".to_string(),
};

client.subscriber_command(vec![command]).await?;

Unsubscribe A subscriber

rust
use bento::{CommandData, CommandType};

let command = CommandData {
    command: CommandType::Unsubscribe,
    email: "user@example.com".to_string(),
    query: "".to_string(), // No query needed for unsubscribe
};

client.subscriber_command(vec![command]).await?;

Common use cases

  • Track onboarding, purchase, and lifecycle events so automations fire with one API call.
  • Tag subscribers in response to events rather than separate API loops.
  • Store contextual data (cart contents, plan tier) for segmentation and personalization.

Tracking a user login "Event"

rust
use bento::EventData;
use std::collections::HashMap;

let mut details = HashMap::new();
details.insert("method".to_string(), serde_json::json!("password"));
details.insert("device".to_string(), serde_json::json!("mobile"));

let event = EventData {
    event_type: "$login".to_string(),
    email: "user@example.com".to_string(),
    fields: None,
    details: Some(details),
};

client.track_events(vec![event]).await?;

Updating user information

rust
use bento::EventData;
use std::collections::HashMap;

let mut details = HashMap::new();
details.insert("account".to_string(), serde_json::json!("active"));
details.insert("device".to_string(), serde_json::json!("mobile"));

let event = EventData {
    event_type: "$activation".to_string(),
    email: "user@example.com".to_string(),
    fields: None,
    details: Some(details),
};

client.track_events(vec![event]).await?;

Adding multiple tags to a subscriber

rust
use bento::ImportSubscriberData;
use std::collections::HashMap;

let subscriber = ImportSubscriberData {
    email: "user@example.com".to_string(),
    first_name: None,
    last_name: None,
    tags: Some("Premium,Annual Plan,Early Adopter".to_string()),
    remove_tags: None,
    custom_fields: HashMap::new(),
};

client.import_subscribers(vec![subscriber]).await?;

Intermediate Guide

Custom fields and tags

Combine fields for rich profile data with namespaced tags for segmentation. Tags keep audiences organized (`plan:pro`, `status:vip`) while fields store free-form values like locale or favorite product.

Namespaced tag ideas

subscription:basicsubscription:prosubscription:enterprise

Apply via automations whenever possible so marketing and data teams stay in sync.

Create a new custom field definition

rust
let field = client.create_field("membershipLevel").await?;

Get all existing fields

rust
let fields = client.get_fields().await?;

// Now you can iterate through the fields
for field in fields {
    println!("Field: {} (Key: {})", field.attributes.name, field.attributes.key);
}

Create a new tag

rust
let tag = client.create_tag("Power User").await?;

Get all existing tags

rust
let tags = client.get_tags().await?;

// Now you can iterate through the tags
for tag in tags {
    println!("Tag: {} (ID: {}, Created: {})",
        tag.attributes.name,
        tag.id,
        tag.attributes.created_at);
}

Tracking purchase events

Always include a `unique` key (order ID, cart token, etc.) so Bento dedupes purchases. Capture cart contents and totals to power LTV reporting and Liquid personalization.

Track a purchase event to monitor customer lifetime value

rust
use bento::EventData;
use std::collections::HashMap;

let mut details = HashMap::new();
details.insert("unique".to_string(), serde_json::json!({
    "key": "order-123" // Unique order identifier
}));
details.insert("value".to_string(), serde_json::json!({
    "amount": 9999, // Amount in cents
    "currency": "USD"
}));
details.insert("cart".to_string(), serde_json::json!({
    "items": [
        {
            "product_id": "prod-456",
            "product_name": "Premium Widget",
            "product_price": 9999,
            "quantity": 1,
            "product_sku": "SKU-456"
        }
    ]
}));

let event = EventData {
    event_type: "$purchase".to_string(),
    email: "customer@example.com".to_string(),
    fields: None,
    details: Some(details),
};

client.track_events(vec![event]).await?;

Namespaced tags

Reserve tags for segmentation keys (e.g., `product:analytics`). Use fields when you need arbitrary values or timestamps. This keeps your marketing taxonomy predictable.

Advanced Guide

Batch operations

Batch endpoints cover more than 80% of API work. Keep payloads lean, batch 200–300 records, and retry only failed chunks.

Import multiple subscribers at once

rust
use bento::ImportSubscriberData;
use std::collections::HashMap;

let mut fields1 = HashMap::new();
fields1.insert("company".to_string(), serde_json::json!("Acme Inc"));

let mut fields2 = HashMap::new();
fields2.insert("company".to_string(), serde_json::json!("Beta Corp"));

let subscribers = vec![
    ImportSubscriberData {
        email: "user1@example.com".to_string(),
        first_name: Some("Alice".to_string()),
        last_name: None,
        tags: None,
        remove_tags: None,
        custom_fields: fields1,
    },
    ImportSubscriberData {
        email: "user2@example.com".to_string(),
        first_name: Some("Bob".to_string()),
        last_name: None,
        tags: None,
        remove_tags: None,
        custom_fields: fields2,
    },
    // Can add up to 1,000 subscribers in a single batch
];

client.import_subscribers(subscribers).await?;

Import multiple events at once

rust
use bento::EventData;
use std::collections::HashMap;

let mut fields1 = HashMap::new();
fields1.insert("date".to_string(), serde_json::json!("2023-01-01"));

let mut details2 = HashMap::new();
details2.insert("unique".to_string(), serde_json::json!({
    "key": "order-123"
}));
details2.insert("value".to_string(), serde_json::json!({
    "currency": "USD",
    "amount": 9999
}));

let events = vec![
    EventData {
        event_type: "$login".to_string(),
        email: "user@example.com".to_string(),
        fields: Some(fields1),
        details: None,
    },
    EventData {
        event_type: "$purchase".to_string(),
        email: "user@example.com".to_string(),
        fields: None,
        details: Some(details2),
    },
    // Can add up to 1,000 events in a single batch
];

client.track_events(events).await?;

Transactional emails

Great candidates

  • Onboarding confirmation or welcome sequences
  • Password reset and login verification links
  • Payment, receipt, or fulfillment notices
  • Account or compliance notifications

Avoid using transactional for

  • CC / BCC workflows or multi-recipient fan-out
  • Attachments (link to files instead)
  • Marketing, promotional, or newsletter content
  • Bulk announcements where unsubscribes must be honored

Send a transactional email

rust
// Note: Email sending functionality is not yet implemented in the Rust SDK
// This example shows the expected structure for when it's available

use bento::{EmailData, EmailBatch};
use std::collections::HashMap;

let mut personalizations = HashMap::new();
personalizations.insert("link".to_string(), serde_json::json!("https://example.com/reset-password"));

let email = EmailData {
    to: "recipient@example.com".to_string(),
    from: "sender@example.com".to_string(),
    subject: "Reset Password".to_string(),
    html_body: "<p>Here is a link to reset your password ... {{ link }}</p>".to_string(),
    transactional: true,
    personalizations: Some(personalizations),
};

let batch = EmailBatch::new(vec![email])?;
// let results = client.send_emails(batch).await?; // Not yet implemented

// println!("Successfully queued {} emails for delivery", results);

Subscriber updates

Events remain the preferred way to create and update subscribers—they trigger flows and can mutate tags or fields downstream. Use direct endpoints when workflows demand determinism.

Events-first creation

Create a subscriber when they sign up

rust
use bento::EventData;
use std::collections::HashMap;

let mut fields = HashMap::new();
fields.insert("firstName".to_string(), serde_json::json!("Jane"));
fields.insert("lastName".to_string(), serde_json::json!("Doe"));
fields.insert("signupSource".to_string(), serde_json::json!("website"));

let event = EventData {
    event_type: "$subscribe".to_string(),
    email: "new-user@example.com".to_string(),
    fields: Some(fields),
    details: None,
};

client.track_events(vec![event]).await?;

Create a subscriber when they make a purchase

rust
use bento::EventData;
use std::collections::HashMap;

let mut details = HashMap::new();
details.insert("unique".to_string(), serde_json::json!({
    "key": "order-123"
}));
details.insert("value".to_string(), serde_json::json!({
    "amount": 9999,
    "currency": "USD"
}));

let mut fields = HashMap::new();
fields.insert("firstName".to_string(), serde_json::json!("John"));
fields.insert("lastName".to_string(), serde_json::json!("Smith"));
fields.insert("customerType".to_string(), serde_json::json!("new"));

let event = EventData {
    event_type: "$purchase".to_string(),
    email: "customer@example.com".to_string(),
    fields: Some(fields),
    details: Some(details),
};

client.track_events(vec![event]).await?;

Direct creation options

Create a single subscriber (email only)

rust
let subscriber = client.create_subscriber("user@example.com").await?;

Import multiple subscribers

rust
use bento::ImportSubscriberData;
use std::collections::HashMap;
use chrono::Utc;

let mut fields1 = HashMap::new();
fields1.insert("membershipTier".to_string(), serde_json::json!("gold"));
fields1.insert("accountStatus".to_string(), serde_json::json!("active"));
fields1.insert("lastRenewalDate".to_string(), serde_json::json!(Utc::now().to_rfc3339()));

let mut fields2 = HashMap::new();
fields2.insert("membershipTier".to_string(), serde_json::json!("silver"));
fields2.insert("accountStatus".to_string(), serde_json::json!("pending"));
fields2.insert("trialEndsAt".to_string(), serde_json::json!(
    (Utc::now() + chrono::Duration::days(30)).to_rfc3339()
));

let subscribers = vec![
    ImportSubscriberData {
        email: "user1@example.com".to_string(),
        first_name: None,
        last_name: None,
        tags: None,
        remove_tags: None,
        custom_fields: fields1,
    },
    ImportSubscriberData {
        email: "user2@example.com".to_string(),
        first_name: None,
        last_name: None,
        tags: None,
        remove_tags: None,
        custom_fields: fields2,
    },
];

client.import_subscribers(subscribers).await?;

Event-driven profile updates

Update subscriber when they update their profile

rust
use bento::EventData;
use std::collections::HashMap;

let mut fields = HashMap::new();
fields.insert("subscriptionTier".to_string(), serde_json::json!("premium"));

let event = EventData {
    event_type: "$subscription_change".to_string(),
    email: "user@example.com".to_string(),
    fields: Some(fields),
    details: None,
};

client.track_events(vec![event]).await?;

Single attribute tweaks

Add or update a single field

rust
use bento::ImportSubscriberData;
use std::collections::HashMap;
use chrono::Utc;

let mut fields = HashMap::new();
fields.insert("membershipTier".to_string(), serde_json::json!("premium"));
fields.insert("accountStatus".to_string(), serde_json::json!("pending"));
fields.insert("trialEndsAt".to_string(), serde_json::json!(
    (Utc::now() + chrono::Duration::days(30)).to_rfc3339()
));

let subscriber = ImportSubscriberData {
    email: "user2@example.com".to_string(),
    first_name: Some("Jesse".to_string()),
    last_name: Some("Bento".to_string()),
    tags: Some("membership:premium".to_string()),
    remove_tags: Some("membership:silver".to_string()),
    custom_fields: fields,
};

client.import_subscribers(vec![subscriber]).await?;

Add a tag

rust
use bento::ImportSubscriberData;
use std::collections::HashMap;

let subscriber = ImportSubscriberData {
  email: "user2@example.com".to_string(),
    first_name: Some("Jesse".to_string()),
    last_name: Some("Bento".to_string()),
    tags: Some("example:tag".to_string()),
    remove_tags: None,
    custom_fields: HashMap::new(),
};

client.import_subscribers(vec![subscriber]).await?;

Remove a field

rust
use bento::{CommandData, CommandType};

let command = CommandData {
  command: CommandType::RemoveField,
    email: "user@example.com".to_string(),
    query: "temporaryStatus".to_string(),
};

client.subscriber_command(vec![command]).await?;

Batch updates

Update multiple subscribers

rust
use bento::ImportSubscriberData;
use std::collections::HashMap;
use chrono::Utc;

let mut fields1 = HashMap::new();
fields1.insert("membershipTier".to_string(), serde_json::json!("gold"));
fields1.insert("accountStatus".to_string(), serde_json::json!("active"));
fields1.insert("lastRenewalDate".to_string(), serde_json::json!(Utc::now().to_rfc3339()));

let mut fields2 = HashMap::new();
fields2.insert("membershipTier".to_string(), serde_json::json!("silver"));
fields2.insert("accountStatus".to_string(), serde_json::json!("pending"));
fields2.insert("trialEndsAt".to_string(), serde_json::json!(
  (Utc::now() + chrono::Duration::days(30)).to_rfc3339()
));

let subscribers = vec![
  ImportSubscriberData {
    email: "user1@example.com".to_string(),
    first_name: None,
    last_name: None,
    tags: None,
    remove_tags: None,
    custom_fields: fields1,
  },
  ImportSubscriberData {
    email: "user2@example.com".to_string(),
    first_name: None,
    last_name: None,
    tags: None,
    remove_tags: None,
    custom_fields: fields2,
  },
];

client.import_subscribers(subscribers).await?;

Specialized operations

Change a subscribers email address

rust
use bento::{CommandData, CommandType};

let command = CommandData {
  command: CommandType::ChangeEmail,
    email: "old@example.com".to_string(),
    query: "new@example.com".to_string(),
};

client.subscriber_command(vec![command]).await?;

Update all fields at once

rust
use bento::ImportSubscriberData;
use std::collections::HashMap;

let mut fields = HashMap::new();
fields.insert("address".to_string(), serde_json::json!({
  "street": "123 Main St",
  "city": "New York",
  "state": "NY",
  "zip": "10001"
}));
fields.insert("preferences".to_string(), serde_json::json!({
  "theme": "dark",
  "notifications": true
}));

let subscriber = ImportSubscriberData {
  email: "user@example.com".to_string(),
    first_name: Some("Updated".to_string()),
    last_name: Some("Name".to_string()),
    tags: None,
    remove_tags: None,
    custom_fields: fields,
};

client.import_subscribers(vec![subscriber]).await?;

Unsubscribe a user

rust
use bento::{CommandData, CommandType};

let command = CommandData {
  command: CommandType::Unsubscribe,
    email: "user@example.com".to_string(),
    query: "".to_string(), // No query needed for unsubscribe
};

client.subscriber_command(vec![command]).await?;

Utility features

Validate addresses, guess gender, geolocate IPs, or check blacklists without integrating third-party APIs. These helpers are rate-limited—cache responses when you can.

Validate Email

rust
use bento::experimental::ValidationData;

let data = ValidationData {
  email: "user@example.com".to_string(),
    name: Some("John Doe".to_string()),        // Optional
    user_agent: Some("Mozilla/5.0...".to_string()),  // Optional
    ip: Some("192.168.1.1".to_string()),     // Optional
};

let validation_result = client.validate_email(&data).await?;

println!("Email is valid: {}", validation_result.valid);

Guess Prediction (for personalization)

rust
let gender_result = client.get_gender("Alex").await?;

println!("Gender prediction: {:?}", gender_result);

Geolocate IP Address

rust
let location_result = client.geolocate_ip("208.67.222.222").await?;

println!("Location: {:?}", location_result);

Check Domain/IP blacklist

rust
use bento::experimental::BlacklistData;

let blacklist_data = BlacklistData {
  domain: Some("example.com".to_string()),
    ip: None,  // Or check an IP instead: domain: None, ip: Some("192.168.1.1".to_string())
};

let blacklist_result = client.get_blacklist_status(&blacklist_data).await?;

println!("Blacklist status: {:?}", blacklist_result);

API Reference

The Rust SDK mirrors Bento's REST resources—most services expose async methods for batch operations plus convenience helpers for validation.

Transactional & commands

  • Send transactional email

    client.send_emails(batch).await?

    Emails API
  • Execute subscriber commands

    client.subscriber_command(commands).await?

    Subscriber command
  • Fetch broadcasts/stats

    client.get_broadcasts().await?

    Broadcasts

Troubleshooting

Not Authorized

Verify publishable + secret keys and ensure the team member still has access.

Rate Limited

Implement exponential backoff for batch operations and honor Retry-After headers.

Network Errors

Confirm outbound traffic can reach app.bentonow.com and retry transient socket resets.

Payload Exceptions

Double-check Author emails and ensure payloads match the documented schema.

Debugging tips

  1. Bubble up `Result` errors and log the `request_id` from every failure so you can replay payloads quickly.
  2. Wrap API calls in `tokio::time::timeout` or cancellation tokens so long-running imports don’t clog executors.
  3. Batch 200–300 records per request and drive uploads through structured concurrency (FuturesUnordered) to keep retries isolated.

FAQ

Can I use this SDK in a frontend environment?

No. Keep the client on trusted servers (or serverless functions). Use the JavaScript SDK or build a signed proxy when you need browser tracking.

How do I handle rate limiting?

Retry 429s with exponential backoff—`tokio_retry` or `backoff` crates make it easy. Log the `Retry-After` header for observability.

rate_limiting

rust
use tokio_retry::{strategy::ExponentialBackoff, RetryIf};
use std::time::Duration;

let retry_strategy = ExponentialBackoff::from_millis(100)
  .max_delay(Duration::from_secs(5))
  .take(3);

RetryIf::spawn(
  retry_strategy,
  || client.track_events(events.clone()),
  |err: &Error| matches!(err, Error::RateLimit)
).await?;
What's the maximum batch size for importing subscribers or events?

The API accepts up to 1,000 records, but 200–300 per request keeps payloads fast and makes retries far less painful.

max_batch_size

rust
use bento::ImportSubscriberData;
use std::sync::Arc;
use tokio::task::JoinSet;

// Split subscribers into batches of 200
let batch_size = 200;
let batches: Vec<_> = all_subscribers
  .chunks(batch_size)
  .map(|chunk| chunk.to_vec())
.collect();

// Process batches with limited concurrency
let mut join_set = JoinSet::new();
let client = Arc::new(client);
let semaphore = Arc::new(tokio::sync::Semaphore::new(3)); // Limit to 3 concurrent requests

for batch in batches {
  let client = Arc::clone(&client);
  let permit = semaphore.clone().acquire_owned().await?;

  join_set.spawn(async move {
    let _permit = permit; // Hold permit until task completes
    client.import_subscribers(batch).await
  });
}

// Wait for all tasks to complete
while let Some(result) = join_set.join_next().await {
  match result {
    Ok(Ok(_)) => println!("Batch completed successfully"),
      Ok(Err(e)) => eprintln!("Batch failed: {}", e),
      Err(e) => eprintln!("Task failed: {}", e),
  }
}
How do I track anonymous users?

Every request currently requires an email. Capture identifiers early (checkout, waitlists, etc.) so you can enrich events with real addresses.

Which Rust versions are supported?

Rust 1.70 or newer with Tokio 1.x is recommended. Newer toolchains provide better TLS, perf, and async ergonomics.

How can I contribute to the SDK?

Open issues or PRs on github.com/bentonow/bento-rust-sdk and coordinate in the Bento Discord when you need guidance.

How do I handle async operations and cancellation?

All methods are async. Use `tokio::time::timeout` or cancellation tokens when you need strict SLAs, and propagate contexts through your tasks.

context_support

rust
use tokio::time::{timeout, Duration};

// Use timeout for operations
let result = timeout(Duration::from_secs(5),
  client.find_subscriber("example@test.com")
).await;

match result {
  Ok(Ok(subscriber)) => {
    // Handle successful result
  }
  Ok(Err(e)) => {
    // Handle API error
  }
  Err(_) => {
    // Handle timeout
    eprintln!("Operation timed out");
  }
}
Can I use the SDK with AWS Lambda or other serverless environments?

Yes. Reuse the client between invocations, keep payloads lean, and chunk imports so they respect execution timeouts.

serverless_support

rust
use bento::{Client, ConfigBuilder};
use std::sync::OnceLock;
use std::time::Duration;

static CLIENT: OnceLock<Client> = OnceLock::new();

fn get_client() -> &'static Client {
CLIENT.get_or_init(|| {
  let config = ConfigBuilder::new()
    .publishable_key(std::env::var("BENTO_PUBLISHABLE_KEY").expect("BENTO_PUBLISHABLE_KEY not set"))
.secret_key(std::env::var("BENTO_SECRET_KEY").expect("BENTO_SECRET_KEY not set"))
.site_uuid(std::env::var("BENTO_SITE_UUID").expect("BENTO_SITE_UUID not set"))
.timeout(Duration::from_secs(5))
  .build()
  .expect("Failed to build config");

Client::new(config).expect("Failed to create client")
})
}

// In your Lambda handler
async fn lambda_handler(event: LambdaEvent) -> Result<String, Box<dyn std::error::Error>> {
  let client = get_client();
  // Use client for Bento operations
  Ok("Success".to_string())
}

Contribute or debug further

The Rust SDK is open source. Report bugs, request features, or open pull requests at https://github.com/bentonow/bento-rust-sdk. Keep your local tooling on Rust 1.70+ (Tokio runtime) to match production builds.

Need the original Markdown? Open raw file