WealthTrackr

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.app

Authentication

Send your API key using either header:

Authorization: Bearer wtk_your_api_key_here

or

X-API-Key: wtk_your_api_key_here

A 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/metrics

Endpoints

GET/v1/data/summary

Net 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"
}
GET/v1/data/items

Account 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"
        }
      ]
    }
  ]
}
GET/v1/data/snapshots

Monthly 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"
    }
  ]
}
GET/v1/data/metrics

Monthly 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 */]
}
GET/v1/data/expenses

Expense 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
}
GET/v1/data/expenses/summary

Spending 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 }]
}
GET/v1/data/income

One-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
}
GET/v1/data/goals

Financial 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" }] }
POST/v1/data/expenses

Create 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"
}
PUT/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 }
DELETE/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

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

© 2026 Apercall LLC · Privacy · Terms

Personal Data API documentation | WealthTrackr