Back to Blog

Clams REST API Guide | For Developers & Coding Agents

· 7 min read

The Clams Server exposes a REST API that lets you build Bitcoin accounting into your own tools. Automate tax reporting, build portfolio dashboards, or let a coding agent manage your books. This post walks through the full workflow, from authenticating to pulling reports. The full API reference is available at clams.tech/docs/api.

Contents:
Authentication
Creating a Workspace
Setting Up a Profile
Connecting Wallets
Syncing
Real-Time Updates
Processing Journals
Metadata and Annotations
Reports
Tooling Tips

Before anything else, download and run Clams Server.

If you're wiring up Clams from Claude Code, Codex, Open Code, or any other agent framework, everything here applies. The curl commands are copy-pasteable and both the Auth and Server API docs are designed to be machine-readable.

Setting up your agent? Point it at these resources in order:

  1. Auth quickstart: plain markdown that explains how the auth layer works. It's the fastest way for an LLM to understand token flows, refresh semantics, and client credentials.
  2. Auth API reference: full Swagger/OpenAPI spec for the auth server.
  3. REST API reference: full Swagger/OpenAPI spec for the Clams Server API.

Authentication

The API uses Bearer tokens (EdDSA-signed JWTs) issued by the Clams auth server at auth.clams.tech. You don't need to run the auth server yourself. We host it.

The basic flow: log in via a browser, exchange the resulting token for an audience-scoped access token and refresh token, then refresh as needed. The access token goes in the Authorization header on every API call. The auth quickstart walks through each step with curl commands.

Key things to know:

  • Access tokens expire in 5 minutes. The Clams CLI handles refresh automatically, but if you're calling the API directly, refresh is your responsibility.
  • Refresh tokens are opaque, 90-day TTL, and rotate on every use. Reusing an old refresh token revokes the session. When the 90-day TTL expires, you'll need to re-authenticate via the browser login flow.
  • Audiences: when requesting a token, you specify an audience. Use svc for the Clams Server API, rates for exchange rates, or feedback for the feedback endpoint. Most of what this guide covers uses svc.
  • Client credentials: for machine-to-machine access without a browser login, provision an OAuth client via POST /v1/oauth-clients, then request tokens with grant_type "client_credentials".

If you're already using the Clams CLI, your credentials are stored locally and you can bootstrap a token from them. The credentials file location depends on your OS:

  • macOS: ~/Library/Application Support/Clams/backend/cli/credentials.json
  • Linux: ~/.local/share/clams/backend/cli/credentials.json (or $XDG_DATA_HOME/clams/backend/cli/credentials.json)
  • Windows: %APPDATA%\Clams\backend\cli\credentials.json

The script below is a macOS example that reads your stored refresh token, hits the auth server, and exports a fresh access token:

#!/usr/bin/env bash
CREDS="$HOME/Library/Application Support/Clams/backend/cli/credentials.json"
RT=$(jq -r .svc_refresh_token "$CREDS")
RESP=$(curl -s -X POST https://auth.clams.tech/v1/token \
  -H 'Content-Type: application/json' \
  -d "{\"grant_type\":\"refresh_token\",\"refresh_token\":\"$RT\",\"audience\":\"svc\"}")
NEW_TOKEN=$(echo "$RESP" | jq -r .access_token)
NEW_RT=$(echo "$RESP" | jq -r .refresh_token)
if [ "$NEW_TOKEN" = "null" ] || [ -z "$NEW_TOKEN" ]; then
  echo "ERROR: failed to get token"
  echo "$RESP" | jq .
  return 1 2>/dev/null || exit 1  # return if sourced, exit if executed
fi
export TOKEN="$NEW_TOKEN"
jq --arg at "$NEW_TOKEN" --arg rt "$NEW_RT" \
  '.svc_access_token=$at | .svc_refresh_token=$rt' "$CREDS" > "$CREDS.tmp" \
  && mv "$CREDS.tmp" "$CREDS"
echo "TOKEN set (expires in ~5 min)"

Note: This script writes the new refresh token back to the CLI's credentials file. If the CLI refreshes independently, its token will invalidate the one this script wrote (and vice versa) — refresh tokens are single-use.

Save it somewhere convenient and source it whenever you get a 401:

