Point your AI agent at freehire and let it find you a job.

Get the CLI →
v1.0.0
OpenAPI 3.1.0

freehire API

Base URL

All endpoints are served under https://freehire.me/api/v1. The API is read-first and open: the job, search, facet, and company endpoints need no authentication and may be called cross-origin.

Authenticated endpoints accept either the browser session cookie (set by sign-in, same-origin) or a personal API key sent as a Bearer token — see the authentication model below. Keys are created in your account, at freehire.me/my/api-keys, not through this API.

Response envelope

Single items are wrapped as { "data": ... }. Collections add pagination metadata: { "data": [...], "meta": { "total", "limit", "offset" } }. Errors are { "error": "message" } with a matching HTTP status.

{ "data": { "...": "single item" } }

{ "data": [ ... ], "meta": { "total": 4213, "limit": 20, "offset": 0 } }

{ "error": "job not found" }

Pagination

List and search endpoints page with limit (default 20, max 100) and offset (default 0). meta.total reports the total matching the current filters, so you can compute the number of pages.

Pagination is bounded on every list endpoint: offset + limit may not exceed 10000 (pagination too deep → 400). This is deep-paging protection, not a cap on the reported total — use filters to narrow rather than paging that far.

The bound is a refusal rather than a clamp, deliberately: a clamped page would answer 200 carrying rows you did not ask for, and a client walking pages would loop over the same page forever without being able to tell. A 400 means "there is no such page", which is the truth.

It applies whichever store answers the endpoint, and it is not the same thing as the rate limit. A rate limit bounds requests per minute; on an endpoint whose offset you choose, the work per request is also yours to choose — so a client inside its request budget can still be asking for far more work than the budget implies. Both bounds exist and neither substitutes for the other.

Errors

Errors use standard HTTP status codes: 400 (bad request / invalid value), 401 (missing or invalid credentials), 403 (authenticated but not allowed, e.g. a non-moderator), 404 (no such job, company, or owned resource), and 503 (search temporarily unavailable). The body is always { "error": ... }.

Authentication model

Browser clients authenticate with an HttpOnly session cookie set on sign-in (same-origin; the SPA cannot read it). Non-browser clients use a personal API key as Authorization: Bearer <token>.

Endpoints marked “Session or API key” accept either; endpoints marked “Session only” (saved searches, subscriptions) accept only the cookie, so a leaked key cannot manage credentials. “Moderator” endpoints additionally require the moderator role.

External vs internal endpoints

This reference covers every endpoint a client can call from outside the browser: public reads, and endpoints that also accept a personal API key. Endpoints that require the browser’s own session cookie — saved searches, notifications, moderator tools, the browser extension — are not reachable with a key alone and are documented separately at freehire.me/docs/api/internal.

What is not here

