---
title: Introducing the Truto CLI
slug: introducing-truto-cli
date: 2026-04-15
author: Roopendra Talekar
categories: [Product Updates, Engineering]
excerpt: "Manage integrations, query unified APIs, and configure per-account schema mappings from the terminal - with pass-through data flow and no persisted response payloads."
tldr: "The Truto CLI gives you full control over integrations, unified APIs, and per-account custom mappings - pass-through by default, no persisted response bodies, encrypted OAuth credentials."
canonical: https://truto.one/blog/introducing-truto-cli/
---

# Introducing the Truto CLI


Dashboards are for overview. Terminals are for doing. If you're an engineer working with Truto, you shouldn't have to leave your terminal to manage integrations, query data, or debug a failing sync job.

The Truto CLI is a standalone binary that puts the entire Truto platform at your fingertips - unified APIs, proxy APIs, admin resources, bulk exports, batch operations, and record diffs. One `curl` to install, one command to authenticate, and you're running.

<link rel="stylesheet" href="https://esm.sh/terminal-demo@0.1.11/dist/style.css">
<script type="importmap">{"imports":{"terminal-demo":"https://esm.sh/terminal-demo@0.1.11"}}</script>
<div id="cli-blog-demo" style="margin: 2rem 0; border-radius: 8px; overflow: hidden;"></div>
<script type="module">
import { TerminalDemo } from 'terminal-demo'
var fast = window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 100 : 1.2
new TerminalDemo(document.getElementById('cli-blog-demo'), {
  title: 'truto — zsh',
  promptText: '~',
  promptSymbol: '$',
  autoPlay: true,
  loop: true,
  speed: fast,
  theme: {
    background: '#1a1a1a',
    foreground: '#f9f8f6',
    headerBackground: '#252525',
    cursor: '#fc5b37',
    prompt: '#8a8a8a',
    buttonClose: '#ff5f57',
    buttonMinimize: '#febc2e',
    buttonMaximize: '#28c840',
    gray: '#8a8a8a',
    green: '#28c840',
    cyan: '#7dcfff',
    yellow: '#febc2e',
    red: '#ff5f57',
    purple: '#bb9af7',
    white: '#f9f8f6'
  },
  scenarios: [
    {
      name: 'install',
      steps: [
        { type: 'prompt' },
        { type: 'command', text: 'curl -fsSL https://cli.truto.one/install.sh | bash', delay: 30 },
        { type: 'output', text: ' [gray]Detected: darwin arm64 [/gray]' },
        { type: 'spinner', text: 'Downloading truto v0.2.1...', duration: 1200 },
        { type: 'output', text: ' [green]\u2713 Installed to ~/.truto/bin/truto [/green]' },
        { type: 'wait', ms: 1200 }
      ]
    },
    {
      name: 'login',
      steps: [
        { type: 'prompt' },
        { type: 'command', text: 'truto login', delay: 50 },
        { type: 'output', text: ' [cyan]\u25c6  Profile name [/cyan]' },
        { type: 'output', text: ' [gray]\u2502 [/gray]  default' },
        { type: 'wait', ms: 400 },
        { type: 'output', text: ' [cyan]\u25c6  API base URL [/cyan]' },
        { type: 'output', text: ' [gray]\u2502 [/gray]  https://api.truto.one' },
        { type: 'wait', ms: 400 },
        { type: 'output', text: ' [cyan]\u25c6  API token [/cyan]' },
        { type: 'output', text: ' [gray]\u2502 [/gray]  \u25aa\u25aa\u25aa\u25aa\u25aa\u25aa\u25aa\u25aa\u25aa\u25aa\u25aa\u25aa\u25aa\u25aa\u25aa\u25aa\u25aa\u25aa\u25aa\u25aa' },
        { type: 'wait', ms: 300 },
        { type: 'spinner', text: 'Verifying credentials', duration: 900 },
        { type: 'output', text: ' [green]\u25c7  Credentials verified [/green]' },
        { type: 'output', text: ' [green]\u2713 Logged in as profile "default" at https://api.truto.one [/green]' },
        { type: 'wait', ms: 1200 }
      ]
    },
    {
      name: 'query',
      steps: [
        { type: 'prompt' },
        { type: 'command', text: 'truto unified crm contacts -a acme-sf -o table', delay: 35 },
        { type: 'spinner', text: 'Fetching contacts...', duration: 1000 },
        { type: 'output', text: '\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510' },
        { type: 'output', text: '\u2502 name         \u2502 email               \u2502 company    \u2502' },
        { type: 'output', text: '\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524' },
        { type: 'output', text: '\u2502 Jane Cooper  \u2502 jane@acme.co        \u2502 Acme Corp  \u2502' },
        { type: 'output', text: '\u2502 Alex Rivera  \u2502 alex.r@globex.io    \u2502 Globex     \u2502' },
        { type: 'output', text: '\u2502 Sam Chen     \u2502 schen@initech.com   \u2502 Initech    \u2502' },
        { type: 'output', text: '\u2502 Maya Wong    \u2502 mwong@soylent.co    \u2502 Soylent    \u2502' },
        { type: 'output', text: '\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518' },
        { type: 'wait', ms: 1500 }
      ]
    },
    {
      name: 'export',
      steps: [
        { type: 'prompt' },
        { type: 'command', text: "truto export crm/contacts -a acme-sf -o ndjson | jq '.email'", delay: 30 },
        { type: 'spinner', text: 'Exporting...', duration: 800 },
        { type: 'output', text: ' [cyan]"jane@acme.co" [/cyan]' },
        { type: 'output', text: ' [cyan]"alex.r@globex.io" [/cyan]' },
        { type: 'output', text: ' [cyan]"schen@initech.com" [/cyan]' },
        { type: 'output', text: ' [cyan]"mwong@soylent.co" [/cyan]' },
        { type: 'output', text: ' [cyan]"priya.k@umbrella.io" [/cyan]' },
        { type: 'output', text: ' [gray]Exported 247 records [/gray]' },
        { type: 'wait', ms: 1500 }
      ]
    }
  ]
})
</script>

