API

One REST API for agents: search the job index, track applications on the candidate's own board, and store a tailored CV per job that renders to an ATS-friendly PDF. Everything is scoped to the key's owner.

Authentication

Generate a key at /agents and send it as a bearer token. One key per account; generating a new one revokes the old.

Authorization: Bearer cnk_live_…

Permissions

Every key carries explicit permissions, chosen when it is created. Searching the public job market never implies access to private data.

jobs:readSearch jobs, read job details, list boards and check your quota. Public data only.
applications:readSee the jobs you track, their status, notes and the CVs attached to them.
applications:writeSave jobs, move them through your pipeline, add notes and attach CV versions.
cv:readRead the CV and parsed profile stored on your career.now account.

New keys get jobs:read, applications:read and applications:write; cv:read is opt-in. A missing permission answers 403 with required_scope. Remote MCP clients get the same permissions through OAuth instead of a key.

Quickstart

Track a job, attach a CV, download the PDF.

curl -s https://career.now/api/v1/applications \
  -H "Authorization: Bearer $CAREERNOW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://boards.example.com/jobs/123",
       "title":"Senior Backend Engineer","company":"Acme"}'

# → {"application":{"id":"01JB…","status":"saved", …}}

curl -s https://career.now/api/v1/applications/01JB…/cv \
  -H "Authorization: Bearer $CAREERNOW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"basics":{"name":"Jan Kowalski","email":"[email protected]"},
       "work":[{"name":"Acme","position":"Senior Backend Engineer",
                "startDate":"2023-01",
                "highlights":["Cut p99 latency by 40%"]}]}'

# → {"cv":{"id":"01JC…","version":1,"pdf_url":"/api/v1/cv/01JC….pdf"}}

curl -s https://career.now/api/v1/cv/01JC….pdf \
  -H "Authorization: Bearer $CAREERNOW_API_KEY" -o cv.pdf

Jobs

Read-only search over the live index. This is where an agent finds work to track.

GET /jobs/{token} limit: job needs jobs:read

Full detail for one job, including the external apply URL.

Returns The rich job object with description_markdown and a consolidated salary.

  • 404 when the token is invalid or the job has aged out of the index.

GET /boards limit: boards needs jobs:read

Every indexed board with its live job count.

Returns { boards: [{ board, count }] }

Applications

The candidate's pipeline — the same board they see at /dashboard/board. Writes here are immediately visible in the UI.

GET /applications limit: applications needs applications:read

List tracked applications, newest first.

Query

status enum Filter: saved, applied, interview, offer, rejected.

Returns { applications: [{ id, url, status, title, company, location, salary_raw, notes, created_at, updated_at }] }

POST /applications limit: applications needs applications:write

Track a job.

Body

url required string The job's URL on the original board.
title string Snapshot, shown on the card.
company string Snapshot.
location string Snapshot.
salary_raw string Snapshot, as printed by the source.
description string Snapshot, max 20 000 chars.
job_token string The search token, when the job came from /jobs/search.
status enum Start somewhere other than saved.

Returns 201 { application }

  • Idempotent on url: posting the same job again refreshes the snapshot and never resets a status the candidate already advanced.
  • Send the snapshot fields. Jobs leave the index after ~35 days and the card must still read correctly a year later.

GET /applications/{id} limit: applications needs applications:read

One application with its CV versions.

Returns { application, cvs[] }

PATCH /applications/{id} limit: applications needs applications:write

Move a card through the pipeline, or annotate it.

Body

status enum saved, applied, interview, offer, rejected.
notes string Free text, max 5 000 chars. Replaces the existing note.

Returns { application }

DELETE /applications/{id} limit: applications needs applications:write

Untrack a job. Its CV versions go with it.

Returns { deleted: true }

CVs

A CV belongs to an application, because a tailored CV is tailored to one job. Versions are append-only: posting again creates the next version and never edits the last, so a version's PDF stays a record of what was actually sent.

POST /applications/{id}/cv limit: cv needs applications:write

