# ClassicsLens API Reference

All endpoints are under `https://classicslens.com`. When `AUTH_ENABLED=true` (always in production), all `/api/*` routes require an authenticated session cookie except where noted.

---

## Authentication

### GET /api/auth/status

Returns current authentication state. No session required.

**Response:**
```json
{
  "enabled": true,
  "authenticated": true,
  "user": {
    "id": "uuid",
    "email": "user@example.com",
    "displayName": "Jane Smith",
    "avatarUrl": "https://..."
  }
}
```

### GET /api/auth/google

Initiates Google OAuth flow. Redirects to Google.

### POST /api/auth/register

Register with email and password.

**Body:** `{ "email": "string", "password": "string", "displayName": "string" }`

**Response:** `{ "ok": true }` or `{ "error": "..." }`

### POST /api/auth/login

Sign in with email and password.

**Body:** `{ "email": "string", "password": "string" }`

**Response:** `{ "ok": true }` or `401`

### POST /api/auth/logout

Signs out the current user. Destroys the session.

### POST /api/auth/forgot-password

Sends a password reset email via Resend.

**Body:** `{ "email": "string" }`

**Response:** `{ "ok": true }` (always — no enumeration)

### POST /api/auth/reset-password

Completes a password reset.

**Body:** `{ "token": "string", "password": "string" }`

### DELETE /api/auth/account

Permanently deletes the authenticated user's account and all data (FK cascades).

---

## Passages

### GET /api/passage/today

Returns the daily passage for the current user (based on day counter and language mix setting).

**Response:** `PassagePayload` (see schema below)

### GET /api/passage/id/:id

Returns a specific passage by ID.

### POST /api/passage/:id/complete

Marks a passage as completed. Advances day counter and updates streak.

**Response:** `{ "ok": true }`

---

## Morphological Parser

### GET /api/parse

Parse a Latin or Greek word. Free accounts: 10 parses/day. Pro/Instructor/Academic: unlimited.

**Query params:**
- `q` (required) — the word to parse; may be inflected, accentless input accepted

**Response:**
```json
{
  "query": "amat",
  "found": true,
  "entries": [
    {
      "lemma": {
        "id": "lat-amo-v",
        "headword": "amō",
        "language": "latin",
        "partOfSpeech": "verb",
        "shortDef": "to love",
        "principalParts": ["amō", "amāre", "amāvī", "amātum"],
        "frequencyRank": 42
      },
      "logeionUrl": "https://logeion.uchicago.edu/amo",
      "perseusUrl": "https://www.perseus.tufts.edu/hopper/morph?l=amo&la=la",
      "parses": [
        {
          "surfaceForm": "amat",
          "morphologyStr": "pres. act. ind. 3sg",
          "morphology": {
            "type": "verb",
            "person": "3rd",
            "number": "singular",
            "tense": "present",
            "mood": "indicative",
            "voice": "active",
            "case_": null,
            "gender": null,
            "dialect": null
          }
        }
      ],
      "compound": null,
      "gloss": "The verb amō is the most common Latin word for affection..."
    }
  ],
  "source": "morpheus-db",
  "fallbackUrls": {
    "logeion": "https://logeion.uchicago.edu/amat",
    "perseus": "https://www.perseus.tufts.edu/hopper/morph?l=amat&la=la"
  }
}
```

**Source values:**
- `morpheus-db` — answered from local SQLite database (fast)
- `morpheus-api` — answered by Morpheus sidecar in real time (slower)
- `inflection-table` — answered from in-memory fallback tables
- `not-found` — word not found; `found: false`, `entries: []`

**Quota exceeded (402):**
```json
{
  "error": "Parse quota exceeded",
  "quotaExceeded": true,
  "limit": 10,
  "used": 10,
  "resetAt": "2026-04-08T00:00:00.000Z"
}
```

---

## Paradigm Tables

### GET /api/paradigm

Returns full conjugation or declension table for a lemma.

**Query params:**
- `lemma` (required) — dictionary headword (e.g. `amo`, `λόγος`)
- `language` (required) — `latin` or `greek`
- `pos` (optional) — part of speech hint (`verb`, `noun`, etc.)

**Response:**
```json
{
  "lemma": "amo",
  "language": "latin",
  "paradigm": {
    "type": "verb",
    "finite": {
      "ind": {
        "active": {
          "pres": { "1sg": "amō", "2sg": "amās", "3sg": "amat", "1pl": "amāmus", "2pl": "amātis", "3pl": "amant" }
        }
      }
    },
    "nonFinite": {
      "inf": { "pres_act": "amāre", "perf_act": "amāvisse" }
    }
  }
}
```

Returns `{ "paradigm": null }` if no data is available for the lemma.

---

## Review (SRS)

### GET /api/review

Returns due cards for the current session.

**Response:** `{ "due": [...cards], "total": 42 }`

### POST /api/review/answer/:id

Submit an answer for a card.

**Body:** `{ "grade": "again" | "good" | "easy" }`

