# Rust SDK

> Source: https://truto.one/docs/sdks/rust/

`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](/docs/api-reference/overview/introduction). It doesn't wrap [notification destinations](/docs/api-reference/admin/notification-destinations), [SSO connections](/docs/api-reference/admin/sso-connections), [SCIM](/docs/api-reference/admin/scim), or [group mappings](/docs/api-reference/admin/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](https://crates.io/crates/truto). API docs: [docs.rs/truto](https://docs.rs/truto). Source: [github.com/trutohq/truto-rust-sdk](https://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

```bash
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](/docs/guides/api-tokens/creating-api-tokens), then:

```rust
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 `Option`s, and every struct implements `Default` (see [Typed models](#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](#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](#errors)).
- **Base URL.** It defaults to `https://api.truto.one`. Call `.base_url(..)` on `Client::builder()` to use a different one.

### Client options

```rust
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

```rust
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](/docs/api-reference/overview/errors#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](/docs/api-reference/overview/errors#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:

```rust
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:

```rust
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()`:

```rust
let tenants = client.tenants().list().collect_all().await?;
let first = client.tenants().list().first().await?; // Option<TenantResponse>
```

Or fetch one page at a time:

```rust
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](/docs/api-reference/overview/introduction) for the models and resources available.

```rust
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.

```rust
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:

```rust
#[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.

```rust
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 `/`.

```rust
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](#mcp-servers).

```rust
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](/docs/api-reference/admin/integrated-accounts).

```rust
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](/docs/api-reference/admin/mcp).

```rust
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](/docs/api-reference/admin/link-token).

```rust
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](/docs/guides/link-sdk/connections-widget). Accessible via `client.session_tokens()`.

```rust
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](/docs/api-reference/admin/tenants).

```rust
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](/docs/api-reference/admin/integrations).

```rust
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](/docs/api-reference/admin/installed-integrations-environment-integrations).

| 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](/docs/api-reference/admin/unified-models).

| 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](/docs/api-reference/admin/installed-unified-models-environment-unified-models).

| 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](/docs/api-reference/admin/unified-model-mappings) |
| `environment_unified_model_resource_methods()` | `/environment-unified-model-resource-method` | [Environment unified model mappings](/docs/api-reference/admin/installed-unified-models-environment-unified-model-mappings) |

```rust
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](/docs/api-reference/admin/sync-jobs) |
| `sync_job_runs()` | `/sync-job-run` | | [Sync job runs](/docs/api-reference/admin/sync-job-runs) |
| `sync_job_cron_triggers()` | `/sync-job-cron-trigger` | `schedule(id)`: `POST /sync-job-cron-trigger/{id}/schedule` | [Sync job cron triggers](/docs/api-reference/admin/sync-job-cron-triggers) |
| `sync_job_templates()` | `/sync-job-template` | | [Sync job templates](/docs/api-reference/admin/sync-job-templates) |
| `sync_job_run_states()` | `/sync-job-run-state` | | [Sync job run state](/docs/api-reference/admin/sync-job-run-state) |

```rust
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](/docs/api-reference/admin/webhooks).

```rust
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](/docs/api-reference/admin/workflows), [Workflow runs](/docs/api-reference/admin/workflowruns).

```rust
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](/docs/api-reference/admin/datastores).

| 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](/docs/api-reference/admin/static-gates).

| Method | Endpoint |
|---|---|
| `list`, `get`, `create`, `update`, `delete` | `/static-gate`, `/static-gate/{id}` |

## Observability destinations

Accessible via `client.observability_destinations()`. [API reference](/docs/api-reference/admin/observability-destinations).

```rust
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](/docs/api-reference/admin/documentation).

| Method | Endpoint |
|---|---|
| `list`, `get`, `create`, `update`, `delete` | `/documentation`, `/documentation/{id}` |

## Batch requests

Accessible via `client.batch_requests()`. [API reference](/docs/api-reference/admin/batch-requests).

| Method | Endpoint |
|---|---|
| `create(&body)` | `POST /batch-request` |

## Analytics

Accessible via `client.analytics()`. [API reference](/docs/api-reference/admin/analytics). Pass query parameters with `.query(..)`.

```rust
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](/docs/api-reference/admin/environments) |
| `api_tokens()` | `list`, `get` | `/api-token` | [API tokens](/docs/api-reference/admin/api-tokens) |
| `categories()` | `list` | `/category` | [Categories](/docs/api-reference/admin/categories) |
| `logs()` | `list(log_type)` | `/log` | [Logs](/docs/api-reference/admin/logs) |

`logs().list(log_type)` takes the `log_type` the API requires: `unified_proxy_api`, `rapid_bridge`, `webhook`, `mcp`, or `sync_job_cron_trigger`.

```rust
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](/docs/guides/daemon/overview).

| Method on `Client` | Methods | Endpoint | Reference |
|---|---|---|---|
| `daemons()` | `list`, `get`, `create`, `update`, `delete` | `/daemon`, `/daemon/{id}` | [Daemons](/docs/api-reference/admin/daemons) |
| `daemon_jobs()` | `list`, `get`, `create`, `update`, `delete` | `/daemon-job`, `/daemon-job/{id}` | [Daemon jobs](/docs/api-reference/admin/daemon-jobs) |
| `daemon_job_runs()` | `list`, `get`, `create`, `update`, `delete` | `/daemon-job-run`, `/daemon-job-run/{id}` | [Daemon job runs](/docs/api-reference/admin/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](/docs/api-reference/admin/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()`.