This reference covers every endpoint you can call. A handful are deliberately left out because calling them directly is meaningless: the Gmail and calendar consent redirects (/me/gmail/connect, /me/calendar/connect, and their callbacks), which only a browser can complete; the Telegram bot webhook; the browser-tool websocket relay; the sitemap-cursor helpers behind /sitemap.xml; and the /og/*.png social-preview cards, which render an image rather than answer with JSON.

Managing API keys (POST, GET and DELETE on /me/api-keys) is not documented here, because no API client can call it. Those endpoints accept only the browser session cookie, and additionally require a freshly proven identity — a password re-entry or a sign-in-provider check — that a script has no way to supply. Create and revoke keys in your account instead, at freehire.me/my/api-keys; the plaintext token is shown exactly once, at creation, so store it then.

The /jobs/{slug}/fit endpoints are pre-rename aliases of /jobs/{slug}/match-analysis and hit the same handlers. They still work, so existing clients do not break — use the match-analysis paths in new code.

Most of this API is also a CLI. If you are writing an agent rather than an integration, freehire covers the same surface with less ceremony — search, tracking, the inbox and CV tailoring — over one API key.

Filtering jobs

These parameters apply to GET /jobs/search and GET /jobs/facets. Combine any of them with full-text q.

  • Pass multiple values as a comma-separated list to OR them: skills=go,rust matches either. Repeating the param (skills=go&skills=rust) also works.
  • Add <param>_mode=and to require all selected values: skills=go,rust&skills_mode=and matches both.
  • Add <param>_exclude=<value> to exclude matches: company_type_exclude=outstaff drops outstaff jobs.
  • Different facets are ANDed together; numeric and boolean filters are ANDed too. The geography facets are the one exception — see below.
  • Geography is a single OR group: regions, countries and cities widen each other instead of narrowing. regions=eu&countries=IT means "in Europe or in Italy", so it returns everything regions=eu alone would. To search one country, drop the region: countries=IT. The three name a single concept — where — so picking two places reads as "either", which is what makes regions=eu&countries=BR ("Europe or Brazil") useful. There is no AND to switch on: _mode=and does not apply to geography.
  • Use regions=none to match jobs with no resolved geography (an empty region set); it ORs with real region values and supports _exclude like any region.
  • A param no filter reads is ignored rather than refused — so old links and saved searches keep working — and comes back in meta.ignored_params, with did_you_mean when it is only the singular of a real facet. Check it: a dropped filter otherwise looks like a genuinely broad result. At most 10 are listed per response. The same report rides on /jobs/facets, /market/coverage and /companies, which answer {"data": ...} and grow a meta block only when there is something to warn about. /companies filters on its own vocabulary, so a jobs facet sent there is reported as ignored.

Facets

Every facet below supports repeat-OR, _mode=and, and _exclude as described above.

Param Filter Values
collections Collection yc, techstars, a16z-portfolio, a16z-speedrun, european, ai, mag7, bigtech, unicorn, fortune500, eastern-roots, indian-roots, ai-native, uk-skilled-worker-sponsor, nl-recognised-sponsor, us-h1b-sponsor
regions Region global, north_america, latam, eu, uk, mena, africa, apac, cis, none
work_mode Work format remote, hybrid, onsite
category Specialization software_engineering, backend, frontend, fullstack, mobile, devops, sre, network_engineering, data_engineering, data_science, data_analytics, ml_ai, ai_engineering, qa, security, hardware, embedded, blockchain, architecture, design, creative, engineering_design, industrial_engineering, product, project_management, management, healthcare, skilled_trades, retail, hospitality, logistics, education, personal_services, administration, marketing, sales, support, business_analysis, solutions_engineering, developer_relations, technical_writing, recruiting, hr, finance, legal, operations, customer_success, other
ai_archetype AI Specialization rag_app_builder, agent_builder, cloud_ml_platform_engineer, ml_trainer_researcher, fullstack_ai_engineer, devops_infra_engineer
seniority Seniority intern, junior, middle, senior, lead, staff, principal, c_level
role_type Role type people_manager — the title names a people-management role. One value only: carrying nothing means no management marker was found, NOT that the posting is individual-contributor work, so role_type_exclude means "no marker", not "IC"
skills Skills Open vocabulary — call /jobs/facets for live values
domains Industry fintech, crypto, ecommerce, gambling, gamedev, media, travel, healthcare, edtech, govtech, devtools, cybersecurity, ai, hrtech, adtech, proptech, logistics, mobility, climatetech, other
company_type Company type product, startup, outsource, outstaff, agency, inhouse, government
countries Countries Open vocabulary — call /jobs/facets for live values
cities City Open vocabulary — call /jobs/facets for live values
relocation Relocation not_supported, supported, required
employment_type Employment full_time, part_time, contract, internship, fellowship
english_level English none, a1, a2, b1, b2, c1, c2, native
posting_language Job language Open vocabulary — call /jobs/facets for live values
reality Posting reality fresh, stale, likely-evergreen
salary_currency Currency USD, EUR, GBP, RUB
company_slug Company Open vocabulary — call /jobs/facets for live values
source Source Open vocabulary — call /jobs/facets for live values
company_size Company size 1-10, 11-50, 51-200, 201-500, 501-1000, 1000+
education_level Education level none, bachelor, master, phd
salary_period Salary period year, month, day, hour

Numeric & boolean filters

Param Filter Values
visa_sponsorship Visa sponsorship true, false
requires_clearance Security clearance required true, false
salary_min Minimum salary integer — jobs whose minimum salary is at least this (pair with salary_currency)
salary_max Maximum salary integer — jobs whose maximum salary is at most this (pair with salary_currency)
experience_years_min Minimum experience integer — jobs requiring at least this many years
experience_years_max Maximum experience integer — jobs requiring at most this many years, the same figure experience_years_min bounds from below. Use 0 for jobs stating no prior experience is required. Either bound excludes jobs that state no requirement at all
posted_within_days Posted within integer — jobs whose SOURCE states a posting date in the last N days. Some boards restate that date on every crawl, so a job open for months can satisfy a narrow bound here; open_within_days is the one that cannot be rewritten from outside
open_within_days Open within integer — jobs first recorded by freehire in the last N days, i.e. how long the posting has been in the catalogue regardless of the date its source states. Independent of posted_within_days; both may be set and they narrow together

Recipes

  • Senior Go, remote, in the CIS regionq=go&seniority=senior&work_mode=remote&regions=cis
  • Backend roles, freshest first, in Germanycategory=backend&countries=DE&sort=posted_at&order=desc
  • One country only — no region param, or it widens back outcountries=IT&employment_type=contract
  • Must use both Go and Rustskills=go,rust&skills_mode=and
  • Exclude outstaff companiescompany_type_exclude=outstaff
  • At least $100k, with visa sponsorshipsalary_currency=USD&salary_min=100000&visa_sponsorship=true
  • Excluding jobs that need a security clearancerequires_clearance=false
  • Only jobs that need a security clearancerequires_clearance=true
Server:https://freehire.me/api/v1
No authentication selected
Client Libraries

Jobs

Public, unauthenticated reads. Jobs are returned in one wire shape (addressed by public_slug, never an internal id) shared by the list, detail, company, and search responses. Closed postings are excluded from lists and search and served only by the detail endpoint.

List jobs, newest first, with limit/offset pagination.

Takes no filters at all — limit and offset are the whole vocabulary. Anything else is ignored rather than refused, and listed in meta.ignored_params: a filter param sent here returns the entire catalogue, which is indistinguishable from a narrow result unless you check. Use /jobs/search to filter.

Query Parameters
  • limit
    Type: integer

    Page size, 1–100.

  • offset
    Type: integer

    Rows to skip.

Responses
  • application/json
Request Example for get/jobs
curl https://freehire.me/api/v1/jobs
{
  "data": [
    {
      "public_slug": "senior-go-engineer-acme-1a2b",
      "source": "greenhouse",
      "manually_added": false,
      "external_id": "123",
      "url": "https://boards.greenhouse.io/acme/jobs/123",
      "title": "Senior Go Engineer",
      "company": "Acme",
      "company_slug": "acme",
      "location": "Remote — EU",
      "description": "...",
      "countries": [
        "DE"
      ],
      "regions": [
        "eu"
      ],
      "work_mode": "remote",
      "skills": [
        "go",
        "postgresql"
      ],
      "cities": [
        "Berlin"
      ],
      "collections": [
        "yc"
      ],
      "is_tech": "tech",
      "posted_at": "2026-06-18T00:00:00Z",
      "created_at": "2026-06-18T09:12:00Z",
      "updated_at": "2026-06-18T09:12:00Z",
      "closed_at": null,
      "enrichment": {
        "summary": "...",
        "seniority": "senior",
        "category": "backend",
        "employment_type": "full_time",
        "relocation": "not_supported",
        "visa_sponsorship": false,
        "experience_years_min": 5,
        "english_level": "b2",
        "education_level": "bachelor",
        "domains": [
          "fintech"
        ],
        "posting_language": "en",
        "company_type": "product",
        "company_size": "51-200",
        "salary_min": 90000,
        "salary_max": 130000,
        "salary_currency": "EUR",
        "salary_period": "year"
      },
      "enriched_at": "2026-06-18T09:20:00Z",
      "enrichment_version": 7,
      "view_count": 42,
      "applied_count": 8,
      "reality": {
        "class": "fresh",
        "age_days": 3,
        "repost_count": 0,
        "mass_posting_count": 0,
        "fake_freshness": false
      }
    }
  ],
  "meta": {
    "total": 4213,
    "limit": 20,
    "offset": 0
  }
}

Full-text + faceted search over open jobs.

Combine free-text q with any of the filter params below. Repeated facet params are ORed; add <param>_mode=and to require all, or <param>_exclude=<value> to exclude. Without q, results default to newest first; with q, to relevance. A param no filter reads is ignored rather than refused, and listed in meta.ignored_params — check it, since a dropped filter otherwise looks like a broad result.

Plus every filter in the "Filtering jobs" section above.

Query Parameters
  • q
    Type: string

    Full-text query over title, company, description, and location. Unquoted words match as an OR of stemmed tokens; quoted ("systems engineer") as an order-independent AND — quoting is not a contiguous-phrase match. Narrow which fields it matches with q_fields. Exact-phrase matching is not offered, and default relevance order does not reliably rank a contiguous match above a scattered-token one.

  • q_fields
    Type: string

    Comma-separated subset of title,company,description,location to restrict q to, e.g. title. A name outside that set drops the whole parameter (reported in meta.ignored_params) rather than partially applying.

  • sort
    Type: string

    One of created_at, posted_at, view_count, salary_min, salary_max. Omit for relevance/newest.

  • order
    Type: string

    asc or desc (default desc).

  • limit
    Type: integer

    Page size, 1–100.

  • offset
    Type: integer

    Rows to skip; offset + limit ≤ 10000.

Responses
  • application/json
Request Example for get/jobs/search
curl https://freehire.me/api/v1/jobs/search
{
  "data": [
    {
      "public_slug": "...",
      "title": "Senior Go Engineer",
      "...": "..."
    }
  ],
  "meta": {
    "total": 137,
    "limit": 20,
    "offset": 0
  }
}

Search with full descriptions, for programmatic/agent consumers.

Same query and filters as /jobs/search, but each result carries the description in full (verbatim from the database) instead of the truncated index preview. Use description_format to choose how it is rendered.

Plus every filter in the "Filtering jobs" section above.

Query Parameters
  • q
    Type: string

    Full-text query over title, company, description, and location. Unquoted words match as an OR of stemmed tokens; quoted ("systems engineer") as an order-independent AND — quoting is not a contiguous-phrase match. Narrow which fields it matches with q_fields. Exact-phrase matching is not offered, and default relevance order does not reliably rank a contiguous match above a scattered-token one.

  • q_fields
    Type: string

    Comma-separated subset of title,company,description,location to restrict q to, e.g. title. A name outside that set drops the whole parameter (reported in meta.ignored_params) rather than partially applying.

  • description_format
    Type: string

    One of html (default, verbatim), text (tags stripped), markdown (HTML converted to Markdown). Unknown values fall back to html.

  • sort
    Type: string

    One of created_at, posted_at, view_count, salary_min, salary_max. Omit for relevance/newest.

  • order
    Type: string

    asc or desc (default desc).

  • limit
    Type: integer

    Page size, 1–100.

  • offset
    Type: integer

    Rows to skip; offset + limit ≤ 10000.

Responses
  • application/json
Request Example for get/agent/jobs/search
curl https://freehire.me/api/v1/agent/jobs/search
{
  "data": [
    {
      "public_slug": "...",
      "title": "Senior Go Engineer",
      "description": "## About the role\n...",
      "...": "..."
    }
  ],
  "meta": {
    "total": 137,
    "limit": 20,
    "offset": 0
  }
}

Count of matching jobs per facet value (and numeric stats).

Takes the same q and filter params as search, but returns the distribution of values instead of a page of jobs — use it to build filter UIs or see how a filter narrows the set. Continuous numeric facets are returned as stats (min/max), not per-value buckets.

Any search filter param narrows the counted set. (e.g. work_mode=remote)

Query Parameters
  • q
    Type: string

    Same full-text query as search.

Responses
  • application/json
Request Example for get/jobs/facets
curl https://freehire.me/api/v1/jobs/facets
{
  "data": {
    "total": 1820,
    "facets": {
      "seniority": {
        "senior": 640,
        "middle": 410,
        "junior": 120
      },
      "category": {
        "backend": 700,
        "frontend": 380
      }
    },
    "stats": {
      "salary_min": {
        "min": 20000,
        "max": 400000
      }
    }
  }
}

A single job by its public slug (serves closed jobs too).

Path Parameters
  • slug
    Type: string
    required

    The job public_slug.

Responses
  • application/json
Request Example for get/jobs/{slug}
curl https://freehire.me/api/v1/jobs/senior-go-engineer-acme-1a2b
{
  "data": {
    "public_slug": "senior-go-engineer-acme-1a2b",
    "title": "Senior Go Engineer",
    "closed_at": null,
    "...": "..."
  }
}

Jobs similar to the given one (semantic; may be empty).

Backed by the optional semantic index. Returns an empty list (not an error) when the source job is not indexed.

Path Parameters
  • slug
    Type: string
    required

    The job public_slug.

Query Parameters
  • limit
    Type: integer

    Max similar jobs.

Responses
  • application/json
Request Example for get/jobs/{slug}/similar
curl https://freehire.me/api/v1/jobs/senior-go-engineer-acme-1a2b/similar
{
  "data": [
    {
      "public_slug": "...",
      "title": "...",
      "...": "..."
    }
  ]
}

Other open postings in the same role cluster (per-city duplicates).

The per-city openings folded under one canonical card by content-dedup — each keeps its own location and apply_url so a seeker picks their city. The anchor job itself is included. meta.total is the whole cluster size, so it stays accurate when the list is a capped page.

Path Parameters
  • slug
    Type: string
    required

    The job public_slug.

Query Parameters
  • limit
    Type: integer

    Page size, 1–200 (default 50).

  • offset
    Type: integer

    Rows to skip.

Responses
  • application/json
Request Example for get/jobs/{slug}/copies
curl https://freehire.me/api/v1/jobs/senior-go-engineer-acme-1a2b/copies
{
  "data": [
    {
      "public_slug": "senior-go-engineer-acme-1a2b",
      "location": "Berlin, Germany",
      "apply_url": "https://boards.greenhouse.io/acme/jobs/123",
      "posted_at": "2026-06-18T00:00:00Z"
    }
  ],
  "meta": {
    "total": 4
  }
}

The captured ATS application form for the posting.

The questions a candidate will have to answer, shaped for reading. Only captured for a minority of postings (roughly a sixth of technical ATS platforms are readable at all) — no captured form is a 404, distinguishable from an unknown slug (also 404, but at the job lookup rather than the form lookup).

Path Parameters
  • slug
    Type: string
    required

    The job public_slug.

Responses
  • application/json
Request Example for get/jobs/{slug}/apply-form
curl https://freehire.me/api/v1/jobs/senior-go-engineer-acme-1a2b/apply-form
{
  "data": {
    "provider": "greenhouse",
    "basics": [
      "resume",
      "phone",
      "linkedin"
    ],
    "questions": [
      {
        "text": "Why do you want to work here?",
        "required": true
      }
    ]
  }
}

AI analysis (Collapsed)

Personalized signals computed against the caller’s profile or stored CV. All accept the session cookie or an API key. The skill-match endpoint is deterministic (no LLM); the match-analysis endpoints run the LLM chain and draw on your daily allowance. All take the same facet filter params as search where they narrow a market or candidate set.

Companies (Collapsed)

Public reads. A company detail also returns a page of its open jobs.

Geography (Collapsed)

Public reference data backing search and profile city autocompletes.

Geography Operations

Company feedback (Collapsed)

Signed-in users leave a 1–5 star rating plus category and text about a company, shown under their site-wide pseudonymous persona (never a user id). Reads are public; writes are cookie-only. A reader can report a specific review, and a moderator can hide it.

Company feedback Operations

Authentication (Collapsed)

Register/login set the session cookie and return the user. Logout clears it. me resolves the caller (cookie or API key). OAuth sign-in is a redirect flow. Credential endpoints are rate-limited.

In-app assistant (Collapsed)

The same agent the web app and browser extension chat with, over HTTP: create a conversation, then post messages to it. All routes accept the session cookie or an API key (autopilot is cookie-only — see below) and act on a session you own; someone else’s session id answers 404, same as one that never existed. Turns run one at a time per session — a message sent while one is already running queues rather than running alongside it. preset selects which conversation this is: chat (default), profile (the experience interviewer), browse (a browsing session held from the extension’s side panel), interview (a mock-interview rehearsal) or debrief (after a real one) — the last two must name the vacancy via ?job=<slug>, an application you already hold. A tailor session cannot be created here; it is minted by the CV tailoring bootstrap, which knows the CV and vacancy to bind it to.

Job submissions (Collapsed)

Any signed-in user can submit a vacancy for moderation and read their own queue. The review actions are moderator-only; approval mints a live job.

Job reports (Collapsed)

Any signed-in user can flag a problem with a live vacancy. Review actions are moderator-only; resolving may soft-close the reported job.

Job reports Operations

Ghost job reports (Collapsed)

One person states they applied to a posting and were never answered, feeding the ghost-signal evidence used elsewhere in the API (see the reality field on a job). Distinct from Job reports above: nothing here reaches a moderator and nothing here closes the job directly.

Screening answers (Collapsed)

The six candidate-stated facts that repeat across ATS application forms and no CV can supply. A singleton per user — no id in the path. PUT is a partial update: a field the body omits keeps its stored value.

Screening answers Operations

Onboarding survey (Collapsed)

What you told us about your search when you signed up: how far along it is, the single biggest thing in its way, and what you earn today. A singleton per user — no id in the path. These answers describe you to us and reach no employer and no job search; what you WANT to be paid is a screening answer (desired_salary_*), not one of these. PUT is a partial update: a field the body omits keeps its stored value, and there is no way to clear one back to unstated.

Onboarding survey Operations

Activity & shared job lists (Collapsed)

Two public reads — the catalogue-activity time series and a shared job list by slug. A published list exposes no owner identity.

Activity & shared job lists Operations

Account, plan & extension (Collapsed)

The rest of the account surface: the password, deleting the account, the plan and what it allows today, and the two endpoints the browser extension runs on. Password and deletion are session-only — an API key must not be able to change or destroy the credential it would outlive.

Votes, notifications & discussions (Collapsed)

The lighter per-user surfaces: a vote on a job or company, the account-level notification rule (gates the saved-job apply reminder and both lifecycle nudges), and the public discussion threads. Votes and notification settings take a key; posting to a thread is browser-owned.

Market insights & stats (Collapsed)

Aggregate market intelligence, all public and all unauthenticated. Every figure is served from a precomputed rollup rather than counted live, and nothing here exposes a record-level field or a user identifier — these are counts over the catalogue, not a way to read it.

Employee referrals (Collapsed)

Two sides of the same marketplace: an employee offers to refer into the company they work at (proof required, moderated), and a candidate asks one of that company's approved referrers for an intro. A candidate never sees who the referrer is, and a referrer sees the candidate's CV only through a short-lived view. Moderation of offers is moderator-gated.

Talent Network (Collapsed)

A public, anonymised catalogue of candidates who opted into being found. Membership is a single toggle on users, distinct from user_profiles; a member is addressed by a minted handle, never by their account id or username. Every string a public response carries is a value a dictionary resolved, a number or a date — never text copied from a CV — so no name, employer, institution or free-text field appears anywhere in it.

CV builder & tailoring (Collapsed)

Your CVs, and the tailoring flow that reframes one toward a single vacancy. Authoring is browser-owned — create, update and delete are session-only — while reading a CV, patching it, and rendering the PDF also accept an API key, because that is the half a tailoring agent drives. Every CV is owner-scoped: an id you do not own is a 404, not a 403.

Experience bank (Collapsed)

The durable record a CV is built from: employments, and the evidence atoms under them. Every atom carries who asserted it — you, or a model that inferred it — and only your own assertions may be written into a CV. Reading accepts an API key; editing is session-only.

Application mail (Collapsed)

Your job mail, and the link between a message and the application it belongs to. Mail arrives three ways: a connected Gmail account, the hosted freehire address, or a batch your own client pushed. Only the first two are classified by freehire — pushed mail is yours to triage, which is what makes that tier free. Everything here takes a full-scope API key, so a harness can drive the whole surface; the Gmail consent redirect is the one exception and stays browser-only.

Speech (Collapsed)

Turns one recording into text for the assistant composer’s dictation control. Nothing is stored — the audio is read, forwarded to the transcription gateway, and dropped.

Tailor your CV for this role?

We couldn't check your fit for this role — add a CV to your profile to see it next time.

A new version of freehire is available