Versioned under /api/v1. All endpoints (except health) require an API key.
Authorization: Bearer fill_k_... # preferred X-Api-Key: fill_k_... # alternative
Create keys on the API Keys page. Rate limits: 60 req/min per key; AI endpoints 10–20 req/min. 429 responses carry Retry-After.
Errors are JSON: {"error": "message"} with a meaningful HTTP status (402 = no fills available / balance empty).
/api/v1/health/api/v1/me/api/v1/balance{plan, balance_cents, fill_price_cents, free_fills_per_day, free_fills_used_today, free_fills_remaining_today}/api/v1/profile/api/v1/resumes/api/v1/resumes/:id/fileX-Filename header carries the name — used by the extension to attach the file to upload fields./api/v1/select-resume{jd_text?, title?, company?} → {resume_id, label, reasoning}. AI picks the best resume for the job./api/v1/map-fieldsThe main call. Send the scanned fields and job context; get a value for every field.
{
"url": "https://boards.greenhouse.io/acme/jobs/123",
"title": "Senior Engineer", // optional
"company": "Acme", // optional
"jd_text": "...", // scraped job description
"resume_id": "uuid", // optional — skip AI resume selection
"fields": [{
"id": "f1", // scanner-assigned, echoed back
"kind": "text|email|tel|url|number|date|textarea|select|combobox|radio|checkbox|checkbox-group|file",
"label": "First Name",
"name": "first_name", // optional attrs that improve matching
"autocomplete": "given-name", "placeholder": "", "required": true,
"section": "Personal info", // optional containing heading
"options": [{"value": "us", "text": "United States"}] // choice kinds
}]
}Response:
{
"resume": {"id", "label", "filename", "mime"},
"resume_reasoning": "…why this resume…",
"mappings": [{
"id": "f1", "value": "Jane", "display": "United States", // display = option text
"confidence": 0.95, // 0..1
"source": "profile|questionnaire|qa|ai|ai_generated|resume|none",
"needs_review": false,
"field_type": "standard|open_ended|cover_letter|resume_file|other_file",
"generate": "cover_letter" // present when the field wants a generated letter
}],
"duplicate": {"id","url","company","title","status","created_at","match":"url|company_title"} | null,
"quota": { …same as /balance… }
}Values come from three layers: deterministic profile matching (fast, high confidence), your Q&A knowledge base (semantic match), then LLM mapping/generation for the rest. Returns 402 before doing AI work if no fill is available.
/api/v1/cover-letter{jd_text, title?, company?, resume_id?} → {cover_letter} (plain text, 250–350 words)./api/v1/fills/completeCall after the user approves + the form is filled. Creates the application record and charges one fill (balance first, else free quota; 402 if neither).
{
"url": "...", "title": "...", "company": "...",
"jd_text": "full JD copy stored with the record",
"form_snapshot": [{"label": "First Name", "value": "Jane"}],
"resume_id": "uuid"
}
→ { "ok": true, "application_id": "uuid", "charged_cents": 100,
"balance_cents": 400, "quota": {…} }/api/v1/qa{question, answer} or {pairs: [{question, answer}]} (≤50). Upserts by question text and re-embeds./api/v1/qa/api/v1/applications?status=applied|interview|rejected|offer)./api/v1/applications/check?url=… or ?company=…&title=… → {duplicate|null}. URL matching ignores query strings./api/v1/applications/api/v1/applications/:id/api/v1/applications/:idstatus / company / title./api/v1/applications/:id