**Response:** `{ "ok": true, "nextReview": "ISO date string" }`

### POST /api/review/add

Add a word occurrence to the review deck by occurrence ID.

**Body:** `{ "occurrenceId": "string" }`

### POST /api/review/add-by-lemma

Add a word to the review deck by lemma (used from Analyze tab and word popups).

**Body:**
```json
{
  "lemmaId": "string",
  "headword": "string",
  "shortDef": "string",
  "language": "latin" | "greek",
  "surfaceForm": "string",
  "morphologyStr": "string"
}
```

### DELETE /api/review/:id

Remove a card from the deck.

### GET /api/review/export

Downloads a tab-separated Anki-compatible export of all review cards. Returns a `.txt` file. In Anki: File → Import, separator: Tab. Cards tagged `ClassicsLens`.

---

## User Settings & State

### GET /api/user/settings

Returns current user settings.

**Response:**
```json
{
  "userId": "uuid",
  "languageMix": "alternate",
  "posColoring": false,
  "theme": "dark"
}
```

`languageMix` values: `latin_only`, `greek_only`, `alternate`, `mostly_latin`, `mostly_greek`

### PATCH /api/user/settings

Update settings. All fields optional.

**Body:**
```json
{
  "languageMix": "latin_only",
  "posColoring": true,
  "theme": "light"
}
```

### GET /api/user/state

Returns user progress state.

**Response:**
```json
{
  "userId": "uuid",
  "currentDay": 42,
  "streakCount": 7,
  "lastCompletedDate": "2026-04-06"
}
```

### GET /api/user/export

Full data export (JSON). Includes all user data: settings, state, review items, completions.

---

## Analytics

### GET /api/analytics

Returns learning analytics for the current user.

**Response:**
```json
{
  "totalCards": 150,
  "activeCards": 140,
  "matureCards": 85,
  "youngCards": 55,
  "avgEasiness": 2.35,
  "streakCount": 7,
  "currentDay": 42,
  "languageBreakdown": { "latin": 90, "greek": 50 },
  "posBreakdown": { "verb": 60, "noun": 55 },
  "vocabGrowth": [
    { "month": "2026-01", "count": 12 }
  ],
  "readingActivity": [
    { "date": "2026-04-06", "count": 2 }
  ]
}
```

### GET /api/analytics/grammar-gaps

Returns morphological patterns ranked by weakness (lowest average easiness = most forgotten).

**Response:**
```json
[
  {
    "pos": "verb",
    "mood": "subjunctive",
    "tense": "aorist",
    "language": "greek",
    "avgEasiness": 1.82,
    "cardCount": 14
  }
]
```

---

## Student Routes

### GET /api/student/assignments

Returns all assignments for the current user across all active class memberships. Sorted by due date ascending.

**Response:**
```json
[
  {
    "assignmentId": "uuid",
    "passageId": "uuid",
    "customTextId": null,
    "isCustomText": false,
    "dueDate": "2026-04-10",
    "instructions": "Focus on the ablative absolute constructions.",
    "sourceAuthor": "Caesar",
    "sourceWork": "De Bello Gallico",
    "sourceReference": "1.1",
    "className": "LATN 201",
    "instructorName": "Prof. Smith",
    "completedAt": null
  }
]
```

---

## Instructor Routes

All instructor routes require `instructor` or `academic` tier.

### GET /api/instructor/classes

Returns classes owned by the current instructor.

### POST /api/instructor/classes

Create a class. **Body:** `{ "className": "string" }`

### DELETE /api/instructor/classes/:id

Delete a class (cascades to roster and assignments).

### POST /api/instructor/classes/:id/invite

Invite a student by email.

**Body:** `{ "email": "string" }`

**Errors:**
- `409` — student already in this class (not removed)
- `400 { "limitReached": true, "limit": 30, "activeCount": 30 }` — student cap reached

### DELETE /api/instructor/classes/:id/members/:email

Remove a student from a class.

### GET /api/instructor/classes/:id/roster

Class roster with per-student stats (passages completed, card count).

### POST /api/instructor/classes/:id/assign

Assign a passage or custom text to the class.

**Body:**
```json
{
  "passageId": "uuid or null",
  "customTextId": "uuid or null",
  "dueDate": "2026-04-15",
  "instructions": "string (optional)"
}
```

Exactly one of `passageId` or `customTextId` must be non-null.

### DELETE /api/instructor/assignments/:id

Remove an assignment.

### GET /api/instructor/classes/:id/dashboard

Completion tracking: all assignments with per-student completion timestamps.

### GET /api/instructor/texts

Returns the instructor's uploaded custom texts.

### POST /api/instructor/texts

Upload a custom text.

**Body:**
```json
{
  "title": "string",
  "author": "string",
  "language": "latin" | "greek",
  "rawText": "string (max 50,000 chars)",
  "copyrightConfirmed": true
}
```

### DELETE /api/instructor/texts/:id

Delete a custom text and all its assignments.

### GET /api/texts/:id

