Skip to main content

REST API v1

Cashlytics offers a read-only JSON API under /api/v1 for integrations such as Home Assistant. It is separate from the Server Actions the UI uses (see API and Server Actions).

Versioning​

The version is part of the path (/api/v1). The integer apiVersion (currently 1) is returned by /health and /me and is increased on breaking changes to response shapes. The app version is returned as version.

Authentication​

Create a personal API token in Settings, API tokens & integrations (see Settings). The token is csk_ followed by 64 hex characters, is shown once, and only its SHA-256 hash is stored (table api_tokens). Send it as a bearer token:

Authorization: Bearer csk_...
  • A token belongs to one user and only reads that user's data.
  • The scope is read; there are no write endpoints.
  • Tokens expire after 30, 90, or 365 days, or never. Revoking a token takes effect on the next request.
  • Each token may make 120 requests per minute. Beyond that the API answers 429 with a Retry-After header (seconds). The limit is in-memory, per server process.
  • The routes are exempt from the session check in src/proxy.ts; each handler authenticates the token itself (withApiToken in src/lib/api/v1.ts).

GET /api/v1/health is the only endpoint without a token.

Conventions​

  • Amounts are numbers with two decimals in the user's base currency (currency in each response). balance (accounts) and originalAmount (payments) use the entry's own currency.
  • Calendar days are YYYY-MM-DD; timestamps are ISO 8601 in UTC.
  • All responses carry Cache-Control: no-store.

Errors​

Errors share one shape:

{ "error": { "code": "unauthorized", "message": "Missing, invalid or expired API token" } }
StatuscodeWhen
400invalid_requestA query parameter is missing or invalid (the message names the field)
401unauthorizedMissing, malformed, unknown, or expired token (with WWW-Authenticate: Bearer realm="cashlytics")
404not_foundThe token's user no longer exists
429rate_limitedMore than 120 requests per minute
500internal_errorUnexpected server error

Endpoints​

GET /api/v1/health​

Public. Use it to check that a URL points to a Cashlytics instance.

curl https://cashlytics.example.com/api/v1/health
{ "status": "ok", "version": "0.10.0", "apiVersion": 1 }

GET /api/v1/me​

The token owner. userId is stable and suitable as an integration's unique ID.

curl -H "Authorization: Bearer $CASHLYTICS_TOKEN" https://cashlytics.example.com/api/v1/me
{
"userId": "00000000-0000-0000-0000-000000000001",
"name": "Demo User",
"email": "demo@example.com",
"baseCurrency": "EUR",
"locale": "de",
"version": "0.10.0",
"apiVersion": 1
}

GET /api/v1/summary​

Everything an integration usually polls, in one request.

ParameterDefaultRangeMeaning
upcomingDays71 to 90Window for upcoming
forecastMonths31 to 24Months in forecast, starting with the current month
curl -H "Authorization: Bearer $CASHLYTICS_TOKEN" \
"https://cashlytics.example.com/api/v1/summary?upcomingDays=14&forecastMonths=6"
{
"generatedAt": "2026-10-15T13:21:32.717Z",
"currency": "EUR",
"totalAssets": 24496.05,
"hasMultiCurrency": false,
"month": {
"year": 2026,
"month": 10,
"cashflow": {
"income": 4150,
"expenses": 1663.28,
"surplus": 2486.72,
"incomeChangePct": 0,
"expensesChangePct": -29.6
},
"reserve": {
"income": 4151.56,
"expenses": 1663.28,
"surplus": 2488.28,
"incomeChangePct": 0,
"expensesChangePct": -29.6
}
},
"recurring": {
"fixedCostsMonthly": 1162.44,
"fixedCostsCount": 10,
"subscriptionsMonthly": 34.97,
"subscriptionsCount": 3
},
"upcoming": {
"days": 7,
"count": 10,
"total": 1162.44,
"next": {
"id": "...",
"type": "expense",
"name": "Rent",
"date": "2026-10-18",
"amount": 850,
"category": { "name": "Housing", "icon": "🏠", "color": "#3b82f6" },
"isSubscription": false
},
"payments": ["...same shape as next..."]
},
"forecast": {
"months": 3,
"projectedIncome": 12450,
"projectedExpenses": 4989.84,
"projectedBalance": 7460.16,
"monthly": [
{ "year": 2026, "month": 10, "income": 4150, "expenses": 1663.28, "balance": 2486.72 }
]
},
"insights": {
"unread": 2,
"bySeverity": { "warning": 2 },
"byType": { "spending_anomaly": 1, "budget_warning": 1 }
}
}
  • month.cashflow counts what is actually due this month. month.reserve spreads non-monthly items over the months. These are the two dashboard views. surplus is income minus expenses.
  • *ChangePct is the change against the previous month, in percent with one decimal.
  • recurring sums the smoothed monthly amount of recurring expenses active this month.
  • upcoming lists the next due date of each recurring expense and planned one-time expenses. Incomes are not included. next is null if nothing is due.
  • forecast uses the reserve view, like the analytics page.
  • insights counts unread, unexpired insights (at most 50 are counted).

