jobhunt-platform/docs/api-contract-v2.md
hermes 3035e4eac9 W2: api v1 features (CV import, AF fetch, batch scoring, today/nudges, interview prep, SMTP+clipboard transports, scheduler, 90 tests)
Implements docs/api-contract-v2.md. Migration 002 adds follow-up fields.
EchoTransport replaced by SMTP->Clipboard selection behind unchanged approval gate.
Scheduler (APScheduler) daily 07:00 fetch+score, env-gated, default off.
Test image installs workspace packages; build context moved to repo root.
Recovered and committed by integration lead after W2 worker hit iteration limit.
2026-07-30 18:29:17 +00:00

199 lines
No EOL
4.5 KiB
Markdown

# 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.