Rust SDK
truto is an async Rust client for the Truto API, built on tokio and reqwest. It covers the Unified, Proxy, and Custom APIs, MCP, and most of the Admin API in the Truto API Reference. It doesn't wrap notification destinations, SSO connections, SCIM, or group mappings yet, and it has no raw request method for calling them. Admin API responses and most request bodies are typed structs generated from Truto's OpenAPI spec.
Crate: crates.io/crates/truto. API docs: docs.rs/truto. Source: github.com/trutohq/truto-rust-sdk. The methods on this page are in version 0.1.0.
Requirements
Rust 1.88+ and a tokio runtime. TLS uses rustls, so there is no OpenSSL dependency.
Installation
cargo add truto
cargo add tokio --features macros,rt-multi-thread
cargo add serde_json
cargo add futures-util # for streaming a Pager
cargo add serde --features derive # for decoding into your own typesQuick start
Create an API token, then:
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), truto::Error> {
let client = truto::Client::new(std::env::var("TRUTO_API_TOKEN").unwrap());
// Installed integrations in the environment
let installed = client.environment_integrations().list().collect_all().await?;
for integration in &installed {
println!("{:?}", integration.id);
}
// Integrated accounts for a tenant
let accounts = client
.integrated_accounts()
.list()
.query(json!({ "tenant_id": "acme-1" }))
.collect_all()
.await?;
// A Unified API call
let Some(account) = accounts.first() else { return Ok(()) };
let account_id = account.id.clone().unwrap_or_default();
let contacts = client
.unified_api()
.list("crm", "contacts", &account_id)
.collect_all()
.await?;
for contact in &contacts {
println!("{:?}", contact.get("first_name"));
}
Ok(())
}How the client works
- Request builders. Every method returns a request builder. Chain options onto it (
.query(..),.timeout(..),.max_retries(..)), then.awaitit. Nothing is sent until you await. - Cheap to clone.
Clientholds anArcinside; clone it into tasks freely. - Resources. Each Admin API resource is a method on
Client, such asclient.tenants()orclient.sync_jobs(). Most exposelist,get,create,update(PATCH), anddelete. - Typed models. Admin API methods take and return generated structs from
truto::models, such asTenantCreateRequestandIntegratedAccountResponse. Optional fields areOptions, and every struct implementsDefault(see Typed models). - Records. Unified, Proxy, Custom, and MCP results depend on the integration, so they come back as
truto::Record(serde_json::Map<String, Value>). Decode one into your own type with.typed::<T>()(see Decode into your own types). - Query parameters.
.query(..)takes aserde_json::Valueobject, aserde_json::Map, or atruto::Query. Nested values are encoded the way the API expects (filter[status]=open,ids[0]=a), and repeated.querycalls merge. Admin list filters on related fields use a dotted key, such as"integration.name": "hubspot". - Errors. Every method returns
Result<T, truto::Error>(see Errors). - Base URL. It defaults to
https://api.truto.one. Call.base_url(..)onClient::builder()to use a different one.
Client options
use std::time::Duration;
let client = truto::Client::builder()
.token(std::env::var("TRUTO_API_TOKEN").unwrap())
.base_url("https://api.truto.one") // default
.timeout(Duration::from_secs(30)) // per attempt, default 30s
.max_retries(3) // default 3
.default_retry_after(Duration::from_secs(10)) // 429 without Retry-After
.on_retry(|n, status| eprintln!("retry {n} after {status:?}"))
.on_rate_limited(|after, n| eprintln!("rate limited, waiting {after}s (retry {n})"))
.build()?;Errors
use truto::Error;
match client.tenants().get("acme-1").await {
Ok(tenant) => println!("{tenant:?}"),
Err(e) if e.is_not_found() => println!("no such tenant"), // also is_unauthorized, is_rate_limited
Err(Error::Api(api)) => {
// status, status_text, parsed body (JSON or text), response headers
println!("{} {}: {:?}", api.status, api.status_text, api.body);
}
Err(Error::Timeout) => println!("timed out"),
Err(Error::Transport(e)) => println!("network: {e}"),
Err(Error::Decode { message, .. }) => println!("unexpected response: {message}"),
Err(e) => println!("{e}"),
}api.body is a truto::ErrorBody: Json(Value) for Truto's JSON error responses, so message and truto_error_insight are readable, or Text(String) otherwise. e.status() returns the status code of any error that has one.
Retries and timeouts
The Rust SDK retries failed requests for you. Keep the rate limits in mind: 100 requests per second per API token, and 50 requests per 10 seconds per integrated account.
- Retried statuses.
408,409,425,429,502,503, and504, plus any added with.retry_status_codes(..). Connection failures (could not connect) are retried too; a reset connection or a failed body read returnsError::Transportat once.409is retried by default, unlike the retry strategy advice. Use.no_retry()where a conflict is expected, such astenants().createwith an ID that may exist. - Waits. A
429waits for theRetry-Afterheader, or 10 seconds when it is missing. Other retryable statuses back off with full jitter: a random wait up to 500 ms, with the ceiling doubling each time, capped at 10 seconds. - Retries. Up to 3 by default.
- Timeout. 30 seconds per attempt by default. A timed-out attempt returns
Error::Timeoutand is not retried. Dropping the future cancels any wait.
Override any of these per request:
use std::time::Duration;
let account = client
.integrated_accounts()
.get("ia-id")
.timeout(Duration::from_secs(5))
.max_retries(5)
.retry_status_codes([500]) // in addition to the defaults
.header("x-request-tag", "nightly-sync")
.on_retry(|n, status| eprintln!("retry {n} ({status:?})"))
.await?;
let once = client.integrated_accounts().get("ia-id").no_retry().await?;Reuse a set of overrides with truto::RequestOptions and .options(opts).
Retry a create or other write only when sending it twice is harmless: a 502 or 504 can arrive after the provider has already applied the change. Call .no_retry() on writes that aren't safe to repeat.
Pagination
list methods return a lazy Pager<T> that follows next_cursor for you. Nothing is fetched until you read from it.
Stream it item by item:
use futures_util::TryStreamExt;
let mut runs = client
.sync_job_runs()
.list()
.query(json!({ "sync_job_id": "sync-job-id" }))
.into_stream();
while let Some(run) = runs.try_next().await? {
println!("{:?} {:?}", run.id, run.status);
}Collect every page with collect_all(), or take the first item with first():
let tenants = client.tenants().list().collect_all().await?;
let first = client.tenants().list().first().await?; // Option<TenantResponse>Or fetch one page at a time:
let mut pager = client.tenants().list().query(json!({ "limit": 50 }));
while let Some(page) = pager.next_page().await? {
println!("{} tenants, next cursor {:?}", page.items.len(), page.next_cursor);
}The pager copies your query and adds next_cursor for each following page. It stops when the server returns no cursor.
Unified API
Accessible via client.unified_api(). Each call takes the unified model, the resource, and the integrated account ID. See the Unified API reference for the models and resources available.
let ia = "766cc1ee-...";
let api = client.unified_api();
let contacts = api.list("crm", "contacts", ia).query(json!({ "limit": 50 })).collect_all().await?;
let contact = api.get("crm", "contacts", "123", ia).await?;
let created = api.create("crm", "contacts", ia, json!({ "first_name": "Ada" })).await?;
api.update("crm", "contacts", "123", ia, json!({ "first_name": "Grace" })).await?;
api.delete("crm", "contacts", "123", ia).await?;
// Custom methods: POST /unified/{model}/{resource}/{method}
let results = api
.custom_method("crm", "contacts", "search", ia)
.body(json!({ "query": "acme" }))
.await?;| Method | Endpoint |
|---|---|
list(model, resource, ia) |
GET /unified/{model}/{resource} |
get(model, resource, id, ia) |
GET /unified/{model}/{resource}/{id} |
create(model, resource, ia, body) |
POST /unified/{model}/{resource} |
update(model, resource, id, ia, body) |
PATCH /unified/{model}/{resource}/{id} |
delete(model, resource, id, ia) |
DELETE /unified/{model}/{resource}/{id} |
custom_method(model, resource, method, ia) |
POST /unified/{model}/{resource}/{method}; set the body with .body(..) |
meta(model, resource, integration_name, method) |
GET /unified/{model}/{resource}/{integration_name}/meta/{method} |
model_meta(model, integration_name) |
GET /unified/meta/{model}/{integration_name} |
meta returns an integration's mapping metadata (schema and supported parameters) for one unified method. model_meta returns the resources and methods of a unified model that an integration supports.
let meta = client.unified_api().meta("crm", "contacts", "hubspot", "list").await?;
let crm_on_hubspot = client.unified_api().model_meta("crm", "hubspot").await?;Decode into your own types
Call .typed::<T>() on any request or pager, or truto::decode on a record:
#[derive(serde::Deserialize)]
struct Contact {
id: String,
first_name: Option<String>,
}
let contacts: Vec<Contact> = client
.unified_api()
.list("crm", "contacts", ia)
.typed::<Contact>()
.collect_all()
.await?;
let record = client.unified_api().get("crm", "contacts", "123", ia).await?;
let contact: Contact = truto::decode(record)?;Proxy API
Accessible via client.proxy_api(). Calls go to the integration's own API through Truto, with resource names as defined on the integration.
let ia = "766cc1ee-...";
let api = client.proxy_api();
let tickets = api.list("tickets", ia).collect_all().await?;
let ticket = api.get("tickets", "123", ia).await?;
api.create("tickets", ia, json!({ "subject": "Help" })).await?;
api.update("tickets", "123", ia, json!({ "status": "closed" })).await?;
api.delete("tickets", "123", ia).await?;
api.custom_method("tickets", "merge", ia).body(json!({ "ids": ["1", "2"] })).await?;| Method | Endpoint |
|---|---|
list(resource, ia) |
GET /proxy/{resource} |
get(resource, id, ia) |
GET /proxy/{resource}/{id} |
create(resource, ia, body) |
POST /proxy/{resource} |
update(resource, id, ia, body) |
PATCH /proxy/{resource}/{id} |
delete(resource, id, ia) |
DELETE /proxy/{resource}/{id} |
custom_method(resource, method, ia) |
POST /proxy/{resource}/{method}; set the body with .body(..) |
Custom API
Accessible via client.custom_api(). Calls a custom endpoint defined on the integration, at /custom/{path}. The path may contain /.
let result = client.custom_api().get("my/custom/path", ia).await?;
client.custom_api().post("my/custom/path", ia, json!({ "key": "value" })).await?;
// Non-JSON responses, such as files: use .raw()
let report = client.custom_api().get("reports/export.csv", ia).raw().await?;
println!("{} bytes of {:?}", report.body.len(), report.content_type());| Method | Endpoint |
|---|---|
get(path, ia) |
GET /custom/{path} |
post(path, ia, body) |
POST /custom/{path} |
put(path, ia, body) |
PUT /custom/{path} |
patch(path, ia, body) |
PATCH /custom/{path} |
delete(path, ia) |
DELETE /custom/{path} |
MCP
Accessible via client.mcp(). Talks to an integrated account's MCP server, using the token from Create MCP server.
let mcp_token = "your-mcp-token";
client.mcp().initialize(mcp_token, "my-client", "1.0.0").await?;
let tools = client.mcp().list_tools(mcp_token).await?;
let result = client.mcp().call_tool(mcp_token, "list_tickets", json!({ "status": "open" })).await?;
// Any other JSON-RPC message
client.mcp().call(mcp_token, json!({ "jsonrpc": "2.0", "method": "tools/list", "id": 3 })).await?;| Method | Description |
|---|---|
initialize(token, client_name, client_version) |
Sends initialize |
list_tools(token) |
Sends tools/list |
call_tool(token, name, arguments) |
Sends tools/call |
call(token, payload) |
Sends a raw JSON-RPC payload to POST /mcp/{token} |
Integrated accounts
Accessible via client.integrated_accounts(). API reference.
let accounts = client
.integrated_accounts()
.list()
.query(json!({ "tenant_id": "acme-1" }))
.collect_all()
.await?;
let account = client.integrated_accounts().get("ia-id").await?;
client.integrated_accounts().validate("ia-id").await?;
client.integrated_accounts().bulk_delete(json!({ "tenant_id": "acme-1" })).await?;
let capabilities = client.integrated_accounts().capabilities("ia-id").await?;| Method | Endpoint |
|---|---|
list() |
GET /integrated-account |
get(id) |
GET /integrated-account/{id} |
create(&body) |
POST /integrated-account (import an account with existing credentials) |
update(id, &body) |
PATCH /integrated-account/{id} |
delete(id) |
DELETE /integrated-account/{id} |
update_credentials(id, &body) |
PATCH /integrated-account/{id}/credentials |
bulk_delete(body) |
POST /integrated-account/bulk-delete (by ids or tenant_id) |
create_token(id) |
POST /integrated-account/token |
refresh_credentials(id) |
POST /integrated-account/refresh-credentials |
refresh_mapping_cache(&body) |
POST /integrated-account/refresh-mapping-cache |
create_form_token(id) |
POST /integrated-account/form-token |
run_post_install_actions(id) |
POST /integrated-account/run-post-install-actions |
validate(id) |
POST /integrated-account/{id}/validate |
capabilities(id) |
GET /integrated-account/{id}/capabilities |
tools(id, methods, tags) |
GET /integrated-account/{id}/tools (deprecated; use capabilities) |
tools takes slices of methods and tags to filter on; pass &[] for every tool.
MCP servers
Each integrated account can have MCP servers. API reference.
use truto::models::{IntegratedAccountMcpCreateRequest, IntegratedAccountMcpCreateRequestConfig};
let server = client
.integrated_accounts()
.create_mcp_server(
"ia-id",
&IntegratedAccountMcpCreateRequest {
name: "Support agent".into(),
config: IntegratedAccountMcpCreateRequestConfig {
methods: Some(vec!["list".into(), "get".into()]),
..Default::default()
},
..Default::default()
},
)
.await?;
let servers = client.integrated_accounts().list_mcp_servers("ia-id").collect_all().await?;| Method | Endpoint |
|---|---|
list_mcp_servers(ia_id) (a pager) |
GET /integrated-account/{ia_id}/mcp |
create_mcp_server(ia_id, &body) |
POST /integrated-account/{ia_id}/mcp |
get_mcp_server(ia_id, id) |
GET /integrated-account/{ia_id}/mcp/{id} |
update_mcp_server(ia_id, id, &body) |
PATCH /integrated-account/{ia_id}/mcp/{id} |
delete_mcp_server(ia_id, id) |
DELETE /integrated-account/{ia_id}/mcp/{id} |
Link tokens
Accessible via client.link_tokens(). API reference.
use truto::models::LinkTokenRequest;
let token = client
.link_tokens()
.create(&LinkTokenRequest {
tenant_id: Some("acme-1".into()),
..Default::default()
})
.await?;
println!("{:?}", token.link_token);| Method | Endpoint |
|---|---|
create(&body) |
POST /link-token |
Session tokens
Short-lived tokens scoped to one tenant, used by the connections widget. Accessible via client.session_tokens().
use truto::models::SessionTokenRequest;
let session = client
.session_tokens()
.create(&SessionTokenRequest {
tenant_id: "acme-1".into(),
..Default::default()
})
.await?;| Method | Endpoint |
|---|---|
create(&body) |
POST /session-token |
revoke(&body) |
DELETE /session-token |
renew() |
POST /session-token/renew |
me() |
GET /session-token/me |
Tenants
Accessible via client.tenants(). API reference.
use truto::models::{TenantCreateRequest, TenantUpdateRequest};
client
.tenants()
.create(&TenantCreateRequest {
id: "acme-1".into(),
name: Some("Acme".into()),
..Default::default()
})
.await?;
client
.tenants()
.update("acme-1", &TenantUpdateRequest {
name: Some("Acme Inc".into()),
..Default::default()
})
.await?;| Method | Endpoint |
|---|---|
list, get, create, update, delete |
/tenant, /tenant/{id} |
bulk_create(&body) |
POST /tenant/bulk |
Integrations
Accessible via client.integrations(). API reference.
let integration = client.integrations().get("integration-id").await?;
let capabilities = client.integrations().capabilities("hubspot").await?; // ID or name
client.integrations().install("integration-id", true).await?;
client.integrations().uninstall("integration-id").await?;| Method | Endpoint |
|---|---|
list, get, create, update, delete |
/integration, /integration/{id} |
capabilities(id_or_name) |
GET /integration/{id}/capabilities |
tools(id_or_name) |
GET /integration/{id}/tools (deprecated; use capabilities) |
unified_apis(id_or_name) |
GET /integration/{id}/unified-apis (deprecated; use capabilities) |
install(integration_id, is_enabled) |
POST /environment-integration |
uninstall(integration_id) |
Finds the integration's environment integration and deletes it |
Environment integrations
Integrations installed in your environment. Accessible via client.environment_integrations(). API reference.
| Method | Endpoint |
|---|---|
list() |
GET /environment-integration |
get(id) |
GET /environment-integration/{id} |
create(&body) |
POST /environment-integration |
update(id, &body) |
PATCH /environment-integration/{id} |
delete(id) |
DELETE /environment-integration/{id} |
Unified models
Accessible via client.unified_models(). API reference.
| Method | Endpoint |
|---|---|
list, get, create, update, delete |
/unified-model, /unified-model/{id} |
install(unified_model_id) |
POST /environment-unified-model |
uninstall(unified_model_id) |
Finds the model's environment unified model and deletes it |
Environment unified models
Unified models installed in your environment. Accessible via client.environment_unified_models(). API reference.
| Method | Endpoint |
|---|---|
list, get, create, update, delete |
/environment-unified-model, /environment-unified-model/{id} |
Unified model mappings
How a unified model's resource methods map to an integration. Both resources expose list, get, create, update, and delete.
Method on Client |
Endpoint | Reference |
|---|---|---|
unified_model_resource_methods() |
/unified-model-resource-method |
Unified model mappings |
environment_unified_model_resource_methods() |
/environment-unified-model-resource-method |
Environment unified model mappings |
let overrides = client
.environment_unified_model_resource_methods()
.list()
.query(json!({ "environment_unified_model_id": "..." }))
.collect_all()
.await?;Sync jobs
Each resource below exposes list, get, create, update, and delete.
Method on Client |
Endpoint | Extra methods | Reference |
|---|---|---|---|
sync_jobs() |
/sync-job |
Sync jobs | |
sync_job_runs() |
/sync-job-run |
Sync job runs | |
sync_job_cron_triggers() |
/sync-job-cron-trigger |
schedule(id): POST /sync-job-cron-trigger/{id}/schedule |
Sync job cron triggers |
sync_job_templates() |
/sync-job-template |
Sync job templates | |
sync_job_run_states() |
/sync-job-run-state |
Sync job run state |
use truto::models::SyncJobRunCreateRequest;
let run = client
.sync_job_runs()
.create(&SyncJobRunCreateRequest {
sync_job_id: "sync-job-id".into(),
integrated_account_id: "766cc1ee-...".into(),
..Default::default()
})
.await?;
client.sync_job_runs().delete(run.id.as_deref().unwrap_or_default()).await?; // stops the runWebhooks
Accessible via client.webhooks(). API reference.
use truto::models::{WebhookCreateRequest, WebhookTestRequest};
let webhook = client
.webhooks()
.create(&WebhookCreateRequest {
target_url: "https://example.com/truto".into(),
..Default::default()
})
.await?;
client
.webhooks()
.test(&WebhookTestRequest {
id: webhook.id.clone().unwrap_or_default(),
..Default::default()
})
.await?;| Method | Endpoint |
|---|---|
list, get, create, update, delete |
/webhook, /webhook/{id} |
test(&body) |
POST /webhook/test |
Workflows
Accessible via client.workflows() and client.workflow_runs(). API reference: Workflows, Workflow runs.
let runs = client
.workflow_runs()
.list()
.query(json!({ "workflow_id": "workflow-id" }))
.collect_all()
.await?;
for run in &runs {
println!("{:?}", run.status);
}Method on Client |
Methods | Endpoint |
|---|---|---|
workflows() |
list, get, create, update, delete |
/workflow, /workflow/{id} |
workflow_runs() |
list, get, create, update, delete |
/workflow-run, /workflow-run/{id} |
Datastores
Accessible via client.datastores(). API reference.
| Method | Endpoint |
|---|---|
list, get, create, update, delete |
/datastore, /datastore/{id} |
test(id, method, body) |
POST /datastore/{id}/test/{method} |
Static gates
Accessible via client.static_gates(). API reference.
| Method | Endpoint |
|---|---|
list, get, create, update, delete |
/static-gate, /static-gate/{id} |
Observability destinations
Accessible via client.observability_destinations(). API reference.
use truto::models::ObservabilityDestinationTestRequest;
client
.observability_destinations()
.test("observability-destination-id", &ObservabilityDestinationTestRequest::default())
.await?;| Method | Endpoint |
|---|---|
list, get, create, update, delete |
/observability-destination, /observability-destination/{id} |
test(id, &body) |
POST /observability-destination/{id}/test |
Documentation
Accessible via client.documentation(). API reference.
| Method | Endpoint |
|---|---|
list, get, create, update, delete |
/documentation, /documentation/{id} |
Batch requests
Accessible via client.batch_requests(). API reference.
| Method | Endpoint |
|---|---|
create(&body) |
POST /batch-request |
Analytics
Accessible via client.analytics(). API reference. Pass query parameters with .query(..).
let dashboard = client.analytics().dashboard().query(json!({ "range": "7d" })).await?;
let series = client
.analytics()
.timeseries()
.query(json!({
"environment_id": "environment-id",
"metric": "api_calls",
"from": "2026-10-01",
"to": "2026-10-08",
}))
.await?;| Method | Endpoint |
|---|---|
status() |
GET /analytics/status |
metrics() |
GET /analytics/metrics |
dashboard() |
GET /analytics/dashboard |
timeseries() |
GET /analytics/timeseries |
breakdown() |
GET /analytics/breakdown |
Environments, API tokens, categories, and logs
Method on Client |
Methods | Endpoint | Reference |
|---|---|---|---|
environments() |
list, get, update |
/environment |
Environments |
api_tokens() |
list, get |
/api-token |
API tokens |
categories() |
list |
/category |
Categories |
logs() |
list(log_type) |
/log |
Logs |
logs().list(log_type) takes the log_type the API requires: unified_proxy_api, rapid_bridge, webhook, mcp, or sync_job_cron_trigger.
let logs = client.logs().list("unified_proxy_api").first().await?;client.ping() calls GET /ping to check that your token works.
Daemons
These use truto::Record instead of typed models. See the daemon guide.
Method on Client |
Methods | Endpoint | Reference |
|---|---|---|---|
daemons() |
list, get, create, update, delete |
/daemon, /daemon/{id} |
Daemons |
daemon_jobs() |
list, get, create, update, delete |
/daemon-job, /daemon-job/{id} |
Daemon jobs |
daemon_job_runs() |
list, get, create, update, delete |
/daemon-job-run, /daemon-job-run/{id} |
Daemon job runs |
daemon_job_cron_triggers() |
list, get, create, update, delete, schedule |
/daemon-job-cron-trigger, /daemon-job-cron-trigger/{id} |
Daemon job cron triggers |
Other resources
These resources are in the SDK but not in the public API reference. They use truto::Record instead of typed models.
Method on Client |
Methods | Endpoint |
|---|---|---|
users() |
list, get |
/user, /user/{id} |
teams() |
list, get, update |
/team, /team/{id} |
sandbox_integrated_accounts() |
list, get, create, delete |
/sandbox-integrated-account, /sandbox-integrated-account/{id} |
alarms() |
list, get, create, update, delete |
/alarm, /alarm/{id} |
integrated_account_webhooks() |
process(integrated_account_id, body) |
POST /integrated-account-webhook/{id} |
environment_integration_webhooks() |
process(environment_integration_id, body) |
POST /environment-integration-webhook/{id} |
Typed models
Admin API types in truto::models, such as IntegratedAccountResponse, SyncJobCreateRequest, and WebhookSchema, are generated from Truto's OpenAPI spec.
- Lenient decoding. Unknown fields are ignored and missing fields fall back to their defaults, so new API fields don't break decoding.
- Open enums. An enum value the SDK doesn't know yet becomes
Other(String)and serializes back unchanged. - Union types keep the raw JSON, with an
as_<variant>()accessor per variant. - Building requests. Every struct implements
Default, so set the fields you need and finish with..Default::default().