Jobs

Search and retrieve job listings. Filter by skills, location, remote type, salary, and more.

The Jobs endpoint lets you search and retrieve job listings with powerful filtering options.

List Jobs

GET /v1/public/jobs

Returns a paginated list of jobs matching your filters.

curl -H "X-API-Key: $JOBVEN_API_KEY" \
  "https://api.jobven.com/v1/public/jobs?skills%5B%5D=react&remoteType%5B%5D=remote&limit=20"

Query Parameters

ParameterTypeDescription
qstringSearch job titles by whole word, with phrase, OR and exclusion syntax. See Searching
descriptionQuerystringSearch job descriptions for a whole-word phrase. See Searching
skills[]stringFilter by skills, OR logic, up to 10 values (e.g., ?skills[]=react&skills[]=typescript)
industry[]stringFilter by industry, OR logic, up to 10 values (e.g., ?industry[]=technology_software)
languagestringRequired language (ISO 639-1, e.g., fr)
locationstringCity, region, or country name
countrystringCountry code (ISO 3166-1, e.g., US). Case-insensitive
statestringStates or regions, comma-separated (e.g., CA,New York,QC). US states and Canadian provinces match by name or code. Case and accents are ignored
citystringCity names, comma-separated (e.g., San Francisco,Montréal). Case and accents are ignored
remoteType[]enumonsite, remote, hybrid, flexible, OR logic
experienceLevel[]enumentry, mid, senior, lead, executive, OR logic
employmentType[]enumfull_time, part_time, contract, internship, seasonal, volunteer, temporary, per_diem, other, OR logic
minSalarynumberYearly pay of at least this amount (salary.annualMax or annualMin); hourly, weekly and monthly pay is compared per year
maxSalarynumberYearly pay starting at or below this amount (salary.annualMin)
salaryCurrencystringISO 4217 code (e.g. USD, EUR, PLN). Jobs paying in that currency; with minSalary or maxSalary, the amounts are in it
hasVisaSponsorshipbooleantrue: postings that offer visa sponsorship. false: postings that say they do not. A posting that does not mention it matches neither
hasRelocationbooleantrue: postings that offer relocation assistance. false: postings that say they do not. A posting that does not mention it matches neither
statusenumactive (default), closed. pending is deprecated: it is still accepted and matches no jobs
listedAfternumberJobs listed at or after this time, by listedAt (Unix seconds). Use it to sync: see Sync Only New Jobs
postedAfternumberJobs posted at or after this time, by the employer's posting date (Unix seconds)
postedBeforenumberJobs posted at or before this time, by the employer's posting date (Unix seconds)
cursorstringPagination cursor from previous response
limitnumberResults per page. Defaults to your tier's maximum; see Page Size Limits
includeTotalbooleanReturn meta.total, the exact number of matching jobs. Off by default, and slower when on. See Getting a total count
sortByenumpostedAt (default), listedAt, title, company. company sorts by the job's primary company name; a job with several companies still lists them all
orderenumASC, DESC (default)
descriptionFormatenumhtml (default), plain, markdown, all. See Description Formats
See Also: For complete lists of all enum values (industries, experience levels, employment types, etc.), see the Data Types Reference.
Tip: To fetch only new jobs, store the newest listedAt you hold and send it back as listedAfter, minus a few minutes. See Sync Only New Jobs.

Array Parameters

To filter by several values, repeat the parameter, with or without brackets. One value works either way (skills=react or skills[]=react):

GET /v1/public/jobs?skills[]=react&skills[]=typescript&remoteType[]=remote&remoteType[]=hybrid

Searching

Two parameters search text. Everything each one supports:

qdescriptionQuery
SearchesJob titleJob description, as plain text (never the HTML markup)
MatchingWhole words, in any orderOne whole-word phrase, words in the order given
CaseIgnoredIgnored
AccentsMust match: ingenieur does not find "Ingénieur"Must match
Partial wordsNo: kubern does not find "Kubernetes"No
"exact phrase"SupportedOptional: the value is already a phrase, so "distributed systems" and distributed systems are the same
ORSupported: gtm OR enablementNot supported: gtm OR enablement finds only that exact phrase
-word to excludeSupported: engineer -internNot supported: -intern finds the text "-intern"
Dotted or hyphenated wordsSplit into parts: node finds "Node.js", full stack finds "Full-Stack"node finds "Node.js"; full stack and full-stack each find only their own spelling
SymbolsIgnored: c++ searches for "c"Matched as written: c++ finds "C++"
Maximum length200 characters200 characters
Nothing to search (-, "")400 Bad Request"" is no filter; other symbols are matched as text
Blank valueNo filterNo filter
SpeedFastSlower: pair with postedAfter

q examples:

You sendMatches
backend engineerTitles containing both words, in any order: "Senior Backend Engineer", "Engineer, Backend"
"marketing coordinator"The exact phrase, words in that order
gtm OR enablementTitles containing either word
engineer -internTitles with "engineer" but not "intern"
ai"AI Engineer" and "Head of AI/ML", but not "Maintenance Technician"

Both can be combined with each other and with every other filter:

GET /v1/public/jobs?q=backend%20engineer&descriptionQuery=distributed%20systems&postedAfter=1789000000

URL-encode values. A bare + in a query string means a space, so send c++ as descriptionQuery=c%2B%2B.

If a search takes too long to run, the API returns 422 Unprocessable Entity with a message asking you to narrow it. Add postedAfter, or make q or descriptionQuery more specific, and try again.

Description Formats

Job descriptions are stored as sanitized HTML. Use descriptionFormat to request the format your pipeline needs: HTML, plain text, markdown, or all three. All formats are precomputed at ingest time, so responses stay fast even when you request all.

ValueFields populatedUse case
html (default)descriptionRendering in web UIs; backwards-compatible for existing integrations
plaindescriptionPlainSearch indexing, email digests, previews
markdowndescriptionMarkdownMarkdown-native UIs, LLM / RAG pipelines, static site generators
alldescription, descriptionPlain, descriptionMarkdownDual rendering, A/B comparisons

When you request plain or markdown, the HTML description field is omitted, so you only get the format you asked for. all returns every format.

GET /v1/public/jobs?descriptionFormat=markdown

What the HTML contains

The description HTML is structural: paragraphs, headings, lists, tables, emphasis and links. Everything the source site added for its own rendering is removed: scripts, styles, media, form controls, comments, framework attributes, and empty elements. The only attributes kept are href and title on links (an href must be http, https or mailto), and colspan, rowspan and start where they carry structure. Entities are encoded (&,  ).

Links carry no target or rel, so they open in the same tab unless you decide otherwise. To open every link in a new tab, add the attributes when you render:

container.querySelectorAll('a').forEach((a) => {
  a.target = '_blank';
  a.rel = 'noopener';
});

If you sanitize the HTML before rendering (DOMPurify, sanitize-html), do this in a post-sanitize hook, because most sanitizers strip target by default.

{
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "title": "Senior React Developer",
      "descriptionMarkdown": "## About the role\n\nWe are looking for...\n\n- 5+ years React\n- TypeScript fluency"
    }
  ]
}
Formats are deterministic.descriptionPlain uses html-to-text; descriptionMarkdown uses Turndown with ATX headings, - bullets, and fenced code blocks. Output is stable across requests.

Response

{
  "data": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "title": "Senior React Developer",
      "description": "<p>We are looking for...</p>",
      "summary": "Build modern web applications with React",
      "locations": [
        {
          "addressLocality": "San Francisco",
          "addressRegion": "California",
          "addressCountry": "US",
          "workLocation": "hybrid"
        }
      ],
      "remoteType": "hybrid",
      "skills": {
        "primarySkills": ["React", "TypeScript"],
        "secondarySkills": ["Node.js", "PostgreSQL"],
        "softSkills": ["Communication", "Teamwork"]
      },
      "salary": {
        "min": 150000,
        "max": 200000,
        "currency": "USD",
        "period": "annual",
        "annualMin": 150000,
        "annualMax": 200000
      },
      "benefits": ["health_insurance", "pensions_401k", "remote_work_hybrid_work_options"],
      "experienceLevel": "senior",
      "employmentType": "full_time",
      "minYearsExperience": 5,
      "educationRequirement": {
        "required": ["Bachelor's degree"],
        "preferred": ["Master's degree"]
      },
      "industry": ["technology_software"],
      "inLanguage": ["en"],
      "hasVisaSponsorship": true,
      "hasRelocation": false,
      "requiresTravel": false,
      "postedAt": 1705392000,
      "expiresAt": 1708070400,
      "applyUrl": "https://company.com/apply/123",
      "status": "active",
      "companies": [
        {
          "id": 1042,
          "name": "TechCorp Inc.",
          "website": "https://techcorp.com"
        }
      ]
    }
  ],
  "meta": {
    "count": 20,
    "nextCursor": "eyJpZCI6IjEyMyIsInZhbHVlIjoxNzA1MzkyMDAwMDAwfQ==",
    "hasMore": true,
    "requestTimeMs": 45,
    "lastUpdatedAt": "2025-01-15T12:00:00Z"
  }
}

Response Fields

