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
429with aRetry-Afterheader (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 (withApiTokeninsrc/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 (
currencyin each response).balance(accounts) andoriginalAmount(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" } }
| Status | code | When |
|---|---|---|
| 400 | invalid_request | A query parameter is missing or invalid (the message names the field) |
| 401 | unauthorized | Missing, malformed, unknown, or expired token (with WWW-Authenticate: Bearer realm="cashlytics") |
| 404 | not_found | The token's user no longer exists |
| 429 | rate_limited | More than 120 requests per minute |
| 500 | internal_error | Unexpected 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.
| Parameter | Default | Range | Meaning |
|---|---|---|---|
upcomingDays | 7 | 1 to 90 | Window for upcoming |
forecastMonths | 3 | 1 to 24 | Months 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.cashflowcounts what is actually due this month.month.reservespreads non-monthly items over the months. These are the two dashboard views.surplusis income minus expenses.*ChangePctis the change against the previous month, in percent with one decimal.recurringsums the smoothed monthly amount of recurring expenses active this month.upcominglists the next due date of each recurring expense and planned one-time expenses. Incomes are not included.nextisnullif nothing is due.forecastuses the reserve view, like the analytics page.insightscounts unread, unexpired insights (at most 50 are counted).
GET /api/v1/accounts
| Parameter | Default | Range | Meaning |
|---|---|---|---|
forecastMonths | 3 | 1 to 24 | Months 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
}
]
}
}
]
}
balanceis 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,balanceper month,projectedBalanceat the end). Checking accounts only have anetper month;balanceandprojectedBalancearenull.
GET /api/v1/payments
Every due date of expenses, one-time expenses, and incomes in a range, for example for a calendar.
| Parameter | Required | Meaning |
|---|---|---|
start | yes | First day, YYYY-MM-DD |
end | yes | Last 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
| Piece | File |
|---|---|
| Token generation and lookup | src/lib/auth/api-token.ts |
| Token table | api_tokens in src/lib/db/schema.ts |
| Token management | src/actions/api-token-actions.ts, src/components/settings/api-token-settings.tsx |
| Auth, rate limit, errors | src/lib/api/v1.ts |
| Query validation | src/lib/validations/api.ts |
| Calculations | src/lib/services/* (shared with the Server Actions) |
| Routes | src/app/api/v1/*/route.ts |
The upstream reference is docs/api/rest-v1.md.