API and integrations

Thoute has a public HTTP and WebSocket API so you can build your own integrations and automations against your vault: a cron job that writes a daily journal entry, a script that archives attachments, a dashboard that reports your task counts, or an AI agent connected through the Thoute MCP server.

Access uses long-lived API keys that you create in the app. This guide walks through creating a key, authenticating, scopes, rate limits, every endpoint with examples, and how to work with the encrypted data the API returns.

Before you start: what an API key can and cannot see

Your vault is end-to-end encrypted. The server only ever stores ciphertext, and an API key does not change that. The text of your blocks, your page titles, and your attachments are all encrypted with a key derived from your passphrase, and that passphrase never leaves your devices.

So an API key lets a program reach your vault, but the content it receives is ciphertext. To read the actual text, the program has to decrypt it locally with your passphrase. See Working with encrypted data below.

What this means in practice:

Quickstart

1. Create a key. Verify your email if you have not already (key creation is only available to verified accounts), then open Settings → Integrations, click New API key, give it a name, pick its scopes, and optionally limit it to one vault or set an expiry.

2. Copy the key. The full key (thoute_pk_…) is shown only once and is never recoverable. The server stores only a hash of it. If you lose it, revoke it and make a new one.

3. Make your first request. List your vaults:

curl https://app.thoute.com/api/sync/vaults \
  -H "Authorization: Bearer thoute_pk_xxxxxxxxxxxxxxxxxxxxxxxxxxxx"
{
  "vaults": [
    {
      "id": "vault-1771087410473-racgxme76",
      "name": "John Brain",
      "keystore": { "version": 1, "salt": "…", "iv": "…", "ciphertext": "…" },
      "lastSyncedAt": "2026-05-23T17:09:00.007Z",
      "isDefault": false,
      "epoch": 7
    }
  ]
}

That id is the vaultId you pass to the other endpoints.

Authentication

Send the key as a Bearer token on every request:

Authorization: Bearer thoute_pk_xxxxxxxxxxxxxxxxxxxxxxxxxxxx

The base URL is https://app.thoute.com. The same header authenticates the real-time sync WebSocket; the WebSocket also accepts the key as a token query parameter.

You can revoke a key at any time from Settings → Integrations. A revoked key stops working on its next request.

Scopes

Each key carries one or more scopes. A request that lacks the scope an endpoint requires is rejected with 403 and code: "INSUFFICIENT_SCOPE". Grant the narrowest set your integration needs.

Scope Grants
read:vault Read encrypted vault data: vault list, keystore, attachments, and read-only sync.
write:vault Create and update content, upload encrypted attachments, and write over sync.
read:account Read account usage and storage stats.

Per-vault scoping

A key can be bound to a single vault. A bound key only ever sees that vault: the vault list is filtered to it, and any request aimed at a different vault is rejected with 403 and code: "VAULT_SCOPE_MISMATCH". A key with no vault binding can reach every vault on your account. Binding a key to one vault is a good way to limit the blast radius of a key you hand to a third-party script.

Rate limits

Each key is limited to 1000 requests per minute. Every response carries the standard headers so you can pace yourself:

RateLimit-Policy: 1000;w=60
RateLimit: limit=1000, remaining=995, reset=42

reset is the seconds until the window refills. When you exceed the limit the API returns 429 with a Retry-After header.

Errors

Errors are JSON with a human-readable error and, where useful, a machine-readable code:

{ "error": "Missing required scope: write:vault", "code": "INSUFFICIENT_SCOPE" }
Status Meaning Codes you may see
400 The request was malformed (missing or invalid body).
401 The key is missing, invalid, expired, or revoked.
403 The key is valid but not permitted. INSUFFICIENT_SCOPE, VAULT_SCOPE_MISMATCH, ACCOUNT_SUSPENDED
404 The vault, attachment, or resource does not exist.
429 Rate limit exceeded.

For privacy, every authentication failure returns the same generic 401, so the API never reveals whether a particular key exists.

Endpoints

The base URL is https://app.thoute.com. Everything that touches vault content returns ciphertext (see Working with encrypted data).

List vaults

GET /api/sync/vaults · scope read:vault

Returns your vaults, each with its encrypted keystore. A vault-bound key returns only its vault.

curl https://app.thoute.com/api/sync/vaults \
  -H "Authorization: Bearer $KEY"
{
  "vaults": [
    {
      "id": "vault-1771087410473-racgxme76",
      "name": "John Brain",
      "keystore": { "version": 1, "salt": "…", "iv": "…", "ciphertext": "…" },
      "lastSyncedAt": "2026-05-23T17:09:00.007Z",
      "isDefault": false,
      "epoch": 7
    }
  ]
}

Get a vault keystore

GET /api/sync/keystore?vaultId={vaultId} · scope read:vault

Returns the encrypted keystore for one vault. You decrypt this with your passphrase to derive the vault key (see below).

