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:read | Search jobs, read job details, list boards and check your quota. Public data only. |
applications:read | See the jobs you track, their status, notes and the CVs attached to them. |
applications:write | Save jobs, move them through your pipeline, add notes and attach CV versions. |
cv:read | Read 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/search
limit: search
needs jobs:read
Search the index with filters and facets.
Query
q |
string | Free-text query. Min 2 chars unless tags is given; max 200. |
tags |
csv | Tags AND-ed into the query. |
remote_type |
csv | remote, hybrid, onsite. |
seniority |
csv | e.g. senior,lead. |
country |
csv | ISO codes, e.g. US,DE. |
board |
csv | Board keys — see /boards. |
location |
string | Free-text place filter. |
has_salary |
bool | Only jobs with a listed salary. |
visa_sponsorship |
bool | Only jobs stating visa sponsorship. |
new_since |
datetime | ISO 8601. Only jobs career.now first saw at or after this instant (first_seen_at). Jobs seen before tracking began (2026-09-29) never match. |
sort |
enum | newest (default), relevant, salary. |
page |
int | 1–3. Pagination is capped at 3 pages × 25 results. |
Returns { results[], total, facets, page, page_size }. Each result carries a token and the external apply url.
- Check
facetsbefore narrowing further — they show which filter values actually have counts.
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*, `codeandlinks` 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
422with 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_at | Best 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_at | When 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_at | The 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_status | Whether 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.
| Endpoint | Requests / 24h |
search | 50 |
job | 200 |
boards | 30 |
me | 50 |
applications | 500 |
cv | 200 |
cv_pdf | 300 |
feedback | 100 |
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.