For your own data
WealthTrackr Personal Data API
Read-only REST API for accessing your personal financial data.
Overview
The WealthTrackr Personal Data API exposes your financial data as JSON over HTTPS. All endpoints require authentication via a personal API key. Create and revoke keys from Settings → Personal API access in the app. The full wtk_… key is shown once at creation; store it in a secret manager and never commit it.
Base URL
https://server-production-c1e82.up.railway.appAuthentication
Send your API key using either header:
Authorization: Bearer wtk_your_api_key_hereor
X-API-Key: wtk_your_api_key_hereA missing, malformed, or revoked key returns 401 Unauthorized. Keys are read-only unless you create one with write access, which can add, edit and delete expenses but nothing else. Revocation is immediate.
Example Request
curl --fail --silent --show-error \
-H "Authorization: Bearer $WEALTHTRACKR_API_KEY" \
https://server-production-c1e82.up.railway.app/v1/data/metricsEndpoints
/v1/data/summaryNet worth, asset/liability totals and counts, latest snapshot, and as_of timestamp.
Response (200 OK)
{
"net_worth": 128400.00,
"total_assets": 145000.00,
"total_liabilities": 16600.00,
"asset_count": 5,
"liability_count": 2,
"latest_snapshot": {
"id": "uuid",
"month": "2026-08",
"net_worth": 128400.00,
"total_assets": 145000.00,
"total_liabilities": 16600.00,
"category_breakdown": { "bank_accounts": 50000.00 },
"notes": null,
"timestamp": "2026-08-01T12:00:00Z"
},
"as_of": "2026-09-01T12:00:00Z"
}/v1/data/itemsAccount categories and items, including ownership-adjusted effective_value.
Parameters
typestring (optional)Filter by account type. One of "asset" or "liability".Response (200 OK)
{
"categories": [
{
"id": "bank_accounts",
"title": "Bank Accounts",
"type": "asset",
"total": 50000.00,
"items": [
{
"id": "uuid",
"label": "Checking",
"value": 5000.00,
"ownership_percentage": 100,
"effective_value": 5000.00,
"liquidity_rating": "high",
"notes": null,
"last_updated": "2026-08-01T12:00:00Z"
}
]
}
]
}/v1/data/snapshotsMonthly net-worth snapshots, newest first.
Parameters
limitinteger (optional)Number of snapshots to return. Must be between 1 and 1000. Defaults to all.sinceYYYY-MM (optional)Only snapshots for this month or later.Response (200 OK)
{
"snapshots": [
{
"id": "uuid",
"month": "2026-08",
"net_worth": 128400.00,
"total_assets": 145000.00,
"total_liabilities": 16600.00,
"category_breakdown": { "bank_accounts": 50000.00 },
"notes": null,
"timestamp": "2026-08-01T12:00:00Z"
}
]
}/v1/data/metricsMonthly check-in history with derived financial metrics. Credit score and each monthly money value are independently optional; every record contains at least one score or money value. Calculated fields are null when their required inputs were not recorded.
Response (200 OK)
{
"latest": {
"month": "2026-08",
"credit_score": null,
"monthly_income": 7500.00,
"monthly_expenses": 4000.00,
"monthly_debt_payments": null,
"monthly_savings": 1500.00,
"retirement_savings": null,
"debt_to_income_ratio": null,
"monthly_cash_flow": 3500.00,
"savings_rate": 20.0,
"updated_at": "2026-08-01T12:00:00Z"
},
"history": [/* same shape, newest-first */]
}/v1/data/expensesExpense ledger, newest first. Deleted records are never returned. Amounts stay in each record's own currency.
Parameters
start / endYYYY-MM-DD (optional)Inclusive date range.categorystring (optional)Case-insensitive category match.currencyISO 4217 (optional)Only this currency, e.g. USD.updated_sinceISO 8601 (optional)Only records changed after this time, for incremental sync.limit / offsetinteger (optional)Pagination. limit 1–1000, default 100.Response (200 OK)
{
"expenses": [
{
"id": "uuid",
"description": "Groceries",
"amount": 54.20,
"currency": "USD",
"category": "Food",
"date": "2026-09-28",
"notes": null,
"updated_at": "2026-09-28T18:02:11Z"
}
],
"total": 212,
"limit": 100,
"offset": 0,
"has_more": true
}/v1/data/expenses/summarySpending totals by currency, month and category. Currencies are never mixed.
Parameters
start / endYYYY-MM-DD (optional)Inclusive date range.Response (200 OK)
{
"count": 212,
"totals": [{ "currency": "USD", "total": 8123.55 }],
"by_month": [{ "currency": "USD", "month": "2026-09", "total": 2310.10 }],
"by_category": [{ "currency": "USD", "category": "Food", "total": 1204.33 }]
}/v1/data/incomeOne-off and recurring income records, newest first.
Parameters
limit / offsetinteger (optional)Pagination. limit 1–1000, default 100.Response (200 OK)
{
"income": [
{
"id": "uuid",
"description": "Salary",
"amount": 4200.00,
"currency": "USD",
"category": "Salary",
"date": null,
"recurring": "monthly",
"start_date": "2026-01-01",
"end_date": null,
"notes": null,
"updated_at": "2026-01-01T09:00:00Z"
}
],
"total": 1,
"limit": 100,
"offset": 0,
"has_more": false
}/v1/data/goalsFinancial goals as the apps store them (camelCase fields) plus updated_at.
Response (200 OK)
{ "goals": [{ "id": "uuid", "title": "Emergency fund", "updated_at": "2026-09-01T00:00:00Z" }] }/v1/data/expensesCreate an expense. Requires a write-scoped API key (read-only keys get 403). Appears on every device at its next sync. Send your own id to make retries safe: a repeat returns 409.
Parameters
descriptionstring, 1–80What the expense was for.amountnumber > 0In the currency's own precision (2 decimals; none for JPY).currencyISO 4217USD, EUR, GBP, JPY, CAD, AUD, CHF, CNY, INR or MXN.categorystring, 1–40Any category label, e.g. Food.dateYYYY-MM-DDWhen it happened.notes / idoptionalNotes up to 240 characters; a client-chosen id up to 128.Response (200 OK)
201 {
"id": "uuid",
"description": "Coffee",
"amount": 4.50,
"currency": "USD",
"category": "Food",
"date": "2026-03-10",
"notes": null,
"updated_at": "2026-03-10T14:02:11.250Z"
}/v1/data/expenses/{expense_id}Replace an existing expense's fields. Requires a write-scoped API key. Returns 404 if it does not exist.
Response (200 OK)
200 { ...the saved expense, same shape as POST }/v1/data/expenses/{expense_id}Delete an expense. Requires a write-scoped API key. A tombstone is kept so other devices cannot bring it back.
Response (200 OK)
204 (no body)Webhooks
Add an HTTPS endpoint in Settings → Webhooks and WealthTrackr will POST a signed JSON event whenever an expense is created, updated or deleted — from any app or from the API. Events: expense.created, expense.updated, expense.deleted. Use Send test to receive a webhook.test event.
POST https://your-server.example.com/wealthtrackr
WealthTrackr-Event: expense.created
WealthTrackr-Delivery: 3f6c0b9e-… (unique per delivery)
WealthTrackr-Signature: t=1791696424,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
{
"id": "evt_9b2f…",
"type": "expense.created",
"created_at": "2026-10-11T05:27:04.433Z",
"data": {
"expense": {
"id": "uuid", "description": "Coffee", "amount": 4.5, "currency": "USD",
"category": "Food", "date": "2026-10-11", "notes": null, "updated_at": "2026-10-11T05:27:04.433Z"
}
}
}Verify every request
The signature is an HMAC-SHA256 of "<t>.<raw body>" using the signing secret shown once when you create the webhook. Reject requests whose signature does not match or whose timestamp is more than a few minutes old.
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody must be the exact bytes received (don't re-serialize parsed JSON).
export function verify(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(signatureHeader.split(",").map((p) => p.split("=")));
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > toleranceSeconds) return false; // replay protection
const expected = createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
return parts.v1.length === expected.length && timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
}import hashlib, hmac, time
def verify(raw_body: bytes, signature_header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in signature_header.split(","))
if abs(time.time() - int(parts["t"])) > tolerance: # replay protection
return False
expected = hmac.new(secret.encode(), parts["t"].encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(parts["v1"], expected)Delivery rules
- Respond with any
2xxwithin 8 seconds. Anything else (or a timeout) is retried after 30 s, 2 min, 10 min, 1 h and 6 h — six attempts in all. Handle duplicates by de-duplicating on the eventid. - After 15 consecutive failed attempts the endpoint is switched off; turn it back on in Settings.
- Endpoints must be public
https://host names. Private, loopback and link-local addresses, IP literals and redirects are refused; the host is re-checked on every delivery. - Delivery order is not guaranteed. Use
updated_atto pick the newest version of an expense. - Up to 5 webhooks per account.
Response Format
All responses use JSON with snake_case field names. Monetary values are returned as floating-point numbers in USD. The /v1/data/metrics endpoint returns the newest entry in latest and all entries newest-first in history; calculated fields (debt_to_income_ratio, monthly_cash_flow, savings_rate) are null when their required inputs were not recorded.
Errors
401Missing, malformed, or revoked API key.422Validation error (e.g. invalid query parameter).429Rate limit exceeded.500Internal server error.Rate Limiting
API endpoints are rate-limited to prevent abuse. If you exceed the limit, you will receive a 429 response. Retry with exponential backoff.
Machine-Readable Spec
An OpenAPI 3.1 specification is available at /openapi.json for code generation and tooling integration. An AI-friendly summary is available at /llms.txt.
Support
GitHub: apercallc/wealthtrackr · Email: security@apercallc.com