# API contract v2 (v1.0 additions) Base: `/api`. JSON everywhere. Errors as `{error: {code, message}}` with proper HTTP status. This file documents the **new** endpoints added on top of `docs/api-contract.md` (POC). All existing POC endpoints remain unchanged. ## CV Import ### `POST /cv/import` Extract text from an uploaded file and generate draft CV sections via the LLM gateway (cheap class). Does NOT write to `cv_section` -- returns drafts for user review. Request body: ```json { "filename": "my_cv.pdf", "content_base64": "JVBERi0xLjQK..." } ``` Response 200: ```json { "drafts": [ { "kind": "experience", "title": "Software Engineer", "org": "TechCorp", "location": "Malmo", "start_date": "2022-01", "end_date": null, "bullets": ["Built feature X", "Improved performance by 20%"], "tags": ["python", "fastapi"] } ] } ``` Errors: - 422 `{error: {code: "empty_file", message: "..."}}` when the file is empty or contains no extractable text. - 422 `{error: {code: "unsupported_format", message: "..."}}` when the file type is not recognized. ### `POST /cv/import/confirm` Create `cv_section` rows from the drafts returned by `/cv/import`. Request body: ```json { "drafts": [ { "kind": "experience", "title": "Software Engineer", "org": "TechCorp", "bullets": ["Built feature X"], "tags": ["python"] } ] } ``` Response 201: ```json { "created": 3, "sections": [/* CvSectionOut[] */] } ``` ## Postings Fetch (Arbetsformedlingen connector) ### `POST /postings/fetch` Fetch job postings from the Arbetsformedlingen connector, create `job_posting` + `application(discovered)` for new postings, skip duplicates. Request body: ```json { "query": "python developer", "region": "Skane lan" } ``` Response 200: ```json { "new": 12, "dupes": 3 } ``` Errors: - 503 `{error: {code: "connectors_disabled", message: "Connectors are not enabled. Set CONNECTORS_ENABLED=true to enable."}}` when `CONNECTORS_ENABLED=false`. ## Batch Scoring ### `POST /scoring/batch` Score multiple applications in one call (cheap class). Each response includes `red_flags` (scam/shield checks). Request body: ```json { "application_ids": ["uuid1", "uuid2"] } ``` Response 200: ```json { "results": [ { "application_id": "uuid1", "score": 72, "rationale": {"match": 0.72, "factors": {"skills": 0.8}}, "red_flags": ["unpaid trial period mentioned"] } ] } ``` ## Today Digest ### `GET /today` Returns the daily digest: ranked postings, follow-up nudges, and pending approval count. Response 200: ```json { "digest": [ { "application_id": "uuid", "title": "Backend Developer", "company": "TechCorp", "score": 85 } ], "nudges": [ { "application_id": "uuid", "days_since_sent": 9, "suggestion": "Consider sending a follow-up email asking about the status of your application." } ], "pending_approvals": 2 } ``` Nudge SQL: `state = 'sent' AND days_since(last_activity_at) > follow_up_after_days AND (follow_up_snoozed_until IS NULL OR follow_up_snoozed_until < today)`. ## Interview Prep ### `POST /applications/{id}/interview-prep` Generate interview prep Q&A (strong LLM class), stored as an artifact of kind `other`. Sets `interview_prep_artifact_id` on the application. Response 200: ```json { "artifact_id": "uuid", "content": "# Interview Prep\n\n## Q1: ..." } ``` Mock mode returns deterministic 10-question Q&A markdown. ## Concierge / Demo Seed ### `POST /concierge/seed-demo` Idempotent: seeds a demo profile ('Demo Demosson') with Swedish characters (a,a,o), 6 realistic Skane postings, varied application states (one scored high, one sent 8 days ago for nudge demo). Calling twice does not duplicate data. Response 200: ```json { "profile": "Demo Demosson", "postings": 6, "applications": 6, "sections": 4 } ``` ## SMTP Transport (no new endpoint) The outbox `POST /outbox/send` now selects transport at call time: 1. If `SMTP_HOST` is set: `SmtpTransport` (ssl on port 465, starttls otherwise, auth with `SMTP_USER`/`SMTP_PASS`, from `SMTP_FROM`). 2. Else: `ClipboardTransport` (marks sent + stores payload for UI copy/paste). The approval gate checks (confirmed, unexpired, hash match) are UNCHANGED. ## Scheduler (no new endpoint) APScheduler `AsyncIOScheduler` starts during app lifespan when `SCHEDULER_ENABLED=true` (default false). Runs a daily job at 07:00 that fetches postings and batch-scores pending applications.