source ~/refresh-token.sh

On a different OS, or prefer not to write it yourself? Point your coding agent at the auth quickstart. It has everything needed to generate a refresh script for your platform.

Postman users: You can automate token refresh using a pre-request script on your collection. The script calls the refresh endpoint before each request and stores the new token in an environment variable. No more manual 401 cycling.

Every curl command below assumes $TOKEN and $CLAMS_BASE_URL are set.

Set your base URL before making any API calls. If you've deployed Clams Server to a remote host, replace this with your server's URL (e.g. https://api.your-domain.com).

export CLAMS_BASE_URL="http://127.0.0.1:8080"

Creating a Workspace

A workspace is your top-level organizational unit. Profiles live inside workspaces and each profile isolates its own user data. Create a workspace first:

curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  $CLAMS_BASE_URL/v1/workspaces \
  -d '{"label": "my-workspace"}'

Grab the id from the response. You'll need it for everything that follows.

Setting Up a Profile

A profile holds your connections, settings, and data. The setup endpoint combines four separate calls (create profile, create onchain config, select it, set profile settings) into one:

curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  $CLAMS_BASE_URL/v1/workspaces/{workspace_id}/profiles/setup \
  -d '{
    "profile_label": "main",
    "fiat_currency": "USD",
    "gains_algorithm": "FIFO",
    "tor_proxy": null,
    "onchain_label": "my-electrum",
    "onchain_kind": "Electrum",
    "onchain_url": "tcp://your-electrum-server:50001",
    "rpc_cookie": null,
    "rpc_user": null,
    "rpc_password": null,
    "select_as_default": true
  }'

onchain_kind types:

  • Electrum
  • Esplora
  • BitcoinRpc (requires rpc_user/rpc_password or rpc_cookie)

Tor users: If your node is on Tor (Start9, Umbrel, etc.), you'll need to set the tor_proxy field. The port depends on how you run Tor: standalone Tor daemon defaults to 127.0.0.1:9050, and Tor Browser defaults to 127.0.0.1:9150.

If you need granular control, you can make the four calls individually: create profile, create onchain config, select it, set profile settings. See the API reference for each endpoint.

Connecting Wallets

With a profile ready, add a connection. Most kinds (Descriptor, XPub, Address, etc.) require configuration at creation time. For a descriptor wallet:

curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  $CLAMS_BASE_URL/v1/workspaces/{workspace_id}/profiles/{profile_id}/connections \
  -d '{"label": "my-wallet", "kind": "descriptor", "configuration": {"descriptor": "wpkh([fingerprint/84h/0h/0h]xpub.../0/*)"}}'

You can update a connection's configuration later with PATCH:

curl -s -X PATCH -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  $CLAMS_BASE_URL/v1/workspaces/{workspace_id}/profiles/{profile_id}/connections/{connection_id} \
  -d '{"configuration": {"descriptor": "wpkh([fingerprint/84h/0h/0h]xpub.../0/*)"}}'

List your connections to confirm:

curl -s -H "Authorization: Bearer $TOKEN" \
  "$CLAMS_BASE_URL/v1/workspaces/{workspace_id}/profiles/{profile_id}/connections?limit=50"

Connection kinds available at time of writing:

  • Descriptor
  • XPub
  • Address
  • CoreLn
  • Lnd
  • Nwc
  • Phoenix
  • Custom

Each kind has its own configuration fields. See the API reference for details on each. You can also list available kinds from the API directly:

curl -s -H "Authorization: Bearer $TOKEN" \
  "$CLAMS_BASE_URL/v1/workspaces/{workspace_id}/profiles/{profile_id}/connections/kinds"

Syncing

Sync all connections for a profile:

curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  $CLAMS_BASE_URL/v1/workspaces/{workspace_id}/profiles/{profile_id}/connections/sync

This returns an operation_id. Check its status:

curl -s -H "Authorization: Bearer $TOKEN" \
  $CLAMS_BASE_URL/v1/operations/{operation_id}

The status will move through QueuedRunningSucceeded, SucceededWithFailures, or Failed. SucceededWithFailures means some connections synced while others hit errors — check the failures array in the result for details.

Real-Time Updates

Instead of polling, you can connect to the SSE notifications stream:

