# Clatri API

URL: https://docs.clatri.com/en/api/

> Use the Clatri REST API from your own apps and scripts: create a token, make your first call, permissions, idempotency, errors, limits and security.

The **Clatri API** lets you read and change your Clatri data from your own apps, scripts and automations, over HTTPS and JSON. It runs the **same tools** the Clatri agent and the [Clatri MCP](https://docs.clatri.com/en/mcp/) use: log an expense, read your income statement, create a task, list your medications…

It uses the **same token** as the MCP. If you already have one, you don't need another key.

:::tip API or MCP?
- **MCP**: when you want an AI assistant (Claude, ChatGPT, Cursor…) to look at your Clatri while you chat with it. See [Clatri MCP](https://docs.clatri.com/en/mcp/).
- **API**: when you write code. Your program decides which tool to call and with what data, and gets JSON back.
:::

---

## How it works {#how-it-works}

- **Base URL:** `https://api.clatri.com/v1`
- **Authentication:** an `Authorization: Bearer clt_mcp_XXXX` header on every call.
- **Everything is a tool.** To run one, you `POST` to `/v1/entities/{entity_id}/tools/{tool_name}` with its arguments as the JSON body. The entity (personal, business, project) always goes in the path.

| Method and path | What it's for |
| --- | --- |
| `GET /v1/me` | The token: your user, permissions, areas, entities and expiry date. |
| `GET /v1/entities` | The entities this token can use (id, type, name). |
| `GET /v1/tools` | The tools **this token** can call, with their input schema. |
| `POST /v1/entities/{entity_id}/tools/{tool_name}` | Runs a tool on that entity. |
| `GET /v1/openapi.json` | The full OpenAPI 3.1 specification. It's public: no token needed. |

The full list of tools, with their arguments, is in the [API reference](https://docs.clatri.com/en/api/reference/).

---

## Requirements {#requirements}

<details>
<summary>You need an active Clatri subscription</summary>

Like the MCP, the API requires an active subscription. Without one you can't create tokens, and existing tokens respond `402 subscription_inactive` until you renew. They're not deleted: they start working again on their own when you renew.

<details>
<summary>You need to call it from a server or a script</summary>

The API doesn't accept calls from a browser: it sends no CORS headers. That's on purpose, so nobody puts your token in a web page's code. Call it from a backend, a script, a serverless function or an automation tool.

---

## Step 1 — Create your token {#create-token}

1. In the Clatri app, open **Settings** → **Connections** → **Agents and APIs**. On the web, **Settings** → **Agents and APIs**.
2. Tap **Create token** and give it a name you'll recognize (for example, "My expenses script").
3. Choose the **permissions**: **Read only**, **Read and write** or **Full access** (includes delete).
4. Optionally, limit the **areas** (Money, Health, Administration, General) and **entities** it can use. By default it has all of them.
5. Tap **Create token** and copy it.

:::warning Copy it right away
The token (it starts with `clt_mcp_`) is shown **only once**. If you lose it, revoke it and create another. Treat it like a password: anyone who has it can use your data with the permissions you chose.
:::

Tokens expire after 6 months and you can have up to 10 active ones. After creating one, you can change its name, permissions, areas and entities without generating a new one.

In the examples on this page the token is in an environment variable:

```bash
export CLATRI_TOKEN="clt_mcp_XXXX"
```

---

## Step 2 — Your first call {#first-call}

Check that the token works with `GET /v1/me`:

```bash
curl https://api.clatri.com/v1/me \
  -H "Authorization: Bearer $CLATRI_TOKEN"
```

```python
import os
import requests

BASE = "https://api.clatri.com/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['CLATRI_TOKEN']}"}

me = requests.get(f"{BASE}/me", headers=HEADERS, timeout=30)
me.raise_for_status()
print(me.json())
```

```js
// Node.js 18 or later (fetch built in)
const BASE = "https://api.clatri.com/v1";
const headers = { Authorization: `Bearer ${process.env.CLATRI_TOKEN}` };

const res = await fetch(`${BASE}/me`, { headers });
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
console.log(await res.json());
```

The response looks like this (made-up data):

```json
{
  "user_id": "11111111-1111-4111-8111-111111111111",
  "credential": "personal_token",
  "token_name": "My expenses script",
  "permissions": ["read", "write"],
  "areas": ["money", "administration"],
  "entity_ids": null,
  "entities": [
    {"id": "22222222-2222-4222-8222-222222222222", "type": "personal", "name": "Personal", "is_shared": false},
    {"id": "33333333-3333-4333-8333-333333333333", "type": "business", "name": "My company", "is_shared": false}
  ],
  "expires_at": "2027-04-29T15:00:00+00:00",
  "request_id": "req_0f1e2d3c4b5a69788796a5b4c3d2e1f0"
}
```

- `areas: null` means all areas; `entity_ids: null`, all your entities.
- `is_shared: true` marks an entity someone else shared with you.

### Pick the entity {#choose-entity}

`GET /v1/entities` returns just the token's entities. Keep the `id` of the one you'll use:

```bash
curl https://api.clatri.com/v1/entities \
  -H "Authorization: Bearer $CLATRI_TOKEN"
```

```json
{
  "entities": [
    {"id": "22222222-2222-4222-8222-222222222222", "type": "personal", "name": "Personal", "is_shared": false}
  ],
  "request_id": "req_…"
}
```

### Discover the tools {#discover-tools}

`GET /v1/tools` lists what **this token** can do: it's already filtered by its permissions and areas. Each tool comes with its input schema (JSON Schema):

```bash
curl https://api.clatri.com/v1/tools \
  -H "Authorization: Bearer $CLATRI_TOKEN"
```

```json
{
  "tools": [
    {
      "name": "create_task",
      "title": "Create task",
      "description": "Create a task. …",
      "area": "administration",
      "permission": "write",
      "destructive": false,
      "supports_dry_run": false,
      "requires_confirmation": false,
      "requires_idempotency_key": true,
      "path": "/v1/entities/{entity_id}/tools/create_task",
      "input_schema": {"type": "object", "properties": {"title": {"type": "string"}}, "required": ["title"]}
    }
  ],
  "request_id": "req_…"
}
```

---

## Step 3 — Run a tool {#call-a-tool}

### A read {#read-example}

Income statement for September 2026 (what you earned and spent) on the personal entity:

```bash
curl -X POST \
  https://api.clatri.com/v1/entities/22222222-2222-4222-8222-222222222222/tools/read_income_statement \
  -H "Authorization: Bearer $CLATRI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"start_date": "2026-09-01", "end_date": "2026-09-30", "include_analytics": false}'
```

```python
ENTITY = "22222222-2222-4222-8222-222222222222"

res = requests.post(
    f"{BASE}/entities/{ENTITY}/tools/read_income_statement",
    headers=HEADERS,
    json={"start_date": "2026-09-01", "end_date": "2026-09-30", "include_analytics": False},
    timeout=60,
)
res.raise_for_status()
body = res.json()
print(body["text"])
```

```js
const ENTITY = "22222222-2222-4222-8222-222222222222";

const res = await fetch(`${BASE}/entities/${ENTITY}/tools/read_income_statement`, {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify({ start_date: "2026-09-01", end_date: "2026-09-30", include_analytics: false }),
});
const body = await res.json();
console.log(body.text);
```

Every successful response has the same shape:

```json
{
  "data": {"…": "structured result, when the tool returns JSON"},
  "text": "The tool's answer as text",
  "request_id": "req_6c1d0e2a9b7f4e3c8a5d1f0b2e4c6a8d",
  "idempotent_replay": false
}
```

| Field | What it holds |
| --- | --- |
| `data` | The result as JSON when the tool returns it. If the tool only answers with text, it's `null`. |
| `text` | The tool's full answer, as text. Always present. |
| `request_id` | The id of the call. Also sent in the `Clatri-Request-Id` header. Include it if you contact support. |
| `idempotent_replay` | `true` when this is the stored response of an earlier call with the same `Idempotency-Key` (see below). |

:::note
Don't send `entity_id` in the body: it goes in the path. An argument the tool doesn't know is also rejected with `400 validation_error`, before anything runs.
:::

### A write {#write-example}

Tools that create, edit or delete require the `Idempotency-Key` header. Creating a task on the personal entity:

```bash
curl -X POST \
  https://api.clatri.com/v1/entities/22222222-2222-4222-8222-222222222222/tools/create_task \
  -H "Authorization: Bearer $CLATRI_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 5b0d7f8e-3c2a-4e1b-9f6d-1a2b3c4d5e6f" \
  -d '{"title": "Renew the car insurance", "due_date": "2026-11-15"}'
```

```python
import uuid

res = requests.post(
    f"{BASE}/entities/{ENTITY}/tools/create_task",
    headers={**HEADERS, "Idempotency-Key": str(uuid.uuid4())},
    json={"title": "Renew the car insurance", "due_date": "2026-11-15"},
    timeout=60,
)
res.raise_for_status()
print(res.json()["text"])
```

```js
const res = await fetch(`${BASE}/entities/${ENTITY}/tools/create_task`, {
  method: "POST",
  headers: {
    ...headers,
    "Content-Type": "application/json",
    "Idempotency-Key": crypto.randomUUID(),
  },
  body: JSON.stringify({ title: "Renew the car insurance", due_date: "2026-11-15" }),
});
console.log((await res.json()).text);
```

---

## Permissions, areas and entities {#permissions}

Every token has three limits. A call that goes beyond any of them is rejected before anything runs.

**Permissions.** Each tool requires one permission:

| Permission in the API | What it allows | App level that includes it |
| --- | --- | --- |
| `read` | Query | Read only, Read and write, Full access |
| `write` | Create and edit | Read and write, Full access |
| `delete` | Delete or cancel | Full access |

Without the permission → `403 scope_denied`.

**Areas.** Each tool belongs to one area:

| Area in the app | In the API | What it covers |
| --- | --- | --- |
| Money | `money` | Accounts, cards, movements, debts, savings, investments, sales and accounting |
| Health | `health` | Conditions, episodes, medications, prescriptions, metrics and health documents |
| Administration | `administration` | Spaces, pages, boards, tasks, habits, calendar, contacts and places |
| General | `general` | Context, memory, documents, schedules and activity |

Outside the token's areas → `403 domain_denied`.

**Entities.** If you limited the token to some entities, any other one responds `403 entity_denied`. The same happens if the body mentions a record (an account, a task…) from another entity.

:::tip Give each integration only what it needs
A script that only reads expenses doesn't need to write or see your health. Create a **Read only** token, with the **Money** area and the right entity. If it ever leaks, the damage stays contained.
:::

---

## Safe writes {#safe-writes}

### Idempotency-Key {#idempotency}

Tools with the `write` or `delete` permission require the `Idempotency-Key` header. Without it → `400 idempotency_key_required`.

Generate a unique value per operation (a UUID works; 1 to 255 visible ASCII characters) and **reuse it when you repeat that same operation**, for example after a network drop. That way a retry doesn't log the expense twice:

| Situation | Result |
| --- | --- |
| Same key and same body, the first call already finished | You get the stored response, with `idempotent_replay: true`. It doesn't run again. |
| Same key with another body, another tool or another entity | `409 idempotency_key_reused` |
| Same key while the first call is still running | `409 idempotency_in_progress`, with `Retry-After` |

- A key belongs to the token that used it and is remembered for 24 hours.
- If the first call ended with a server error (5xx), the key is released and you can retry with the same one.
- Reads don't need a key.

### Preview with `?dry_run=true` {#dry-run}

Some tools can show you the result **without saving anything**. They're the ones with `supports_dry_run: true` in `GET /v1/tools`. Add `?dry_run=true` to the URL:

```bash
curl -X POST \
  "https://api.clatri.com/v1/entities/22222222-2222-4222-8222-222222222222/tools/record_consumable_balance_observation?dry_run=true" \
  -H "Authorization: Bearer $CLATRI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"balance_identifier": "Transit card", "observed_amount": 25000, "source": "api"}'
```

- With `?dry_run=true` the tool only computes (`preview: true`), whatever the body says.
- A preview doesn't need an `Idempotency-Key` and its response isn't stored.
- On a tool without a preview, `?dry_run=true` responds `400 dry_run_not_supported`: it never runs "for real" by mistake.

### Confirm deletes with `?confirm=true` {#confirm}

Tools with the `delete` permission (deleting or cancelling data) require `?confirm=true` in the URL, on top of the `Idempotency-Key`. Without the confirmation → `428 confirmation_required` and nothing is deleted:

```bash
curl -X POST \
  "https://api.clatri.com/v1/entities/22222222-2222-4222-8222-222222222222/tools/delete_task?confirm=true" \
  -H "Authorization: Bearer $CLATRI_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9e8d7c6b-5a49-4382-9170-6f5e4d3c2b1a" \
  -d '{"task_id": "44444444-4444-4444-8444-444444444444"}'
```

In `GET /v1/tools` these tools have `requires_confirmation: true`. The `destructive: true` field also flags the ones that overwrite data.

---

## Errors {#errors}

Errors arrive as `application/problem+json` ([RFC 9457](https://www.rfc-editor.org/rfc/rfc9457)), always with a stable `code` you can use in your code:

```json
{
  "type": "https://docs.clatri.com/api/errors#scope_denied",
  "title": "Permission not granted",
  "status": 403,
  "detail": "This token does not have the permission this tool requires.",
  "code": "scope_denied",
  "request_id": "req_6c1d0e2a9b7f4e3c8a5d1f0b2e4c6a8d"
}
```

The wording of `title` and `detail` can change: decide with `code` and `status`.

| HTTP | Codes |
| --- | --- |
| 400 | `validation_error`, `idempotency_key_required`, `dry_run_not_supported`, `https_required` |
| 401 | `unauthorized`, `token_revoked`, `token_expired` |
| 402 | `subscription_inactive` |
| 403 | `scope_denied`, `domain_denied`, `entity_denied` |
| 404 | `tool_not_found`, `tool_not_available`, `not_found` |
| 405 | `method_not_allowed` |
| 409 | `idempotency_key_reused`, `idempotency_in_progress` |
| 413 | `payload_too_large` |
| 422 | `tool_error` |
| 428 | `confirmation_required` |
| 429 | `rate_limited` |
| 500 | `internal_error` |
| 503 | `service_unavailable` |

What each code means and what to do: [API errors](https://docs.clatri.com/en/api/errors/).

`422 tool_error` is the most common day to day: the tool ran but couldn't complete what you asked (for example, an account that doesn't exist). The `detail` explains why.

---

## Limits {#limits}

| Limit | Value |
| --- | --- |
| Reads per token | 120 per minute |
| Writes and deletes per token | 30 per minute |
| Calls per entity (all tokens combined) | 300 per minute |
| Maximum body size | 256 KB |

- Windows are fixed one-minute windows. `GET /v1/me`, `/v1/entities` and `/v1/tools` count as token reads.
- Responses carry `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the window resets). When several limits apply, the headers show the one closest to running out.
- Going over returns `429 rate_limited` with `Retry-After` in seconds. Wait that long before retrying.
- **Blocking after repeated failures:** 20 `401` or `403` responses within 5 minutes, for the same token or the same IP, block that token or IP for 15 minutes (`429` with `Retry-After`). If your script gets a `401` or `403`, fix the cause instead of retrying in a loop.
- Some tools (web search, maps, images) use your account's credits, as when the agent uses them.

---

## Useful headers {#headers}

| Header | Where | What for |
| --- | --- | --- |
| `Authorization: Bearer …` | Request | Your token. Required except on `/v1/openapi.json`. |
| `Idempotency-Key` | Request | Required on writes and deletes. |
| `Content-Type: application/json` | Request | On calls with a body. |
| `Clatri-Request-Id` | Response | Id of the call, on every response. |
| `RateLimit-*`, `Retry-After` | Response | Rate limits (see above). |

The API only works over HTTPS. Responses carry `Cache-Control: no-store`.

---

## Versioning and deprecation {#versioning}

- The version is in the path: `/v1`. A breaking change ships as a new version.
- Before a version is retired, its responses carry the `Deprecation` and `Sunset` headers, with at least **90 days** of notice.
- Error codes are part of the contract: new codes can appear, but existing ones aren't renamed or removed within `v1`.
- The tools are the same ones the Clatri agent uses, so the catalog grows with the product. Check `GET /v1/tools` or the [reference](https://docs.clatri.com/en/api/reference/) for the current schema.

---

## Security {#security}

<details>
<summary>Take care of your token</summary>

- **Never share it** or paste it into chats, screenshots, tickets or forums.
- **Don't commit it to a repository** or put it in the code of a mobile app or a web page. Keep it in an environment variable or a secrets manager.
- **One token per integration.** That way you can revoke one without affecting the others, and the activity shows what each one did.
- **If it leaked, revoke it** in Settings → Connections → Agents and APIs. Revocation is immediate. Then create another.

<details>
<summary>What a token can and can't do</summary>

A token only acts inside Clatri, with the permissions, areas and entities you gave it. It **can't** create other tokens, change your password or email, touch your subscription, delete an entity or see entities outside its scope.

Every tool call is recorded in the token's recent activity (Settings → Connections → Agents and APIs → tap the token), just like MCP calls. If you see something you don't recognize, revoke it.

<details>
<summary>Tools that aren't available through the API</summary>

Some of the agent's tools can't be called through the API even if the token has the permission. They respond `404 tool_not_available` and don't appear in `GET /v1/tools` or in the reference:

- **Free-form database queries.** They're not a stable, reviewable contract.
- **Document passwords.** Saving or using a protected PDF's password is credential handling.
- **Access management.** Inviting or removing members of a space, seeing who else is in it, or linking a contact to another Clatri account: they change who sees your data.
- **Looking up other entities or people.** A token never sees entities outside its own.
- **Schedules.** Creating, editing or running a schedule later puts the full agent to work with all your permissions, outside the token's limits.
- **`whoami` and `list_entities`** from the MCP: in the API, `GET /v1/me` and `GET /v1/entities` replace them.

For these actions, use the Clatri app or chat.

<details>
<summary>If you connect the API to an AI model</summary>

What the API returns is your data: movement descriptions, document text, notes. If you pass it to an AI model, treat it as data, not instructions. Text inside a document could try to give the model orders.

<details>
<summary>OAuth tokens from connected apps</summary>

Besides personal tokens (`clt_mcp_`), the API accepts the OAuth access tokens (`clt_at_`) that apps you connected to Clatri through OAuth receive. Those tokens only see the reduced catalog of tools those apps use in the MCP.

---

## API reference {#reference}

- **[Interactive reference](https://docs.clatri.com/en/api/reference/)**: every tool, with its arguments, permissions, areas and body examples.
- **OpenAPI 3.1:** `https://api.clatri.com/v1/openapi.json` (public, always current). Import it into Postman, Insomnia or your client generator.
- **[API errors](https://docs.clatri.com/en/api/errors/)**: each code, its cause and what to do.
