Rust SDK
Connect maxclicks to your Rust service with explicit request results and errors. The client covers customer data, events, prepared campaigns, and workflow triggers.
Minimum supported Rust version is 1.70.
The crates.io release is pending. Depend on the source repository below, or follow the HTTP quickstart to connect without a client library.
Current API coverage
This guide documents the existing source client interface. The current REST API also exposes event readiness, event identity changes, record deletion status, and workflow history cursors that this client does not yet wrap. Event receipts and deletion results have changed; older static models may omit fields or expect the former { id, deleted } shape.
Use direct HTTP for those operations and verify the current coverage table before adopting a wrapper. Auto-pagination still stops at the API's traversal cap; an iterator or method named all does not guarantee a full workspace export.
Install
The crates.io release is not published yet. Until it is, depend on the source repository:
[dependencies]
maxclicks = { git = "https://github.com/maxclicks-ai/maxclicks-rust" }
Quickstart
Construct a Client with your API key, then call resource groups. Records and event inputs are flat and accept any Serialize value, including serde_json::json!.
use maxclicks::Client;
use serde_json::json;
fn main() -> Result<(), maxclicks::MaxclicksError> {
let client = Client::new("max_...")?;
// Identify the calling key.
let me = client.me()?;
println!("space: {:?}", me.space);
// Upsert a contact by identity. Arbitrary custom keys are preserved.
let contact = client.records().upsert(
"students",
&json!({ "email": "[email protected]", "firstName": "Ada", "plan": "pro" }),
None, // optional Idempotency-Key
)?;
println!("contact id: {}", contact.id);
// Walk every record across all pages.
for record in client.records().list_all("students") {
let record = record?;
println!("{}", record.id);
}
Ok(())
}
Client
Construct a client three ways.
| Constructor | Behavior |
|---|---|
Client::new("max_...") | Bare API key. |
Client::from_env() | Reads MAXCLICKS_API_KEY. |
Client::builder() | Full control over transport and headers. |
use maxclicks::Client;
use std::time::Duration;
let client = Client::builder()
.api_key("max_...")
.base_url("https://api.maxclicks.ai/v1")
.timeout(Duration::from_secs(30))
.max_retries(3)
.default_header("X-My-Trace", "abc")
.on_warning(|warnings, context| {
eprintln!("{} {}: {:?}", context.method, context.path, warnings);
})
.build()?;
- Name
api_key- Type
- string
- Description
Sent as
Authorization: Bearer <key>. Falls back toMAXCLICKS_API_KEY.
- Name
base_url- Type
- string
- Description
Defaults to
https://api.maxclicks.ai/v1. Trailing slashes are trimmed.
- Name
timeout- Type
- Duration
- Description
Per request. Default 60s.
- Name
max_retries- Type
- u32
- Description
Extra attempts on retryable failures. Default 2.
- Name
on_warning- Type
- callback
- Description
Invoked with API warnings per call.
- Name
transport- Type
- Arc<dyn Transport>
- Description
Defaults to blocking
reqwest. Inject a mock to test without a network.
Errors
Every call returns Result<T, MaxclicksError>. Match a variant, or use the uniform accessors error.status(), error.code(), and error.message().
use maxclicks::MaxclicksError;
match client.schemas().get("missing") {
Ok(schema) => println!("{}", schema.name),
Err(MaxclicksError::NotFound(data)) => eprintln!("no such schema: {}", data.message),
Err(MaxclicksError::RateLimit { retry_after, .. }) => {
eprintln!("slow down; retry after {:?}", retry_after);
}
Err(error) => eprintln!(
"status={:?} code={:?} message={}",
error.status(),
error.code(),
error.message(),
),
}
| Variant | Status | Notes |
|---|---|---|
BadRequest | 400 | |
Authentication | 401 | |
PaymentRequired | 402 | insufficient_credits on an AI or auto-fill call, email_limit_reached on a send. |
Permission | 403 | |
NotFound | 404 | |
Conflict | 409 | |
UnprocessableEntity | 422 | |
RateLimit | 429 | Carries retry_after. |
Server | 5xx | |
Api | other | 405/413/415 or an undecodable body. |
Connection | Transport failure. | |
Timeout | Request timed out. | |
Config | Programmer error such as a missing key, raised before any request. |
Retries and rate limits
Reads are retried on 429, any 5xx, and transport or timeout errors, up to max_retries extra attempts. A write is retried on those only when it carries an idempotency key; without one it is retried on 429 alone, because a 5xx or a timeout leaves the outcome unknown and a repeat could send twice. Backoff is exponential with full jitter, base 500ms, cap 8s. A Retry-After header (delay-seconds or HTTP date) sets the delay floor. Per-key limits are 100 req/s for reads and 25 req/s for writes.
Pagination
List endpoints return a Page<T> with data, pagination, and warnings. Every list method except workflows().runs has a *_all(...) variant returning a Paginator<T> that walks every page. It stops when has_more is false or at the offset cap of 10,000. Page workflows().runs yourself with its limit and offset arguments.
events().list_all(...) is the cursor-paginated exception. It returns a CursorPaginator<Event> driven by the opaque next_cursor rather than an offset, and takes the same from and to window as events().list.
// One page.
let page = client.records().list("students", Some(100), Some(0))?;
println!("{} of {}", page.data.len(), page.pagination.total_count);
// Every item, auto-fetching pages.
for record in client.records().list_all("students") {
let record = record?;
// ...
}
// Or collect all at once. Short-circuits on the first error.
let all = client.records().list_all("students").collect_all()?;
Idempotency
Seven methods in this source client take a trailing Option<&str> Idempotency-Key: records().create, records().upsert, suppressions().create_batch, suppressions().remove_batch, templates().send, broadcasts().send, and workflows().trigger. Pass None to skip. Other methods do not expose a key argument. Use direct HTTP for the additional supported API writes; do not assume every update or deletion is safe to replay.
client.records().upsert(
"students",
&json!({ "email": "[email protected]", "firstName": "Ada" }),
Some("signup-2026-07-18-ada"),
)?;
Warnings
The API can attach warnings in the response body under warnings. For the empty-data workflows.trigger response, warnings arrive in the URL-encoded maxclicks-Warning-Message response header. Both surface on Page.warnings and through the on_warning callback.
Resource surface
Each group is a method on Client. Every list method also has an auto-paginating *_all(...) variant, with the single exception of workflows().runs.
| Group | Methods |
|---|---|
| root | me |
schemas() | list, get |
attributes() | list |
records() | create, upsert, list, get, update, delete, audit_trail |
suppressions() | create, create_batch, remove_batch, list, delete |
events() | fire, fire_batch, list |
domains() | list, get |
senders() | list |
topics() | list, get |
segments() | create, list, get, delete, count, contacts |
broadcasts() | list, get, update, send, runs, metrics |
templates() | list, get, send |
webhooks() | create, list, get, update, delete, rotate_secret |
workflows() | list, get, pause, unpause, runs, get_run, trigger |
forms() | submit, confirm_double_opt_in |
emails() | unsubscribe_one_click |
Create schemas, attributes, domains, senders, topics, templates and broadcasts in the maxclicks app, then use the supported API operations to read them or act on prepared work. Manage API keys and CSV imports in the app too. The client's method list and the current API reference define what can be changed or triggered from code; use direct HTTP when a newer operation is not wrapped by this client.
forms.submit, forms.confirm_double_opt_in, and
emails.unsubscribe_one_click are unauthenticated. AI-backed calls
(segments().create and webhooks() writes carrying a custom-filter
condition) return PaymentRequired when the space is out of credits.
Retry outcomes
A repeated write can return a recorded result, including an error. If the API reports idempotency_outcome_unknown, inspect the affected resource or run history before creating a new operation. Keep the original key and payload while you reconcile. See the idempotency guide for supported operations and retention.