# SubPrune API Reference

The SubPrune API is a Hono service. On the public web origin it is proxied at `/backend`, so a browser or agent can call it same-origin: `https://subprune.com/backend/…`. CORS is restricted to the web app origins; the proxy is the supported path.

All JSON error responses share one shape: `{"error": "…", "code": "…", "details": …}`.

## Session endpoints (Better Auth)

- `POST /api/auth/sign-up/email` — Create an account with email and password (Better Auth).
  - Response: 200 session created
- `POST /api/auth/sign-in/email` — Sign in with email and password (Better Auth).
  - Response: 200 session created
- `POST /api/auth/sign-in/social` — Sign in with Google (available when Google credentials are configured).
  - Response: 200 session created
- `GET /api/auth/get-session` — Return the current session, if any (Better Auth).
  - Response: 200 `{"session":…,"user":…}` or `{"session":null,"user":null}`
- `POST /api/auth/sign-out` — End the session (Better Auth).
  - Response: 200 signed out

## Product endpoints

### GET `/health`

Readiness probe. Reports the names (never values) of missing env vars.

- Auth: none
- Response: 200 `{"status":"ok"}` or 503 `{"status":"degraded","missingEnv":["…"]}`

### GET `/`

Service identity.

- Auth: none
- Response: 200 `{"status":"ok","service":"subprune-api"}`

### POST `/detect`

Run Detection on mapped Statement rows and return Subscription candidates. Rows are never persisted. Send `Accept: application/x-ndjson` to stream one JSON line per finished stage instead.

- Auth: session cookie (Requires a valid Better Auth session cookie. Sign in through the web app, or call the Better Auth endpoints below; unauthenticated calls return 401.)
- Request: `{"rows":[{"date":"2026-01-03","description":"Netflix","amount":15.49}]}`
- Response: 200 `{"subscriptions":[{"merchant_name":"Netflix","amount":15.49,"frequency":"monthly","next_renewal_date":"2026-02-03","source":"llm"}],"row_categories":[{"row_index":1,"category_name":"Groceries"}]}`
- Errors: 400 invalid body; 401 unauthenticated; 503 Detection provider unavailable

### POST `/statements/extract`

Extract a Statement (CSV or text-backed PDF, multipart `file` field, in-memory only). The file and its rows are never stored.

- Auth: session cookie (Requires a valid Better Auth session cookie. Sign in through the web app, or call the Better Auth endpoints below; unauthenticated calls return 401.)
- Request: multipart/form-data with a `file` field (CSV or PDF, ≤ 4.5 MB)
- Response: 200 `{"ok":true,"file":{…}}` or `{"ok":false,"reason":"…","fileName":"…","message":"…"}`
- Errors: 400 unsupported format; 401 unauthenticated; 429 rate limited (hourly)

### GET `/subscriptions`

List the Consumer's active Subscriptions with their plan.

- Auth: session cookie (Requires a valid Better Auth session cookie. Sign in through the web app, or call the Better Auth endpoints below; unauthenticated calls return 401.)
- Response: 200 `{"plan":"free"|"unlimited","subscriptions":[{…}]}`
- Errors: 401 unauthenticated

### POST `/subscriptions`

Manually add one Subscription. Alerts always start off.

- Auth: session cookie (Requires a valid Better Auth session cookie. Sign in through the web app, or call the Better Auth endpoints below; unauthenticated calls return 401.)
- Request: `{"merchant_name":"…","amount":9.99,"frequency":"monthly","next_renewal_date":"2026-03-01"}`
- Response: 201 `{"subscription":{…}}`
- Errors: 400 invalid body; 401 unauthenticated

### POST `/scan/save`

Atomically save confirmed Income, reviewed Subscriptions (Alerts off), and leftover Transactions. Never accepts Detection rows.

- Auth: session cookie (Requires a valid Better Auth session cookie. Sign in through the web app, or call the Better Auth endpoints below; unauthenticated calls return 401.)
- Request: `{"idempotency_key":"…","income":{"action":"set","monthly_income":4800},"subscriptions":[{…}],"transactions":[{…}]}`
- Response: 201 `{"subscriptions":[{…}],"transactions":[{…}],"income_updated":true,"budget":{…}}`
- Errors: 400 invalid body or rows payload; 401 unauthenticated

### POST `/subscriptions/commit`

Save every reviewed candidate after authentication. Never accepts Detection `rows`; new Subscriptions start with Alerts off.

- Auth: session cookie (Requires a valid Better Auth session cookie. Sign in through the web app, or call the Better Auth endpoints below; unauthenticated calls return 401.)
- Request: `{"subscriptions":[{"merchant_name":"…","amount":9.99,"frequency":"monthly","next_renewal_date":"2026-03-01"}]}`
- Response: 201 `{"subscriptions":[{…}]}`
- Errors: 400 invalid body or `rows` field; 401 unauthenticated

### POST `/subscriptions/cancellation-plan`

Generate an ephemeral Cancellation plan (with a support script) for one saved Subscription. The plan is returned only and never persisted.

