← All docs REST API

The same API the app uses.

Every screen in Finstracker is a thin wrapper around this JSON API. If the web app can do it, you can script it.

Overview

The Finstracker REST API is a small, predictable JSON surface. Every endpoint:

  • Accepts and returns JSON.
  • Wraps responses in the uniform RO envelope.
  • Uses JWT bearer auth on user-scoped routes (catalog and waitlist are public).
  • Returns HTTP status codes that match the envelope's status field.

Base URL

https://finstracker.com

For local development:

http://127.0.0.1:3000

Response envelope

Every response uses the same shape, success or failure:

{
  "status":  200,
  "message": "Cards retrieved",
  "data":    { /* endpoint-specific payload, or null on error */ }
}

Validation errors include a fields object on data:

{
  "status":  400,
  "message": "Validation failed",
  "data":    { "fields": { "displayName": "Title required (1-80 chars)" } }
}

Authentication

Hit POST /api/auth/login with { username, password }. Read data.token from the response. Send it as Authorization: Bearer <jwt> on every protected route. Tokens are HS256-signed JWTs valid for 7 days.

Example flow:

# 1. Sign up
curl -X POST https://finstracker.com/api/auth/signup \
  -H 'Content-Type: application/json' \
  -d '{"username":"jane","password":"hunter2hunter2"}'

# 2. Login (or reuse the token returned by signup)
TOKEN=$(curl -sS -X POST https://finstracker.com/api/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"jane","password":"hunter2hunter2"}' \
  | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['token'])")

# 3. Use it
curl -H "Authorization: Bearer $TOKEN" https://finstracker.com/api/cards

Trial & paywall

New accounts get a 7-day trial. During and after the trial, read endpoints stay open so you can always see your data. Write endpoints (POST/PUT/DELETE on cards, transactions, perks, offers) return 402 Payment Required once the trial ends if there's no active subscription. The 402 envelope includes data.subscription with the current state and a data.paywall: true flag so the UI knows to show the upgrade screen.

Errors

StatusMeaning
400Validation failed. Inspect data.fields.
401Missing, malformed, or expired bearer token.
402Trial expired and no active subscription. Write endpoints only.
404Resource doesn't exist, or doesn't belong to you.
405HTTP method not allowed for this endpoint.
409Conflict (duplicate username on signup, duplicate waitlist email).
500Unhandled server error. We log these, so please tell us if you hit one.

Auth

Sign up

POST /api/auth/signup · public

Body: { username, password, email? }. Returns the new user, an immediate token, and the starting subscription (trialing).

Login

POST /api/auth/login · public

Body: { username, password }. Returns { token, user, subscription }.

Google sign-in

POST /api/auth/google · public

Body: { idToken } from Google Identity Services. Creates or links a user and returns the same { token, user, subscription } envelope.

Forgot / reset password

POST /api/auth/forgot and POST /api/auth/reset · public

Standard email-based reset flow. forgot accepts { username | email }; reset accepts { token, newPassword }.

Catalog

List preset cards

GET /api/catalog/cards · public

Returns the static card catalog (issuer, name, default reward type, default point value, annual fee). Used by the Add Card flow to auto-seed perks and offers.

Cards

MethodPathPurpose
GET/api/cardsList your cards (newest first).
GET/api/cards/summaryDerived totals (total annual fees, total points, total cashback).
GET/api/cards/:idSingle card.
POST/api/cardsCreate a card. Required: displayName, issuer, rewardType (points or cashback). Optional: cardKey (enables catalog seeding), pointValueCents, annualFee, signupBonus.
POST/api/cards/:id/signup-bonusBody: { action: "spend" | "apply", amount? }. Updates sign-up bonus progress.
PUT/api/cards/:idPartial update.
DELETE/api/cards/:idDelete the card and all its transactions.

Transactions

MethodPathPurpose
GET/api/transactionsList transactions with optional filters: cardId, cardIds (comma-separated), from/to ISO dates, type, unit, min/max amount, q note search, limit (default 200, max 1000), skip.
POST/api/transactionsRecord an earn or redeem. Body: { cardId, type: "earn"|"redeem", unit: "points"|"cashback", amount, note?, bonusSpend? }. Card balance is updated atomically; bonusSpend bumps sign-up bonus progress and auto-credits the bonus if the threshold is crossed.
PUT/api/transactions/:idEdit type, amount, or note. Card balance is reconciled.
DELETE/api/transactions/:idDelete one transaction. Card balance is reversed.
DELETE/api/transactions?cardId=...Bulk-delete every transaction for one card.

Perks

A perk is a dated statement-credit checklist item (monthly Uber Cash, annual hotel credit, etc.). Recurring perks expand into one row per period.