FieldTypeDescription
idstringUnique job identifier (UUID)
titlestringJob title as the employer wrote it, with repeated whitespace collapsed
descriptionstringFull job description (sanitized HTML). Omitted when descriptionFormat=plain or descriptionFormat=markdown
descriptionPlainstringPlain-text description. Returned when descriptionFormat=plain or descriptionFormat=all
descriptionMarkdownstringMarkdown description. Returned when descriptionFormat=markdown or descriptionFormat=all
summarystringBrief summary
locationsarrayLocation objects (see below)
remoteTypestringonsite, remote, hybrid, flexible
skillsobject{ primarySkills, secondarySkills, softSkills }, see Skills Object. The snake_case names (primary_skills and the others) are deprecated and served until December 15, 2026
salaryobject{ min, max, currency, period, annualMin, annualMax }, see Salary Object
benefitsstringList of benefits
experienceLevelstringentry, mid, senior, lead, executive
employmentTypestringfull_time, part_time, contract, etc.
minYearsExperiencenumberMinimum years required. 0 means the posting asks for no experience
educationRequirementobject{ required, preferred }
industrystringIndustry categories
inLanguagestringRequired languages (ISO 639-1)
hasVisaSponsorshipbooleantrue if the posting offers visa sponsorship, false if it says it does not, null if it does not say
hasRelocationbooleantrue if the posting offers relocation assistance, false if it says it does not, null if it does not say
requiresTravelbooleantrue if the posting requires travel, false if it says it does not, null if it does not say
travelPercentagenumberRequired travel as percentage (0-100)
postedAtnumberWhen the employer posted the job (Unix seconds). Jobven adds a posting when this falls within a year of the day Jobven first finds it, or when the employer gives no date
expiresAtnumberThe employer's closing date, when the posting states one (Unix seconds)
listedAtnumberWhen Jobven made the job available (Unix seconds), and again when a closed job reopens
closedAtnumber | nullWhen Jobven closed the job (Unix seconds), about 30 days after the role was last listed. null while the job is open
applyUrlstringURL to apply
statusstringactive: the role is being advertised. closed: the employer's board no longer lists it
companiesarray[{ id, name, website }]. id gets the company's profile from GET /v1/public/company-profiles/{id}
Extracted or enriched? Fields such as salary and postedAt are taken from the posting as written. Others, including experienceLevel, employmentType, minYearsExperience and industry, are AI-enriched and may be inferred when the posting does not state them. See Extracted vs AI-Enriched Fields.

Location Object

FieldTypeDescription
streetAddressstringStreet address
addressLocalitystringCity
addressRegionstringState/Province
postalCodestringPostal/ZIP code
addressCountrystringCountry (ISO 3166-1 alpha-2), or XX. May be absent.
workLocationstringonsite, remote, hybrid, flexible
Special Value: Country code XX means the job carries no location information at all, typically "work from anywhere" positions. A location with a city but no addressCountry is one whose country we couldn't identify, and it matches no country filter. See Data Types for details.

Getting a total count

meta.total is not returned by default. Pass includeTotal=true when you need it:

GET /v1/public/jobs?country=US&includeTotal=true

Counting every matching job is the slowest part of a request, and it gets slower as the dataset grows, so it is opt-in rather than a cost every caller pays. Most integrations never need it: to page through results you use hasMore and nextCursor, and to show progress you count what you have received.

When you do request it, the number is exact for your filters, and may be up to five minutes old. If counting takes too long, meta.total is left out and you still get the page.

Meta Object

FieldTypeDescription
totalnumberExact number of matching jobs. Only present when you pass includeTotal=true
countnumberJobs in this response
nextCursorstringCursor for next page (null if no more)
hasMorebooleanMore results exist
requestTimeMsnumberResponse time in milliseconds
lastUpdatedAtstringWhen dataset was last updated (ISO 8601)

Handling Null Values

Most job fields are optional and may be null. Guaranteed fields: id, title, status, companies, and the description field matching your requested format: description when descriptionFormat=html (default) or all, descriptionPlain when plain, descriptionMarkdown when markdown.

// Safe access pattern
const salary = job.salary?.min
  ? `${job.salary.min.toLocaleString()} ${job.salary.currency ?? ''}+`
  : 'Not specified';

const skills = job.skills?.primarySkills?.join(', ') || 'Not listed';

See Data Types - Handling Null Values for complete guidance.


Get Job by ID

GET /v1/public/jobs/{id}

Retrieve a single job by its UUID.

curl -H "X-API-Key: $JOBVEN_API_KEY" \
  "https://api.jobven.com/v1/public/jobs/550e8400-e29b-41d4-a716-446655440000"

Path Parameters

ParameterTypeDescription
idstringJob UUID

Response

Returns a single job object (same schema as list response items).

Errors

CodeDescription
404Job not found

Pagination

Jobs use cursor-based pagination for efficient traversal of large datasets.

Cursor pagination is more efficient than offset pagination for large datasets. The cursor encodes the position in the result set.

How It Works

  1. Make your first request without a cursor
  2. Use meta.nextCursor from the response for the next page
  3. Continue until meta.hasMore is false
  4. Keep every other parameter the same between pages: filters, sortBy and order. A cursor marks a position in one listing.
let cursor = null;
const allJobs = [];

do {
  const url = new URL('https://api.jobven.com/v1/public/jobs');
  url.searchParams.set('limit', '100');
  if (cursor) url.searchParams.set('cursor', cursor);

  const response = await fetch(url, {
    headers: { 'X-API-Key': process.env.JOBVEN_API_KEY }
  });
  const { data, meta } = await response.json();

  allJobs.push(...data);
  cursor = meta.nextCursor;
} while (cursor);

console.log(`Fetched ${allJobs.length} jobs`);

A cursor that can't be decoded returns 400. Start the walk again without one.

Page Size Limits

TierMax per pageDefault (when limit is omitted)
Free2525
Starter5050
Growth+100100

A limit above your tier's maximum is clamped to it, not rejected. A limit of 0 or below, or a non-integer, returns 400.

See also

  • Webhooks: push the same job lifecycle events (job.created, job.updated, job.closed, job.deleted) to your endpoint in real time instead of polling.