Store a new CV version.

Body

(body) required JSON Resume A JSON Resume document, sent as the body or wrapped in { "resume": … }. Only basics.name is required.

Returns 201 { cv: { id, version, resume, pdf_url, created_at } }

  • bold, *italic*, ` code and links` work inside any string.
  • Dates are ISO 8601 (2023-01). The template prints them in one house style and orders roles newest-first, so the agent does not have to.
  • A malformed document returns 422 with the exact field path, e.g. work[0].position must be a string.

GET /applications/{id}/cv limit: cv needs applications:read

The current (newest) CV version.

Returns { cv }

GET /applications/{id}/cvs limit: cv needs applications:read

Every version for this application, newest first.

Returns { cvs: [...] }

GET /cv/{id} limit: cv needs applications:read

One CV version by id.

Returns { cv }

GET /cv/{id}.pdf limit: cv_pdf needs applications:read

The rendered PDF.

Returns application/pdf — a one-column, ATS-friendly A4 document with selectable text.

  • Owner-only, like every endpoint here. A CV carries a name, a phone number and an employment history, so it never gets an unauthenticated URL.
  • Rendered on first request and cached. A version never changes, so the bytes never do either.

Feedback

Tell us when data or the API is wrong. Reports are aggregated per source and field and feed the data-quality pipeline.

POST /feedback limit: feedback

Report a data or API problem.

Body

category required enum wrong_field, missing_data, dead_link, duplicate, stale, bad_description, search_quality, api_bug, api_suggestion, other.
message required string What is wrong and how you know, 10-2000 chars.
job_token string The affected job's token from /jobs/search.
field string Affected field, e.g. salary, remote_type, posted_at, url.
observed string The value career.now returned.
expected string The correct value per the original posting.
endpoint string For API problems: the endpoint or tool.

Returns 201 { ok, id }

  • Open to every credential. Counted in its own bucket, never against search quota.

Account

The key owner's own data. Reading the CV needs the explicit cv:read permission.

GET /me limit: me needs cv:read

The key owner's CV and parsed profile.

Returns { email, cv_markdown, profile }

GET /me/quota unmetered

Remaining quota per endpoint, and the permissions this credential carries.

Returns { plan: "free", window, scopes, auth, quota: [{ endpoint, limit, used, remaining }] }

  • Unmetered and open to every credential — safe to call any time to check headroom and permissions.

Freshness

Four fields describe time, and they mean different things.

posted_atBest available publication date from the source. Usually the real posting date, but on some sources it is a page-modification date, and some sources give none (null). Don't treat it as exact.
first_seen_atWhen career.now first observed this job. Written once, never moved by a refresh. null means the job was first seen before tracking began on 2026-09-29. For a deduplicated job it is the earliest sighting of any copy. Use new_since to ask for jobs that appeared after a moment.
fetched_atThe most recent crawler refresh (job detail only). Moves every night for a live job, so it says nothing about how new the job is.
liveness_statusWhether career.now believes the posting is still open (job detail only): null = not checked yet, live = the last check found it open, unknown = the last check was inconclusive, gone = retired. Gone jobs leave the search index, and get_job then answers 404. Jobs also leave the live index about 35 days after publication (archived).

A full change feed (created, updated, closed, reopened since a moment) is not offered yet: it needs reliable lifecycle history, which starts with first_seen_at. For "what's new since yesterday", search with new_since.

Rate limits

Per endpoint, per rolling 24 hours, counted against the key's owner — not per IP. Every response carries X-RateLimit-Remaining; a 429 carries Retry-After. career.now is free; there is no paid tier.

EndpointRequests / 24h
search50
job200
boards30
me50
applications500
cv200
cv_pdf300
feedback100

For agents

The same reference, plus a workflow and worked queries, is served as an Agent Skill at /skill.md. MCP clients can use the same API as tools: remotely at https://career.now/mcp (Streamable HTTP, OAuth) or locally with npx -y @careernow/mcp (stdio, API key). Setup per client: /agents.