curl -H "Authorization: Bearer $TOKEN" \
  "$CLAMS_BASE_URL/v1/notifications/stream?operation_id={operation_id}"

Events arrive as JSON:

{
  "operation_id": "...",
  "phase": "Progress",
  "level": "Info",
  "message": "Fetching transactions",
  "progress": { "current": 12, "total": 50 }
}

Useful if you're building a UI or want to trigger the next step automatically.

Processing Journals (Required Before Reports)

Syncing only imports raw records. To turn them into journal entries (which power all the reports), you need to process journals separately. If you skip this step, reports will return empty or incomplete data.

curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  $CLAMS_BASE_URL/v1/workspaces/{workspace_id}/profiles/{profile_id}/journals/process

Unlike syncing, this call is synchronous — it returns a JournalRunOutcome directly with the number of quarantined events (if any). Run this after every sync before pulling reports.

Metadata and Annotations

Once journals are processed, you can add notes, tags, and exclusions to journal events before pulling reports. First, list your journal events to grab an event ID:

curl -s -H "Authorization: Bearer $TOKEN" \
  "$CLAMS_BASE_URL/v1/workspaces/{workspace_id}/profiles/{profile_id}/journals/events?limit=10"

Add a note:

curl -s -X PUT -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  "$CLAMS_BASE_URL/v1/workspaces/{workspace_id}/profiles/{profile_id}/metadata/records/{event_id}/note" \
  -d '{"note": "Office hardware purchase"}'

Create a tag:

curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  "$CLAMS_BASE_URL/v1/workspaces/{workspace_id}/profiles/{profile_id}/metadata/tags" \
  -d '{"code": "business-expense", "label": "Business Expense"}'

The code is a canonical identifier (automatically lowercased). The label is the display name shown in UIs.

Exclude an event from reports:

curl -s -X PUT -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  "$CLAMS_BASE_URL/v1/workspaces/{workspace_id}/profiles/{profile_id}/metadata/records/{event_id}/excluded" \
  -d '{"excluded": true}'

Reports

With your data synced, journals processed, and any annotations in place, you can pull reports. The balance sheet and portfolio summary always return current state. Capital gains supports date range filtering.

Balance sheet:

curl -s -H "Authorization: Bearer $TOKEN" \
  "$CLAMS_BASE_URL/v1/workspaces/{workspace_id}/profiles/{profile_id}/reports/balance-sheet?limit=50"

Portfolio summary:

curl -s -H "Authorization: Bearer $TOKEN" \
  "$CLAMS_BASE_URL/v1/workspaces/{workspace_id}/profiles/{profile_id}/reports/portfolio-summary"

Capital gains CSV (with optional date range for tax reporting):

curl -s -H "Authorization: Bearer $TOKEN" \
  "$CLAMS_BASE_URL/v1/workspaces/{workspace_id}/profiles/{profile_id}/reports/capital-gains?start=2025-01-01T00:00:00Z&end=2025-12-31T23:59:59Z" \
  -o capital-gains-2025.csv

Journal entries CSV:

curl -s -H "Authorization: Bearer $TOKEN" \
  "$CLAMS_BASE_URL/v1/workspaces/{workspace_id}/profiles/{profile_id}/reports/journal-entries" \
  -o journal-entries.csv

Tooling Tips

Readable output: Pipe everything through jq . for formatted JSON.

API docs: Browse the full API reference at clams.tech/docs/api and the auth API at clams.tech/docs/auth.

Postman: Import both OpenAPI specs directly into Postman to get the full collection for each API. Just paste these URLs into Postman's import dialog:

  • https://clams.tech/docs/auth/openapi.json
  • https://clams.tech/docs/api/openapi.json

Error format: Errors return JSON with a code, message, and optionally details and request_id.

Rate limiting: The self-hosted server has concurrency limits (default 128 concurrent requests) and request timeouts (default 30s), both configurable via environment variables. CSV exports are limited to 2 concurrent downloads.

Go Build Something

This is a v1 beta. We're actively improving the API based on what people build with it. If you run into issues or want to share what you're working on, use the built-in feedback endpoint (POST /v1/feedback) or reach out at support@clams.tech.

Clams Team

Stay in the loop

Get updates on new features and guides delivered to your inbox.