## Install in one command

No package managers. No dependencies. A single `curl` downloads a standalone binary for your platform:

```bash
curl -fsSL https://cli.truto.one/install.sh | bash
```

The installer detects your OS and architecture (macOS, Linux, x64, arm64), downloads the correct binary, and drops it into `~/.truto/bin/truto`. That's it.

Need a specific version or a custom install path?

```bash
TRUTO_VERSION=0.1.0 curl -fsSL https://cli.truto.one/install.sh | bash
TRUTO_INSTALL_DIR=/usr/local/bin curl -fsSL https://cli.truto.one/install.sh | bash
```

The CLI is self-updating. Run `truto upgrade` to pull the latest version and replace the binary in-place. Run `truto upgrade --check` to see if there's a new version without installing it.

## Authenticate

Run `truto login` and you get an interactive setup powered by [clack](https://github.com/bombshell-dev/clack) — profile name, API URL, and token (masked input). The token is verified against the API before saving.

```bash
truto login
```

For CI/CD or scripted usage, skip the prompts entirely:

```bash
truto login --token <your-api-token>
truto login --token <token> --profile-name staging --api-url https://your-instance.truto.one
```

Verify your credentials:

```bash
$ truto whoami
┌─────────┬───────────────────────┬──────────────────┐
│ profile │ api_url               │ team_name        │
├─────────┼───────────────────────┼──────────────────┤
│ default │ https://api.truto.one │ Acme Engineering │
└─────────┴───────────────────────┴──────────────────┘
```

The CLI supports multiple profiles — maintain separate credentials for staging, production, and different teams. Switch between them with `truto profiles use <name>`.

## Query any integration

The CLI covers every Truto data-plane API — unified, proxy, and custom.

**Unified API** gives you normalized data across integrations. Same field names whether you're querying Salesforce, HubSpot, or Pipedrive:

```bash
truto unified crm contacts -a <account-id> -o table
truto unified crm contacts <contact-id> -m get -a <account-id>
truto unified crm contacts -m create -a <account-id> -b '{"first_name":"Jane","last_name":"Doe"}'
```

**Proxy API** gives you the raw, integration-specific data without any schema normalization:

```bash
truto proxy tickets -a <account-id>
truto proxy tickets T-42 -m get -a <account-id>
```

**Custom API** lets you call your own endpoints built on Truto:

```bash
truto custom /my-endpoint -a <account-id>
truto custom /my-endpoint -m POST -a <account-id> -b '{"key":"value"}'
```

### Five output formats

Every command supports five output formats via `-o`:

| Format   | Best for                              |
| -------- | ------------------------------------- |
| `table`  | Interactive use, quick inspection     |
| `json`   | Piping to `jq`, saving to files      |
| `ndjson` | Streaming, log processing, piping    |
| `csv`    | Spreadsheets, data analysis           |
| `yaml`   | Config files, human-readable output  |

## Power features

Beyond querying, the CLI ships three power tools that make it genuinely useful for day-to-day workflows.

### Export

Bulk-export any resource with automatic pagination. The CLI handles cursors and streams results in your chosen format:

```bash
truto export crm/contacts -a <account-id> -o ndjson --out contacts.ndjson
truto export crm/contacts -a <account-id> -o csv --out contacts.csv
```

NDJSON and CSV stream page-by-page as data arrives — safe for large datasets. Pipe directly to other tools:

```bash
truto export crm/contacts -a <account-id> -o ndjson | jq '.email'
truto export crm/contacts -a <account-id> -o ndjson | wc -l
```

A slash in the resource name means unified API (`crm/contacts`). No slash means proxy (`tickets`).

### Batch

Execute multiple API requests from a JSON file in a single command:

```bash
truto batch requests.json -a <account-id>
```

Mix creates, updates, and deletes. Each request runs in parallel with individual status reporting:

```
✓ POST crm/contacts → 201 (42ms)
✓ PATCH crm/contacts/c_17 → 200 (51ms)
✓ DELETE crm/contacts/c_99 → 204 (29ms)
3/3 succeeded in 112ms
```

You can also pipe batch requests from stdin:

```bash
cat updates.ndjson | truto batch --stdin -a <account-id>
```

### Diff

Compare two records field-by-field, or diff the same record across two different integrated accounts:

```bash
truto diff crm/contacts abc-123 def-456 -a <account-id>
truto diff crm/contacts abc-123 -a <account-1> --account2 <account-2>
```

```
Diff: abc-123 vs def-456
┌─────────────┬───────────────┬───────────────┐
│ Field       │ abc-123       │ def-456       │
├─────────────┼───────────────┼───────────────┤
│ email       │ old@test.com  │ new@test.com  │
│ status      │ active        │ inactive      │
└─────────────┴───────────────┴───────────────┘
2 field(s) differ
```

Use `-o json` for programmatic consumption of diff results.

## Built for scripting

The CLI follows Unix conventions. Structured output formats (`json`, `ndjson`, `csv`, `yaml`) automatically suppress decorative status messages so only clean data reaches stdout. Error messages always go to stderr.

Pipe JSON or NDJSON into create commands for bulk operations:

```bash
cat contacts.ndjson | truto accounts create --stdin
echo '[{"name":"a"},{"name":"b"}]' | truto integrations create --stdin
```

Chain CLI commands together:

```bash
truto export crm/contacts -a <account-id> -o ndjson | \
  jq -c '{email: .email}' | \
  truto proxy email-list -m create -a <other-account-id> --stdin
```

Debug any request with verbose mode:

```bash
truto integrations list -v
# → GET https://api.truto.one/integration?limit=25
# ← 200 OK
```

## Full platform coverage

The CLI isn't limited to data queries. It covers the entire Truto admin API:

- **Integrations** — list, create, update, delete integration definitions
- **Accounts** — manage integrated accounts, refresh OAuth credentials
- **Sync Jobs** — configure and trigger data-sync pipelines
- **Workflows** — event-triggered automation with conditional steps
- **Webhooks** — outbound event delivery to your URLs
- **MCP Tokens** — create and manage scoped tokens for AI agent access
- **Environments** — manage isolated scopes within your team
- **Unified Models** — customize cross-integration schemas
- **Datastores, Daemons, Gates, Logs, Schema** — everything else

Every resource follows the same pattern: `truto <resource> <operation> [args] [options]`. Every command supports `--help`.

There's also an interactive mode for when you want to explore without memorizing commands:

```bash
truto interactive
```

It walks you through resource selection, operations, and parameters with a guided wizard.

## Quick summary: why choose Truto over Merge for custom mappings

If you're evaluating Merge.dev as your unified API layer and running into limits on custom schema mapping, per-account overrides, or data-storage constraints, the CLI is your fastest way to see how Truto handles these differently. The short version:

- **Custom schemas are configuration, not code.** Every field mapping is a JSONata expression stored as data. Add, edit, or override mappings without a deployment.
- **Three-level override hierarchy.** Platform defaults, environment overrides, and per-account overrides all deep-merge at request time.
- **Pass-through by default.** Unified and proxy API calls transform data in memory and return it. Third-party response bodies are not persisted unless you explicitly opt into a sync job.
- **OAuth credentials stay encrypted server-side.** Tokens are field-level encrypted at rest, refreshed proactively before expiry, and never exposed to the CLI or logs.
- **Bring your own OAuth apps.** Consent screens show your brand, tokens are issued to your app, and there's no shared vendor app to reason about.

The CLI makes all of this inspectable in a terminal - list mappings, apply overrides, verify data flow, and export raw payloads without opening a dashboard.

## How Truto mapping works: in-memory, not persisted

Mapping happens at request time, in memory, on the same call that hits the third-party API. There is no ETL step, no intermediate database, and no delayed sync required to get transformed data.

Here's what happens when you run `truto unified crm contacts -a <account-id>`:

1. The CLI sends `GET /unified/crm/contacts?integrated_account_id=<id>` to Truto.
2. Truto loads the integration config and mapping expressions for that account's integration, plus any environment and account-level overrides.
3. Truto calls the third-party API (HubSpot, Salesforce, etc.) using the account's OAuth token.
4. The raw response is transformed by JSONata expressions into the unified schema.
5. The unified response is returned to the CLI.

The raw provider payload is not written to persistent storage. The unified response is not written to persistent storage. Mapping configuration is loaded from the database, but the actual transformation is a pure function evaluated in memory during the request lifecycle.

If you *want* persisted data - for offline queries, historical analysis, or reducing calls to a rate-limited provider - Truto offers sync jobs that write to a datastore you control. That's opt-in, not the default. The unified and proxy APIs are pass-through.

## Real-time performance: request flow and latency architecture

For enterprise SaaS teams evaluating unified API platforms in 2026 - Merge.dev, Apideck, Nango, and others - "real-time" is often the deciding factor. Some vendors ETL provider data into their own store and let you query the copy. That's not real-time; that's cached. Truto's unified and proxy APIs are pass-through, so every call fetches live data from the source.

Here's the request flow, end to end:

```
your app  →  api.truto.one  →  third-party API  →  api.truto.one  →  your app
            (auth, route,       (live fetch)         (JSONata
             load config)                             mapping)
```

The critical-path work Truto adds is:

1. **Authentication** - validate your API token, load the integrated account.
2. **Config resolution** - load the integration config and merged mapping (platform defaults → environment override → account override).
3. **Request mapping** - evaluate JSONata expressions to translate your unified query into the provider's native format.
4. **HTTP fetch** - one outbound call to the third-party API.
5. **Response mapping** - evaluate JSONata expressions to translate the provider's response into the unified schema.

Steps 1, 2, 3, and 5 run in memory and typically add a small overhead (single-digit to low-double-digit milliseconds for most workloads). Step 4 - the third-party API call - dominates end-to-end latency. If HubSpot's contacts endpoint takes 300 ms to respond, your unified API call will take approximately 300 ms plus Truto's small additive overhead. There is no queue, no batch, and no eventual consistency in the critical path.

A few architectural details matter for latency:

- **OAuth token refresh runs ahead of the request.** Truto refreshes tokens shortly before they expire, so the credential is already fresh when the request arrives. Concurrent refreshes for the same account are serialized so you never pay the refresh cost twice for the same token.
- **Mapping evaluation is pure and side-effect-free.** JSONata expressions run in memory on already-loaded data. No database round trips inside the transformation step.
- **No hidden hops.** Truto sits between your app and the third-party API - one hop out, one hop back.
- **Pagination is client-controlled by default.** Each unified API call fetches one page. You choose when to paginate via cursors; Truto doesn't secretly walk the entire dataset behind your request.

For formal SLA targets (average and percentile latency, uptime, error budgets), contact sales - specific numbers depend on the integration, the region, and the plan. If your evaluation needs latency benchmarks against a specific provider before signing, the team can arrange a targeted test on your traffic pattern.

## Copy-paste mapping example: request, provider response, unified response

Here's the full round trip. You call the CLI:

```bash
truto unified crm contacts -a <hubspot-account-id>
```

Truto calls HubSpot's contacts endpoint. The raw provider response looks like this:

```json
{
  "id": "12345",
  "properties": {
    "firstname": "Jane",
    "lastname": "Doe",
    "email": "jane@acme.co",
    "jobtitle": "VP Engineering",
    "createdate": "2024-01-15T10:30:00Z"
  }
}
```

The Truto response mapping is a JSONata expression stored as data. This is the entire transformation - no compiled code, no per-integration handler:

```
response.{
  "id": id.$string(),
  "first_name": properties.firstname,
  "last_name": properties.lastname,
  "email_addresses": [{ "email": properties.email, "is_primary": true }],
  "title": properties.jobtitle,
  "created_at": properties.createdate
}
```

The unified response the CLI receives:

```json
{
  "result": [{
    "id": "12345",
    "first_name": "Jane",
    "last_name": "Doe",
    "email_addresses": [{ "email": "jane@acme.co", "is_primary": true }],
    "title": "VP Engineering",
    "created_at": "2024-01-15T10:30:00Z",
    "remote_data": { /* original HubSpot payload preserved */ }
  }]
}
```

The `remote_data` field always carries the original payload, so any provider field the unified schema doesn't cover is still accessible without a second API call. Set `truto_ignore_remote_data=true` if you want to drop it.

Switching the same CLI call to Salesforce only changes the mapping expression:

```
response.{
  "id": Id,
  "first_name": FirstName,
  "last_name": LastName,
  "email_addresses": [{ "email": Email, "is_primary": true }],
  "title": Title,
  "created_at": CreatedDate
}
```

CLI command: identical. Unified response shape: identical. The transformation lives entirely in configuration.

## Per-account overrides: example and JSON schema

Custom fields, tenant-specific behaviors, and edge-case data shapes are handled with per-account overrides. These are stored on the integrated account and deep-merged on top of the platform defaults at request time.

The override document is a nested JSON structure keyed by unified model, resource, and method:

```json
{
  "unified_model_override": {
    "crm": {
      "contacts": {
        "list": {
          "response_mapping": "response.{ 'id': id.$string(), 'first_name': properties.firstname, 'internal_ref': properties.acme_internal_id, 'tier': properties.customer_tier }",
          "query_mapping": "{ 'properties': ['firstname','lastname','email','acme_internal_id','customer_tier'] }"
        },
        "get": {
          "response_mapping": "list"
        }
      }
    }
  }
}
```

Any field of the mapping can be overridden: `response_mapping`, `query_mapping`, `request_body_mapping`, `resource`, `method`, `before`, and `after`. Fields you don't override inherit from the platform defaults.

Apply the override from the CLI:

```bash
truto accounts update <account-id> -b '{
  "unified_model_override": {
    "crm": {
      "contacts": {
        "list": {
          "response_mapping": "response.{ \"id\": id.$string(), \"internal_ref\": properties.acme_internal_id }"
        }
      }
    }
  }
}'
```

The next unified API call for that account uses the merged mapping. No deployment. No support ticket. No integration-specific code path.

Environment-level overrides work the same way but apply to every account in the environment. That's useful when a customer has a consistent set of custom fields across all their connected instances.

## Rate limits, retries, and RapidBridge

Every real integration hits rate limits eventually. Different providers signal them differently - HubSpot uses HTTP 429 with a `Retry-After` header, some Salesforce endpoints return 200 with an error payload, and others use custom `X-RateLimit-*` headers. Handling all of that in your own app would mean writing per-provider back-off logic. Truto normalizes it.

**On the pass-through path (unified and proxy APIs):**

When the provider signals a rate limit in any of its native formats, Truto detects it via a per-integration JSONata expression and returns a standard response to your app:

- HTTP status **429**, regardless of what the provider used.
- A **`Retry-After`** header in seconds, normalized from whatever field the provider populated (`Retry-After`, `X-RateLimit-Reset`, a Unix timestamp, an HTTP-date - Truto converts it).
- Standardized **`ratelimit-limit`**, **`ratelimit-remaining`**, and **`ratelimit-reset`** headers when the provider exposes that information.
- On unified API calls, a **`truto_error_insight.rate_limit_error`** field in the response body so your error handler can key on structured data instead of parsing messages.

Truto does not silently retry a rate-limited pass-through call. It returns the 429 to you so your app decides when and how to back off - which is the right choice for real-time calls where a hidden retry would blow your latency budget.

**On the sync path (RapidBridge):**

RapidBridge is Truto's sync job engine. It's the right tool when you want scheduled or on-demand bulk syncs into a datastore you control, and it has different retry semantics because it runs asynchronously:

- **Per-resource errors are non-fatal by default.** A failed record emits a `sync_job_run:record_error` webhook event and the run continues with the next resource. Set `error_handling: "fail_fast"` on the run to abort on the first error instead.
- **Rate limits are respected.** When a provider returns 429, RapidBridge waits the `Retry-After` interval before retrying that specific request.
- **Incremental syncs use `previous_run_date`.** Only records modified since the last successful run are fetched, which naturally reduces the load you place on rate-limited providers.
- **Webhook delivery has its own retry ladder.** Failed outbound deliveries retry on a fixed back-off schedule and are retained for 30 days for post-hoc inspection.

So: real-time calls give you standardized rate-limit signals; RapidBridge sync jobs handle retries and back-off automatically for scheduled workloads. You pick the tool per use case rather than being forced into one model.

## Security and storage posture: where data flows and what is/isn't stored

For InfoSec review, here's the actual picture of data flow and credential handling:

**Data flow (unified and proxy APIs):**
- Requests come in over TLS.
- Truto reads OAuth credentials from encrypted storage, calls the third-party API on your behalf, and streams the response back.
- Mapping and transformation happen in memory during the request lifecycle.
- Third-party response bodies are not written to persistent storage on unified or proxy API calls.

**What Truto does store:**
- Integration configs and mapping expressions (platform metadata).
- OAuth tokens, refresh tokens, API keys, and similar credentials - all field-level encrypted at rest.
- Optional: synced records if you explicitly configure a sync job to a datastore.
- Request metadata (timestamps, status codes, request IDs, integration name) for observability - not response bodies.

**What Truto does not store on pass-through calls:**
- Third-party response bodies.
- Third-party request bodies your app sent through the proxy or unified API.
- The transformed unified payload returned to your app.

**OAuth credential handling:**
- Access tokens and refresh tokens are encrypted at rest with field-level encryption.
- Truto refreshes tokens shortly before they expire, so pass-through calls always use fresh credentials without your app needing to manage the refresh cycle.
- The CLI never sees third-party OAuth tokens. It uses your Truto API token; Truto handles third-party auth server-side.
- If a refresh fails, the account is marked `needs_reauth` and an `integrated_account:authentication_error` webhook fires so you can prompt the end user.

**Verify it yourself:**
- `truto proxy <resource> -a <id> -v` shows the full request lifecycle from the CLI's perspective. Verbose logs show the Truto API call, not the third-party token.
- `truto logs` lets you review historical request metadata for your environment.
- `truto accounts get <id>` shows what's stored on an integrated account. Sensitive credential fields are masked in listings.

## Zero-storage explained: the technical and legal guarantees

"Zero-storage" is a phrase that gets thrown around loosely by unified API vendors. Here's what it means at Truto in precise terms.

**Technical guarantee.** On every unified API call (`/unified/*`) and every proxy API call (`/proxy/*`), the following happens in-memory only and is discarded when the request completes:

- The outbound request body sent to the third-party API.
- The raw response body received from the third-party API.
- The transformed unified payload returned to your app.
- Any intermediate JSONata evaluation contexts.

The only things persisted for a pass-through call are: (a) a log row with request metadata - path, status code, integration name, integrated account ID, latency, correlation IDs - and (b) any error diagnostics if the call fails. Response bodies are never included in that log row.

**When data *is* persisted.** Three code paths write third-party data to storage, and all three are opt-in and explicit:

1. **Sync jobs (RapidBridge).** You define a sync job that reads from an integration and writes to a datastore - your S3 bucket, your Postgres, your object store, etc. Data lives on the destination you control.
2. **Incoming webhooks.** When a third-party fires a webhook, Truto verifies the signature, transforms the event with JSONata, and delivers it to your subscribed URL. The verified payload is briefly held for retry delivery but is not indexed, queried, or retained beyond the retry window.
3. **Datastore integrations.** If you connect a datastore integration (e.g., a warehouse), reads and writes go through the same pass-through pattern.

**Legal guarantee.** The Data Processing Agreement (DPA) reflects the technical posture: Truto processes third-party data as a data processor on your behalf, does not retain response bodies on pass-through calls, and any persisted data is limited to the sync configurations you explicitly create. Sub-processor list, DPA, and the standard InfoSec questionnaire responses are available on request.

## Deployment options: Truto Cloud and self-hosted

Different enterprise environments have different constraints. Truto supports two deployment models:

**Truto Cloud** is the default - the multi-tenant SaaS deployment at `api.truto.one`. All the standard security controls apply: TLS in transit, field-level encryption at rest for credentials, tenant isolation, audit logging, and role-based access control at the team and environment level. Most customers start here and never need to move.

**Self-hosted (single-tenant / on-prem)** is available for customers with strict data-residency, network-isolation, or regulatory requirements. In this mode:

- The Truto stack runs inside your infrastructure boundary (your cloud account, your VPC, or your on-prem environment).
- All third-party API traffic egresses from your network. Truto never sees your customers' data.
- Encryption keys can be managed by your KMS - AWS KMS, GCP KMS, Azure Key Vault, or an HSM you operate.
- Deployments are version-pinned; upgrades run on your schedule.

Self-hosted is a commercial option and involves a scoping conversation - not every integration or feature is available in every deployment topology, and pricing reflects the operational complexity. Contact sales if this is a hard requirement.

A middle option that fits many procurement conversations: **BYO OAuth apps + BYO datastores** on Truto Cloud. Your customers' consent screens show your brand, tokens are issued to OAuth apps you own, and any persisted data lives in datastores under your account. That's usually enough to satisfy "we own the data path" reviews without the operational load of self-hosting.

## Compliance and certifications

Enterprise procurement typically wants evidence, not marketing claims. Here's what the team can share:

- **SOC 2 Type II report** - available under NDA on request. Covers security, availability, and confidentiality trust services criteria.
- **GDPR** - Truto operates as a data processor. Standard Contractual Clauses, sub-processor disclosures, and the DPA are available on request.
- **HIPAA** - a BAA is available for regulated healthcare workloads that require it. In-scope integrations and deployment topology are scoped per customer.
- **ISO 27001 / other frameworks** - depending on the deployment model and current audit cycle, additional attestations may be available; ask sales for the current list.
- **Penetration testing** - executive summaries from recent third-party pen tests are shareable under NDA.

All of these are handled via the standard vendor security process. The security team can also complete common questionnaires (SIG, CAIQ, custom) as part of evaluation.

## Enterprise FAQ: procurement, sub-processors, encryption

**Do you have a public sub-processor list?**
The sub-processor list is provided under NDA as part of vendor onboarding. It includes the cloud infrastructure provider, observability tooling, and any customer-facing services Truto relies on. Notice is provided when the list changes materially.

**Who owns the encryption keys for our OAuth tokens?**
On Truto Cloud, keys are managed by Truto with rotation and access controls documented in the SOC 2 report. On self-hosted deployments, keys can be issued by your KMS/HSM so no plaintext credential material leaves your infrastructure.

**Can we bring our own OAuth apps?**
Yes, this is the default recommendation for any customer of scale. You register OAuth apps with each third-party provider (HubSpot, Salesforce, Google, etc.) under your own developer account, then configure them in Truto. Your customers see your brand on consent screens, tokens are issued to your app, and there's no shared vendor OAuth app in the trust chain.

**How does data residency work?**
Truto Cloud runs in specific regions; contact sales for the current list. If your requirement is "data must never leave region X," a self-hosted deployment in that region is the cleanest answer. For pass-through calls, remember that response bodies are not persisted - the residency question mostly reduces to "where do integration configs and credential metadata live?"

**How do you compare to Merge.dev, Apideck, and Nango for enterprise SaaS?**
All four platforms offer unified APIs; the differences that matter at enterprise scale are: (a) whether data is real-time pass-through or cached in the vendor's store, (b) whether custom schema extensions require code changes or are configuration-only, (c) whether you can bring your own OAuth apps, and (d) whether self-hosted deployment is available. Truto is pass-through by default, configuration-only for schema customization, BYO-OAuth as a first-class flow, and self-hostable. Run the same query against your evaluation criteria for each vendor - the answers will differ.

**How do you handle vendor lock-in?**
Mappings are declarative JSONata stored as data. You can export them via the CLI or API at any time. Sync jobs write to your datastores, not Truto's. OAuth apps are yours. Migrating off Truto means moving your mappings to another engine, not decoupling from a proprietary storage layer.

**Does the CLI need any special permissions?**
The CLI uses your Truto API token, which is scoped to an environment. Standard team RBAC rules apply - the token can only access what its owner can access. IP allowlisting is available on API tokens if you want to restrict CLI usage to specific egress IPs.

**What's the incident-response process?**
Security incidents are handled per the incident-response plan documented in the SOC 2 report. Customer notification timelines follow the DPA and applicable regulations.

## Migration notes: re-auth, token ownership, and enterprise procurement

If you're moving from Merge.dev (or any other unified API vendor) to Truto, a few things to plan for:

**Re-authentication.** OAuth tokens are provider-scoped and vendor-scoped. Existing tokens on your current platform will not work on Truto. Users need to reconnect their accounts through Truto's OAuth flow. At scale, this is handled with Link Tokens - generate a token per user, embed the Link UI in your app, and users complete OAuth in a few clicks. You can script the token issuance from the CLI:

```bash
truto link-tokens create -b '{"tenant_id":"customer_42","integrations":["hubspot","salesforce"]}'
```

**Token ownership.** With Truto, you bring your own OAuth apps for each integration. Consent screens show *your* company name, and tokens are issued to your app - not a shared vendor app. This matters for enterprise procurement (your end customers see your brand), brand consistency, and reducing the blast radius if a vendor app gets suspended.

**Mapping parity.** If you have custom schema mappings on your current platform, they'll need to be rewritten as JSONata expressions in Truto. The good news: JSONata is more expressive than most vendor-specific DSLs, and the override system lets you customize without touching platform defaults. The Truto team can help port existing mappings during onboarding.

**Enterprise procurement.** Truto offers standard commercial terms, self-hosted deployments, and BAA-covered variants for regulated industries. If your InfoSec team needs a SOC 2 report, DPA, or a self-hosted deployment as part of evaluation, that's part of the standard process. The pass-through architecture also simplifies data-residency reviews - since Truto doesn't persist provider payloads on unified or proxy calls, the data-flow diagram your security team draws is shorter than with vendors that ETL everything into their own storage.

## Get started

Install the CLI and start querying your integrations in under a minute:

```bash
curl -fsSL https://cli.truto.one/install.sh | bash
truto login
truto unified crm contacts -a <your-account-id> -o table
```

Read the full CLI documentation at [truto.one/docs](https://truto.one/docs).

> Questions about the CLI or the Truto platform? We'd love to chat.
>
> [Talk to us](https://truto.one/book-a-demo/)