GET /api/v1/accounts​

ParameterDefaultRangeMeaning
forecastMonths31 to 24Months projected, starting with next month
curl -H "Authorization: Bearer $CASHLYTICS_TOKEN" \
"https://cashlytics.example.com/api/v1/accounts?forecastMonths=2"
{
"currency": "EUR",
"accounts": [
{
"id": "...",
"name": "Emergency fund",
"type": "savings",
"currency": "EUR",
"balance": 12500,
"balanceInBaseCurrency": 12500,
"createdAt": "2026-01-01T00:00:00.000Z",
"forecast": {
"months": 2,
"isCumulative": true,
"projectedBalance": 13100,
"monthly": [
{
"year": 2026,
"month": 11,
"income": 0,
"expenses": 0,
"transfersIn": 300,
"transfersOut": 0,
"net": 300,
"balance": 12800
}
]
}
}
]
}
  • balance is the balance the user maintains in Cashlytics, in the account's currency. Cashlytics does not change it when entries are added.
  • The forecast uses the cashflow view in the base currency. Savings and ETF accounts carry their balance forward (isCumulative: true, balance per month, projectedBalance at the end). Checking accounts only have a net per month; balance and projectedBalance are null.

GET /api/v1/payments​

Every due date of expenses, one-time expenses, and incomes in a range, for example for a calendar.

ParameterRequiredMeaning
startyesFirst day, YYYY-MM-DD
endyesLast day (inclusive), not before start, and less than 366 days after it
curl -H "Authorization: Bearer $CASHLYTICS_TOKEN" \
"https://cashlytics.example.com/api/v1/payments?start=2026-10-01&end=2026-11-30"
{
"currency": "EUR",
"start": "2026-10-01",
"end": "2026-11-30",
"payments": [
{
"id": "...",
"type": "expense",
"name": "Netflix",
"date": "2026-10-15",
"amount": 13.99,
"originalAmount": 13.99,
"originalCurrency": "EUR",
"accountId": "...",
"category": { "name": "Subscriptions", "icon": "📺", "color": "#ef4444" },
"isSubscription": true
}
]
}

type is expense (recurring), daily_expense (single entry), or income. A recurring item appears once per due date. amount is in the base currency; originalAmount and originalCurrency are the entry's own.

GET /api/v1/insights​

Unread, unexpired insights, newest first, at most 50. Titles and messages are stored in German; use type, severity, and metadata for anything language independent.

curl -H "Authorization: Bearer $CASHLYTICS_TOKEN" https://cashlytics.example.com/api/v1/insights
{
"insights": [
{
"id": "...",
"type": "spending_anomaly",
"severity": "warning",
"title": "...",
"message": "...",
"metadata": { "categoryId": "..." },
"createdAt": "2026-10-15T06:00:00.000Z",
"expiresAt": "2026-11-01T00:00:00.000Z"
}
]
}

type is one of spending_anomaly, budget_warning, saving_opportunity, subscription_alert, trend_info. severity is info, warning, or critical. metadata and expiresAt can be null.

Implementation​

PieceFile
Token generation and lookupsrc/lib/auth/api-token.ts
Token tableapi_tokens in src/lib/db/schema.ts
Token managementsrc/actions/api-token-actions.ts, src/components/settings/api-token-settings.tsx
Auth, rate limit, errorssrc/lib/api/v1.ts
Query validationsrc/lib/validations/api.ts
Calculationssrc/lib/services/* (shared with the Server Actions)
Routessrc/app/api/v1/*/route.ts

The upstream reference is docs/api/rest-v1.md.