Returns a custom text as `PassagePayload`. Accessible by the owning instructor or any active student with this text assigned.

### POST /api/texts/:id/complete

Mark a custom text assignment as completed for the current user.

**Response:** `{ "ok": true }`

---

## Passage Search (Instructor)

### GET /api/passages/search

Search the passage corpus. Instructor tier required.

**Query params:**
- `q` — search term (matches author, work, reference, text content)
- `language` — `latin` or `greek` (optional)
- `limit` — max results, default 20, max 50

---

## Billing

### GET /api/billing/status

Returns billing info for the current user.

**Response:**
```json
{
  "tier": "free",
  "subscriptionTier": "free",
  "tierOverride": null,
  "stripeCustomerId": null
}
```

Tier values: `free`, `pro`, `instructor`, `academic`

### POST /api/billing/checkout

Creates a Stripe Checkout session. **Body:** `{ "priceId": "price_..." }`

**Response:** `{ "url": "https://checkout.stripe.com/..." }` — redirect user to this URL.

### POST /api/billing/portal

Creates a Stripe Customer Portal session for subscription management.

**Response:** `{ "url": "https://billing.stripe.com/..." }`

### POST /api/billing/webhook

Stripe webhook receiver. Handles `checkout.session.completed`, `customer.subscription.updated`, `customer.subscription.deleted`. Do not call directly.

### POST /api/billing/redeem-promo

Redeem a promo code. **Body:** `{ "code": "string" }`

**Response:** `{ "ok": true, "tier": "pro" }` or `{ "error": "..." }`

---

## Admin Routes

Require the session user to match `ADMIN_EMAIL`.

### GET /api/admin/users

All users with activity stats and current effective tier.

### DELETE /api/admin/users/:id

Delete a user and all data.

### PATCH /api/admin/users/:id/tier

Set or clear a tier override.

**Body:** `{ "tierOverride": "pro" | "instructor" | "academic" | "free" | null }`

### GET /api/admin/passages

Browse passages with pagination.

**Query params:** `page`, `limit`, `q`, `language`

### GET /api/admin/promos

All promo codes.

### POST /api/admin/promos

Create a promo code.

**Body:** `{ "code": "string", "tier": "pro" | "academic", "maxUses": 5, "expiresAt": "2026-12-31" }`

### PATCH /api/admin/promos/:code

Activate or deactivate a promo. **Body:** `{ "active": false }`

### GET /api/admin/instructors

All instructor/academic accounts with student counts and limits.

**Response:**
```json
[
  {
    "id": "uuid",
    "email": "prof@university.edu",
    "displayName": "Dr. Jones",
    "effectiveTier": "instructor",
    "studentLimit": 30,
    "activeStudents": 18,
    "classCount": 2
  }
]
```

### PATCH /api/admin/instructors/:id/limit

Update an instructor's student limit. **Body:** `{ "limit": 50 }`

---

## Common Response Types

### PassagePayload

Returned by `/api/passage/today`, `/api/passage/id/:id`, and `/api/texts/:id`.

```json
{
  "passage": {
    "id": "uuid",
    "language": "latin",
    "sourceAuthor": "Caesar",
    "sourceWork": "De Bello Gallico",
    "sourceReference": "1.1",
    "periodTag": "classical",
    "genreTag": "historiography",
    "rawText": "Gallia est omnis divisa in partes tres...",
    "translationText": "All Gaul is divided into three parts...",
    "dayNumber": 42,
    "wordCount": 180
  },
  "wordOccurrences": [
    {
      "id": "uuid",
      "surfaceForm": "divisa",
      "positionLine": 0,
      "positionToken": 3,
      "morphology": {
        "type": "verb",
        "tense": "perfect",
        "voice": "passive",
        "mood": "participle",
        "gender": "feminine",
        "case_": "nominative",
        "number": "singular"
      },
      "customGloss": null,
      "lemma": {
        "id": "lat-divido-v",
        "headword": "dīvidō",
        "language": "latin",
        "partOfSpeech": "verb",
        "shortDef": "to divide, separate",
        "principalParts": ["dīvidō", "dīvidere", "dīvīsī", "dīvīsum"],
        "frequencyRank": 312
      },
      "logeionUrl": "https://logeion.uchicago.edu/divido",
      "perseusUrl": "https://www.perseus.tufts.edu/hopper/morph?l=divido&la=la"
    }
  ],
  "notes": [],
  "questions": [],
  "context": {
    "contextNote": "This is the famous opening of Caesar's account of the Gallic Wars...",
    "thematicTags": ["military", "geography", "Roman expansion"]
  }
}
```

---

## Rate Limiting & Quotas

- **Free accounts**: 10 parse requests per day via `GET /api/parse`. Resets at midnight UTC. Clicking words in passages does not count against this limit.
- **Pro / Instructor / Academic / students in instructor classes**: unlimited parses.
- Session cookies expire after 7 days of inactivity. Secure cookies require HTTPS.
- No rate limiting on other endpoints (trusted users via session auth).
