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

4.5 KiB

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:

{
  "filename": "my_cv.pdf",
  "content_base64": "JVBERi0xLjQK..."
}

Response 200:

{
  "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:

{
  "drafts": [
    {
      "kind": "experience",
      "title": "Software Engineer",
      "org": "TechCorp",
      "bullets": ["Built feature X"],
      "tags": ["python"]
    }
  ]
}

Response 201:

{
  "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:

{
  "query": "python developer",
  "region": "Skane lan"
}

Response 200:

{
  "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:

{
  "application_ids": ["uuid1", "uuid2"]
}

Response 200:

{
  "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:

{
  "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:

{
  "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:

{
  "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.