Official Rust client for the Email Funnel AI integration API. Connect with your project key and secret, then call typed, resource-oriented methods — no manual URLs, headers, or JSON plumbing.
- Async, built on
reqwest+tokio rustlsTLS by default — no system OpenSSL required- Typed error model (
EmailFunnelError) with status,error_type, and 422 field messages - Injectable transport for custom clients, logging, or offline tests
- Full coverage of all 26 endpoints
# Cargo.toml
[dependencies]
emailfunnelai = "1"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
serde_json = "1"use emailfunnelai::EmailFunnelAi;
use serde_json::json;
#[tokio::main]
async fn main() -> emailfunnelai::Result<()> {
// base_url defaults to https://app.emailfunnel.ai — override only for staging/local.
let client = EmailFunnelAi::new("pk_your_project_key", "sk_your_secret_key");
// 1. Confirm your credentials
client.validate().await?;
// 2. Get or create a list
let list = client.lists().create("Newsletter signups", None).await?;
let list_id = list["id"].as_str().unwrap_or("").to_string();
// 3. Sync a contact into it
client
.contacts()
.sync(json!({ "email": "jane@example.com", "source_type": "custom_saas" }))
.to_list(&list_id)
.await?;
Ok(())
}Every call returns the unwrapped data payload as a [serde_json::Value], so
you can read fields directly or serde_json::from_value into your own structs.
Every group is reached from the client and reads as resource → verb.
use serde_json::json;
// Single contact
client
.contacts()
.sync(json!({ "email": "jane@example.com", "source_type": "app" }))
.to_list("42")
.await?;
// Bulk (up to 500; the server queues automatically above 100 rows)
client
.contacts()
.bulk(vec![json!({ "email": "a@example.com" })], "import")
.to_list("42")
.await?;
// Force queued processing
client
.contacts()
.bulk(rows, "import")
.queued()
.to_list("42")
.await?;
// Real-time webhook event
client
.contacts()
.webhook("order.completed", json!({ "email": "a@example.com" }))
.to_list("42")
.await?;client.lists().all().await?;
client.lists().create("VIP customers", Some("High-value buyers")).await?;
client.lists().find("42").await?;client.bindings().all().await?;
client.bindings().create("custom_crm", "42", None).await?;
client.bindings().find("5").await?;
client.bindings().update("5", json!({ "sync_enabled": false })).await?;
client.bindings().delete("5").await?;
client.bindings().status("5", "completed", Some(0)).await?;client.field_mappings().config("custom_saas").await?;
client.field_mappings().for_binding("5").get().await?;
client.field_mappings().for_binding("5").update(json!({ "email": "Email" })).await?;
client.field_mappings().for_binding("5").reset().await?;client.auto_tagging().rules("custom_saas").await?;
client.auto_tagging().preview("custom_saas", json!({ "plan": "pro" })).await?;use emailfunnelai::HeatmapOptions;
client.analytics().dashboard().await?;
client
.analytics()
.heatmap(HeatmapOptions::new().range(30).email_type("campaign").timezone("America/New_York"))
.await?;
client.analytics().funnels().await?;
client.analytics().campaigns().await?;
client.analytics().forms().await?;client.sso().generate("admin@example.com").await?;
client.sso().team_members().await?;Successful calls return the unwrapped data payload. Any error response yields
an [EmailFunnelError]:
use emailfunnelai::EmailFunnelError;
match client.contacts().sync(contact).to_list(list_id).await {
Ok(data) => { /* … */ }
Err(EmailFunnelError::Api(api)) => {
api.status; // 422
api.error_type; // "validation_error"
api.messages; // Option<HashMap<String, Vec<String>>> — set on 422
api.retry_after; // Option<u64> — set on 429 rate limits
}
Err(EmailFunnelError::Transport(message)) => { /* network / timeout */ }
Err(EmailFunnelError::Decode(message)) => { /* malformed response body */ }
}Branch on error_type for specific conditions — e.g. a contact list that has
been deactivated rejects new members with a 409 list_inactive:
if let Err(EmailFunnelError::Api(api)) = client.contacts().sync(contact).to_list(list_id).await {
if api.error_type == "list_inactive" {
// The target list is inactive — reactivate it or pick another list.
}
}error_type |
Status | Meaning |
|---|---|---|
invalid_credentials |
401 | Missing/invalid project key or secret |
inactive_project |
403 | The connected project is inactive |
validation_error |
422 | Request body failed validation (messages set) |
invalid_email / suppressed |
422 | Email is undeliverable or suppressed |
list_inactive |
409 | Target contact list is inactive and rejects new members |
rate_limit_exceeded |
429 | 1000 req/hour cap hit (retry_after set) |
sync_failed |
500 | Unexpected sync failure |
Use the builder to override the base URL or inject a transport:
use std::sync::Arc;
use emailfunnelai::{EmailFunnelAi, ReqwestHttpClient};
// Point at a local dev server
let client = EmailFunnelAi::builder("pk", "sk")
.base_url("http://localhost:8000")
.build();
// Wrap a pre-configured reqwest client (timeouts, proxies, pools)
let reqwest_client = reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(30))
.build()
.unwrap();
let client = EmailFunnelAi::builder("pk", "sk")
.http_client(Arc::new(ReqwestHttpClient::with_client(reqwest_client)))
.build();Implement the HttpClient trait to stub HTTP entirely (used by this crate's own
test suite) for fast, offline tests.
cargo testMIT © Email Funnel AI. Authored by Email Funnel AI and Ratul Hasan.