---
title: "List the authenticated user's price alerts"
method: GET
path: "/api/alerts"
tags: ["Alerts"]
---

# List the authenticated user's price alerts

`GET /api/alerts`

## Request

- Base URL: `https://www.oxfordledge.com` (the document declares no server; this is the origin it was published from)
- URL: `https://www.oxfordledge.com/api/alerts`
- Auth: none declared

Return the user's price alerts (all statuses, newest-first) plus
the quota banner inputs.

Auth: requires login. Anon → 401 from `require_auth` (NOT 402;
405 is reserved for tier gates per CLAUDE.md status-code conventions).

Response shape (REQUIRED fields per F4.3 boundary contract):
  - alerts: list of Alert dicts
  - count:  len(alerts)
  - quota_total:     tier-defined cap
  - quota_remaining: clamp(quota_total - count_active, 0, ...)

## Response `200`

Successful Response

- AlertsListResponse: `GET /api/alerts` response. Frontend list-view iterates `data.alerts.forEach(...)` for the per-row render and reads `data.quota_total` / `data.quota_remaining` for the upgrade-cta banner. All three fields are REQUIRED so a missing one would NPE the panel — exactly the 2026-04-26/04-27 silent-shadow class.
  - `alerts` Alert[], required: User's alerts, newest-first.
    - `id` integer, required: Primary key.
    - `user_id` integer, required: Owning user's id.
    - `ticker` string, required: Stock ticker (uppercased + stripped).
    - `condition_type` string, required: One of 'price_above', 'price_below', 'pct_move', 'volume_spike', 'earnings_day', 'ex_dividend'. PR1 ships only the price_above/price_below pair; the others are reserved for PR1+ schema-forward-compat.
    - `threshold` number, nullable: Numeric threshold (price level for price_above/below; % for pct_move; multiple of avg-vol for volume_spike). Null for calendar-driven types.
    - `note` string, nullable: User's optional 'why this price' note — PROFESSOR's educational hook surfaced in the PR2 trigger email body.
    - `status` string, required: One of 'active', 'triggered', 'cancelled', 'expired', 'paused_by_downgrade' (Q4 OWNER decision 2026-04-30).
    - `created_at` string, required: ISO-8601 timestamp.
    - `triggered_at` string, nullable: ISO-8601 timestamp; null until the cron fires the alert.
    - `expires_at` string, nullable: ISO-8601 timestamp; optional auto-expire.
    - `last_checked_at` string, nullable: ISO-8601 timestamp of last cron tick that evaluated this alert.
  - `count` integer, required: len(alerts).
  - `quota_total` integer, required: Tier-defined active-alert cap (Learner=5 / Researcher=50 / Investor=200 / Power User=999999 sentinel-for-unlimited).
  - `quota_remaining` integer, required: quota_total - count_of_active_alerts. Can be negative transiently after a tier downgrade if the Stripe-webhook paused_by_downgrade pass hasn't run yet; clamp to 0 in the UI.

## Other responses

- `401`: Unauthorized
- `500`: Internal Server Error

---

[API](https://skmtc.dev/oxfordledge/apis/oxford-ledge-api.md) · [All operations](https://skmtc.dev/oxfordledge/apis/oxford-ledge-api/llms.txt) · [OpenAPI document](https://skmtc.dev/oxfordledge/apis/oxford-ledge-api/revisions/e832a1260f0b?raw)
