§ unswayed-backend · API contract & docs
Lexi AI — cover letters (Phase 14, UN-111)
What it is
The cover-letters slice (src/lexi-ai/cover-letters/) lets an applicant generate a cover letter
for a specific job, in a chosen tone, then keep/edit/delete their letters. Part of the per-user
Lexi /api/v1/* career suite (applicant-only, bare camelCase, per-user rate limits).
Endpoints
POST /api/v1/users/{userId}/cover-letters/generate— 15/hour, behind thecoverLetterGenerationfeature flag. Body{resumeId, jobPostingId | jobDescriptionText, tone?}. The LLM is grounded in the applicant's parsed resume (ApplicantResume.resumeData) + the JD (a platform job or pasted text) + the chosen tone (formaldefault |conversational|enthusiastic|concise). Each call creates a newCoverLetterrow (source:ai_generated) — regenerating with a different tone never overwrites a prior version. Returns the letter plushighlightedPoints— a flat list of the resume↔JD connections the letter leaned on, so the user can verify accuracy. 201.GET /api/v1/users/{userId}/cover-letters?jobPostingId=— paginated list, filterable by job.GET /api/v1/users/{userId}/cover-letters/{id}— detail (404 if not owned).PUT /api/v1/users/{userId}/cover-letters/{id}— manual edit ({content, tone?}); flipssource→edited. Editing one letter never touches another.DELETE /api/v1/users/{userId}/cover-letters/{id}— 204.
How it works
The slice reuses the shared AI seam (AiCompletionPort.complete(..., {model: models.lenux, responseFormat:'json'}), gpt-4o-mini, retry-once parser; LexiUnavailableError → 503) and the
per-user ownership/feature primitives (LexiUserContextService, LexiFeatureGuard). Generation
rejects a resume with no parsed content (422). CoverLetterService.generate(...) is exported so
the Lexi chat slice (UN-114) can call it from a confirmed generate_cover_letter action.
The highlightedPoints mirror the "reasonOfMatch" idea from the scoring engine — a small,
plain-language trust signal rather than an opaque blob.