MethodPathPurpose
GET/api/perksList perks. Filters: cardId (or none for unattached), year, status (open | used | all).
GET/api/perks/catalogFull perks catalog.
GET/api/perks/catalog/:cardKeyCatalog perks for one card.
POST/api/perksCreate a perk. Recurring periods (monthly, quarterly, semi-annual) auto-expand into N rows when year is set and deadline isn't.
POST/api/perks/seedBody: { cardId, year }. Expand the card's catalog perks for that year.
POST/api/perks/rolloverRoll open perks into a new year.
PUT/api/perks/:idPartial update, including used / usedAt.
DELETE/api/perks/:idDelete one perk.

Offers

An offer is a merchant-specific limited-time deal (Amex Offers, Chase Offers, BoA Deals).

MethodPathPurpose
GET/api/offersList offers. Filters: cardId, category, status (active | expiring | used | expired | all), search (merchant substring).
GET/api/offers/catalogFull offers catalog.
GET/api/offers/catalog/:cardKeyCatalog offers for one card.
POST/api/offersCreate a manual offer.
POST/api/offers/seedBody: { cardId, replace? }. Seed catalog offers for one card.
POST/api/offers/seed-allSeed catalog offers for every tracked card with a cardKey.
POST/api/offers/bulkBulk upsert (used by the Chrome extension scraper).
POST/api/offers/:id/usedBody: { used }. Toggle claimed status.
PUT/api/offers/:idPartial update.
DELETE/api/offers/:idDelete an offer.

Billing

MethodPathPurpose
GET/api/billing/statusEffective subscription state: trialing | active | past_due | expired, plus plan, trialEndsAt, currentPeriodEnd, cancelAtPeriodEnd, cancelAt. Best-effort live sync with Stripe.
POST/api/billing/checkoutBody: { plan: "monthly" | "annual" }. Returns { url }, a Stripe Checkout session URL.
POST/api/billing/portalReturns { url }, a Stripe Customer Portal URL for self-serve plan changes.

Waitlist

POST /api/waitlist · public

Body: { email, name?, source? }. Returns 201 on first signup, 409 if the email is already on the list.

Stripe webhook

POST /api/stripe-webhook · Stripe-signed (raw body)

Consumes Stripe events to keep subscriptionStatus, currentPeriodEnd, and cancelAtPeriodEnd in sync. Not for client use.

Object shapes

Card

{
  "id": "string",
  "cardKey": "chase-sapphire-preferred | null",
  "displayName": "Sapphire Preferred",
  "issuer": "Chase",
  "rewardType": "points | cashback",
  "currentPoints": 84210,
  "currentCashBack": 0,
  "pointValueCents": 1.25,
  "annualFee": 95,
  "feeAnniversaryMonth": 9, "feeAnniversaryDay": 12,
  "signupBonus": { "spendRequired": 4000, "currentSpend": 4200, "bonusAmount": 75000, "bonusUnit": "points", "deadline": "ISO", "startDate": "ISO", "completed": true, "applied": true } | null,
  "createdAt": "ISO", "updatedAt": "ISO"
}

Transaction

{
  "id": "string", "cardId": "string",
  "type": "earn | redeem",
  "unit": "points | cashback",
  "amount": 4250,
  "note": "Groceries",
  "createdAt": "ISO"
}

Perk

{
  "id": "string", "cardId": "string | null", "cardKey": "string | null",
  "title": "Uber Cash (Jan)",
  "valueCents": 1500, "capLabel": "$15/mo",
  "period": "monthly | quarterly | semi-annual | annual | anniversary | one-time | custom",
  "year": 2026,
  "deadline": "ISO | null",
  "used": false, "usedAt": "ISO | null",
  "notes": ""
}

Offer

{
  "id": "string", "cardId": "string | null", "cardKey": "string | null",
  "offerKey": "sha1 dedupe key",
  "merchant": "Hilton",
  "title": "$15 back on $75+",
  "category": "dining | travel | streaming | retail | grocery | gas | entertainment | other",
  "valueCents": 1500, "minSpendCents": 7500,
  "discountType": "statement_credit | pct_back | bonus_points",
  "discountValue": 1500,
  "expiresAt": "ISO | null",
  "used": false, "source": "catalog | manual | extension",
  "merchantUrl": "https://..."
}

Rate limits & CORS

No hard rate limits today. CORS is open for browser clients; the server reflects the Origin header on cross-origin responses. If you're building something high-volume, please get in touch first.

Changelog

API changes are noted on the public changelog. The API has been stable since v1.0; current version bumps are additive.

Want LLM-friendly access too?

Same data, accessible through the Model Context Protocol.

See the MCP docs