- Auth: session cookie (Requires a valid Better Auth session cookie. Sign in through the web app, or call the Better Auth endpoints below; unauthenticated calls return 401.)
- Request: `{"subscription_id":"<uuid>"}`
- Response: 200 `{"plan":{…}}`
- Errors: 400 invalid body; 401 unauthenticated; 404 subscription not found; 429 rate limited (hourly)

### GET `/budget/recommendations`

Return ephemeral AI-powered Budget recommendations for the selected horizon. Each item is an in-product next step plus evidence from Remaining, Category limits, and Subscriptions. Recommendations are never persisted.

- Auth: session cookie (Requires a valid Better Auth session cookie. Sign in through the web app, or call the Better Auth endpoints below; unauthenticated calls return 401.)
- Response: 200 `{"recommendations":[{"kind":"remaining","text":"Look at leftover spending — Remaining is $120.00 this month.","evidence":"Remaining is $120.00 this month after Subscriptions and other spending."}]}`
- Errors: 400 invalid query; 401 unauthenticated; 402 quarter/year or prior-month on free

### PATCH `/budget/categories/:id`

Rename a Category or change its optional monthly limit.

- Auth: session cookie (Requires a valid Better Auth session cookie. Sign in through the web app, or call the Better Auth endpoints below; unauthenticated calls return 401.)
- Request: `{"name":"Groceries","monthly_limit":400}` (partial; `monthly_limit` may be null)
- Response: 200 `{"category":{…}}`
- Errors: 400 invalid id or body; 401 unauthenticated; 404 not found

### DELETE `/budget/categories/:id`

Delete a Category. Transactions that used it keep their amounts and have no Category.

- Auth: session cookie (Requires a valid Better Auth session cookie. Sign in through the web app, or call the Better Auth endpoints below; unauthenticated calls return 401.)
- Response: 200 `{"ok":true}`
- Errors: 400 invalid id; 401 unauthenticated; 404 not found

### POST `/budget/transactions/:id/as-subscription`

Identify a leftover outflow Transaction as a Subscription. Creates the Subscription with Alerts off and deletes the Transaction so Remaining does not count it twice.

- Auth: session cookie (Requires a valid Better Auth session cookie. Sign in through the web app, or call the Better Auth endpoints below; unauthenticated calls return 401.)
- Request: `{"merchant_name":"Netflix","frequency":"monthly","next_renewal_date":"2026-09-16"}` (`merchant_name` optional; defaults to the Transaction description)
- Response: 200 `{"budget":{…},"subscription":{…}}`
- Errors: 400 invalid id, body, or inflow; 401 unauthenticated; 404 not found; 409 already counted in Subscriptions

### PATCH `/subscriptions/:id`

Update amount, frequency, next renewal date, alert toggle, or soft-delete (`is_active`). Enforces the free monitored cap when enabling Alerts.

- Auth: session cookie (Requires a valid Better Auth session cookie. Sign in through the web app, or call the Better Auth endpoints below; unauthenticated calls return 401.)
- Request: `{"alert_enabled":true}` (partial)
- Response: 200 `{"subscription":{…}}`
- Errors: 400 invalid id or body; 401 unauthenticated; 402 free monitored cap exceeded; 404 not found

### PATCH `/settings`

Update the Consumer's Alert channel settings (email/SMS toggles, phone).

- Auth: session cookie (Requires a valid Better Auth session cookie. Sign in through the web app, or call the Better Auth endpoints below; unauthenticated calls return 401.)
- Request: `{"alert_email_enabled":true,"alert_sms_enabled":false,"alert_phone":null}`
- Response: 200 updated settings
- Errors: 400 invalid body; 401 unauthenticated

### POST `/billing/checkout`

Create a Stripe Checkout Session for the recurring Premium plan ($7/month).

- Auth: session cookie (Requires a valid Better Auth session cookie. Sign in through the web app, or call the Better Auth endpoints below; unauthenticated calls return 401.)
- Response: 200 `{"url":"https://checkout.stripe.com/…"}`
- Errors: 400 invalid body; 401 unauthenticated; 500 Stripe misconfigured

### POST `/webhooks/stripe`

Stripe webhook: grant Premium while a paid subscription is in good standing, revert to free when it lapses. Signature-verified.

- Auth: shared secret header (see the endpoint)
- Request: Stripe event payload with a `Stripe-Signature` header
- Response: 200 acknowledged
- Errors: 400 missing/invalid signature

### GET `/cron/alerts`

Daily Alert worker (Vercel Cron at 00:00 UTC). Sends email Alerts for renewals 3 days out.

- Auth: shared secret header (see the endpoint)
- Request: Authorization: `Bearer <CRON_SECRET>`
- Response: 200 `{…}` alert run summary
- Errors: 401 missing/invalid secret

## Notes

- Statements and Detection `rows` are never persisted — the API extracts in memory and returns candidates only.
- New Subscriptions always start with Alerts off; the free plan monitors 1 renewal, Premium ($7/month) is unlimited.
- Alerts are email-only, sent 3 days before the renewal; the daily worker runs at 00:00 UTC.
- More context: [Developer docs](https://subprune.com/docs), [llms.txt](https://subprune.com/llms.txt).
