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
statusfield.
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
| Status | Meaning |
|---|---|
| 400 | Validation failed. Inspect data.fields. |
| 401 | Missing, malformed, or expired bearer token. |
| 402 | Trial expired and no active subscription. Write endpoints only. |
| 404 | Resource doesn't exist, or doesn't belong to you. |
| 405 | HTTP method not allowed for this endpoint. |
| 409 | Conflict (duplicate username on signup, duplicate waitlist email). |
| 500 | Unhandled 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
| Method | Path | Purpose |
|---|---|---|
| GET | /api/cards | List your cards (newest first). |
| GET | /api/cards/summary | Derived totals (total annual fees, total points, total cashback). |
| GET | /api/cards/:id | Single card. |
| POST | /api/cards | Create a card. Required: displayName, issuer, rewardType (points or cashback). Optional: cardKey (enables catalog seeding), pointValueCents, annualFee, signupBonus. |
| POST | /api/cards/:id/signup-bonus | Body: { action: "spend" | "apply", amount? }. Updates sign-up bonus progress. |
| PUT | /api/cards/:id | Partial update. |
| DELETE | /api/cards/:id | Delete the card and all its transactions. |
Transactions
| Method | Path | Purpose |
|---|---|---|
| GET | /api/transactions | List 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/transactions | Record 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/:id | Edit type, amount, or note. Card balance is reconciled. |
| DELETE | /api/transactions/:id | Delete 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.
| Method | Path | Purpose |
|---|---|---|
| GET | /api/perks | List perks. Filters: cardId (or none for unattached), year, status (open | used | all). |
| GET | /api/perks/catalog | Full perks catalog. |
| GET | /api/perks/catalog/:cardKey | Catalog perks for one card. |
| POST | /api/perks | Create a perk. Recurring periods (monthly, quarterly, semi-annual) auto-expand into N rows when year is set and deadline isn't. |
| POST | /api/perks/seed | Body: { cardId, year }. Expand the card's catalog perks for that year. |
| POST | /api/perks/rollover | Roll open perks into a new year. |
| PUT | /api/perks/:id | Partial update, including used / usedAt. |
| DELETE | /api/perks/:id | Delete one perk. |
Offers
An offer is a merchant-specific limited-time deal (Amex Offers, Chase Offers, BoA Deals).
| Method | Path | Purpose |
|---|---|---|
| GET | /api/offers | List offers. Filters: cardId, category, status (active | expiring | used | expired | all), search (merchant substring). |
| GET | /api/offers/catalog | Full offers catalog. |
| GET | /api/offers/catalog/:cardKey | Catalog offers for one card. |
| POST | /api/offers | Create a manual offer. |
| POST | /api/offers/seed | Body: { cardId, replace? }. Seed catalog offers for one card. |
| POST | /api/offers/seed-all | Seed catalog offers for every tracked card with a cardKey. |
| POST | /api/offers/bulk | Bulk upsert (used by the Chrome extension scraper). |
| POST | /api/offers/:id/used | Body: { used }. Toggle claimed status. |
| PUT | /api/offers/:id | Partial update. |
| DELETE | /api/offers/:id | Delete an offer. |
Billing
| Method | Path | Purpose |
|---|---|---|
| GET | /api/billing/status | Effective subscription state: trialing | active | past_due | expired, plus plan, trialEndsAt, currentPeriodEnd, cancelAtPeriodEnd, cancelAt. Best-effort live sync with Stripe. |
| POST | /api/billing/checkout | Body: { plan: "monthly" | "annual" }. Returns { url }, a Stripe Checkout session URL. |
| POST | /api/billing/portal | Returns { 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.