Skip to content

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 types

Quick 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 .await it. Nothing is sent until you await.
  • Cheap to clone. Client holds an Arc inside; clone it into tasks freely.
  • Resources. Each Admin API resource is a method on Client, such as client.tenants() or client.sync_jobs(). Most expose list, get, create, update (PATCH), and delete.
  • Typed models. Admin API methods take and return generated structs from truto::models, such as TenantCreateRequest and IntegratedAccountResponse. Optional fields are Options, and every struct implements Default (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 a serde_json::Value object, a serde_json::Map, or a truto::Query. Nested values are encoded the way the API expects (filter[status]=open, ids[0]=a), and repeated .query calls 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(..) on Client::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, and 504, plus any added with .retry_status_codes(..). Connection failures (could not connect) are retried too; a reset connection or a failed body read returns Error::Transport at once. 409 is retried by default, unlike the retry strategy advice. Use .no_retry() where a conflict is expected, such as tenants().create with an ID that may exist.
  • Waits. A 429 waits for the Retry-After header, 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::Timeout and 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}

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 run

Webhooks

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().