curl "https://app.thoute.com/api/sync/keystore?vaultId=vault-1771087410473-racgxme76" \
  -H "Authorization: Bearer $KEY"
{
  "keystore": { "version": 1, "salt": "…", "iv": "…", "ciphertext": "…" },
  "vaultId": "vault-1771087410473-racgxme76",
  "updatedAt": "2026-05-23T17:09:00.007Z"
}

Check vault status

GET /api/sync/vault-status/{vaultId} · scope read:vault

Reports whether a vault exists on the server and when it last synced. Useful as a lightweight health check before connecting the sync WebSocket.

{
  "existsOnServer": true,
  "syncEnabled": true,
  "vaultName": "John Brain",
  "resetEpoch": 7,
  "lastSyncedAt": "2026-05-23T17:09:00.007Z",
  "lastSyncedDevice": "MacBook Pro",
  "lastSyncedDeviceId": "…"
}

Download an attachment

GET /api/sync/attachments/{vaultId}/{filename}/encrypted · scope read:vault

Returns the encrypted attachment as a JSON payload. The filename is the opaque id stored in the attachment block (not the original file name). Decrypt the payload with the vault key to recover the file bytes.

{ "v": 1, "iv": "…", "data": "…" }

Upload an attachment

POST /api/sync/attachments/{vaultId}/encrypted · scope write:vault

Uploads an already-encrypted payload. Encrypt the file with the vault key on your side, then send the ciphertext. extHint is an optional file-type hint used only to label the stored blob.

curl -X POST "https://app.thoute.com/api/sync/attachments/$VAULT/encrypted" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -d '{ "payload": { "v": 1, "iv": "…", "data": "…" }, "extHint": "pdf" }'
{ "filename": "a1b2c3d4.pdf.enc", "uploadedAt": "2026-05-23T17:30:00.000Z", "encrypted": true }

Account stats

GET /api/account/stats · scope read:account

{
  "createdAt": "2026-01-16T15:21:57.630Z",
  "lastSeenAt": "2026-05-23T17:08:06.881Z",
  "vaultCount": 4,
  "attachmentCount": 11,
  "totalStorageBytes": 1657711
}

Working with encrypted data

The vault list and keystore endpoints hand you a small JSON object shaped like { version, salt, iv, ciphertext }. That is your keystore: the vault key, encrypted under your passphrase.

The decryption chain is:

  1. Derive a wrapping key from your passphrase and the keystore salt using PBKDF2.
  2. AES-GCM decrypt the keystore ciphertext (with its iv) to recover the vault key.
  3. Use the vault key to AES-GCM decrypt block and page content, attachment payloads ({ v, iv, data }), and anything else the API returns for that vault.

Implementing this by hand is involved, and getting it wrong is easy. Unless you have a specific reason to roll your own, use the Thoute MCP server, which performs the whole chain for you and exposes plain read and write tools. For the details of the scheme, see Privacy.

Real-time sync

Vault content lives in CRDT documents synced over a WebSocket at wss://app.thoute.com/yjs, the same channel the Thoute apps use. This is how an integration reads and writes blocks and pages (as opposed to attachments, which use the HTTP endpoints above).

The document contents are encrypted, so the same client-side decryption applies. The simplest way to consume this is the MCP server, which wraps both the HTTP API and the sync channel.

The MCP server

The Thoute MCP server is a small program you run on your own machine. You give it an API key and your passphrase, and it exposes your vault to any MCP-aware client (such as Claude Desktop) as a set of natural tools: search your notes, read a page, append to today's journal, and so on. Because it runs locally and holds your passphrase, it does the decryption the cloud never can, which keeps the zero-knowledge guarantee intact.

It is the recommended way to connect Thoute to AI agents, and the reason the API exists in its current shape.

Exposed tools include search_vault (hybrid keyword and on-device semantic search), get_block, get_page, list_today_journal, and summarize_recent, plus opt-in create_block and update_block. Search runs locally over your decrypted notes using the same embedding model as the app, so your content never leaves your machine.

It's published as @thoute/mcp on npm, so there's nothing to install — your MCP client runs it with npx @thoute/mcp. To set it up: create a key in Settings → Integrations, then add a thoute server to your client's MCP config with command: "npx", args: ["-y", "@thoute/mcp"], and your key, passphrase, and (optional) vault id in the env block. Writes are off by default and require both a write:vault key and an explicit opt-in (THOUTE_MCP_ALLOW_WRITE=1). Full details are in the mcp/ README in the Thoute repository.

FAQ

The Create key button is greyed out. Key creation requires a verified email address. Verify your email, then reload Settings.

I lost my key. Keys are shown only once and cannot be recovered. Revoke the old one in Settings → Integrations and create a new one.

A request returns 403 with INSUFFICIENT_SCOPE. The key does not carry the scope that endpoint needs. Create a key with the right scopes, or widen an existing one by making a new key (scopes are fixed at creation).

A request returns 403 with VAULT_SCOPE_MISMATCH. The key is bound to a different vault than the one in the request. Use a key bound to that vault, or an unbound key.

Why is the content gibberish? That is expected. The API returns ciphertext. See Working with encrypted data.

See also