homeresume
 
   

Integration with Lever public jobs API

Published July 2, 2026Last updated July 2, 20264 min read

Lever is an ATS with a public Postings API for read-only access to published job listings. No API key is required - you only need the company's Lever site slug.

This post covers fetching postings with pagination, optional filters, and EU vs US API hosts. For other ATS public feeds, see the Ashby, Greenhouse, and Workable posts.

Lever's authenticated Data API (https://api.lever.co/v1/...) manages candidates and pipeline data and is separate from the public postings feed.

Prerequisites

  • Node.js version 26
  • A company's Lever site slug (see below)
  • No API key required for the Postings API

Find the site slug

Lever-hosted career pages use https://jobs.lever.co/{site_slug}/. The first path segment after jobs.lever.co is the slug passed to the API.

Examples: unlimithttps://jobs.lever.co/unlimit/, API https://api.lever.co/v0/postings/unlimit.

Some accounts are hosted in the EU region and answer on https://api.eu.lever.co/v0/postings/{site_slug} instead.

API overview

ItemValue
US basehttps://api.lever.co/v0/postings/{site_slug}
EU basehttps://api.eu.lever.co/v0/postings/{site_slug}
AuthNone
FormatJSON array (mode=json)

Common query parameters:

ParameterDescription
mode=jsonReturn JSON instead of HTML
skip, limitPagination (limit defaults to 100)
team, department, location, commitmentFilters; repeat a key for multiple values
groupGroup results by location, team, or commitment

Each posting includes id, text (title), hostedUrl, applyUrl, categories (team, department, location), workplaceType, country, and plain-text description fields.

Basic integration

Fetch one page of postings:

const siteSlug = process.env.LEVER_SITE_SLUG ?? 'unlimit';
const url = new URL(`https://api.lever.co/v0/postings/${encodeURIComponent(siteSlug)}`);
url.searchParams.set('mode', 'json');
url.searchParams.set('limit', '100');
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Lever API ${response.status}: ${response.statusText}`);
}
const postings = await response.json();
for (const posting of postings) {
console.log(posting.text, '-', posting.categories?.location, '-', posting.hostedUrl);
}

Paginate until a page returns fewer rows than your limit:

const PAGE_SIZE = 100;
async function fetchAllLeverPostings(siteSlug, baseUrl = 'https://api.lever.co/v0/postings') {
const all = [];
let skip = 0;
for (;;) {
const url = new URL(`${baseUrl}/${encodeURIComponent(siteSlug)}`);
url.searchParams.set('mode', 'json');
url.searchParams.set('skip', String(skip));
url.searchParams.set('limit', String(PAGE_SIZE));
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Lever API ${response.status}`);
}
const page = await response.json();
if (!Array.isArray(page) || page.length === 0) break;
all.push(...page);
if (page.length < PAGE_SIZE) break;
skip += PAGE_SIZE;
}
return all;
}

If the US host returns 404, retry against the EU host:

const EU_BASE = 'https://api.eu.lever.co/v0/postings';
const US_BASE = 'https://api.lever.co/v0/postings';
async function fetchPostingsWithRegionFallback(siteSlug) {
try {
return await fetchAllLeverPostings(siteSlug, US_BASE);
} catch (error) {
if (String(error.message).includes('404')) {
return fetchAllLeverPostings(siteSlug, EU_BASE);
}
throw error;
}
}

Filter at the source

Request only roles in a team or location:

url.searchParams.append('team', 'Engineering');
url.searchParams.append('location', 'Berlin');
url.searchParams.append('location', 'Remote');

Fetch a single posting:

const detailUrl = `https://api.lever.co/v0/postings/${siteSlug}/${postingId}?mode=json`;

Normalize to a stable shape

function buildDescription(posting) {
const parts = [
posting.descriptionPlain,
posting.openingPlain,
posting.descriptionBodyPlain,
posting.additionalPlain,
].filter(Boolean);
for (const list of posting.lists ?? []) {
if (list.text && list.content) {
parts.push(`${list.text}\n${list.content.replace(/<[^>]+>/g, ' ')}`);
}
}
return parts.join('\n\n');
}
function normalizeLeverPosting(posting, companyName) {
const primary = posting.categories?.location?.trim() ?? '';
const extras = (posting.categories?.allLocations ?? []).filter(
(loc) => loc.trim().toLowerCase() !== primary.toLowerCase(),
);
const location = [primary, ...extras].filter(Boolean).join(' | ') || 'Unknown';
const isRemote =
posting.workplaceType?.toLowerCase() === 'remote' ||
/remote/i.test(location);
return {
id: posting.id ?? `${posting.text}:${posting.createdAt}`,
title: posting.text.trim(),
company: companyName,
location,
country: posting.country ?? null,
isRemote,
url: posting.hostedUrl || posting.applyUrl,
postedAt: posting.createdAt ? new Date(posting.createdAt) : null,
team: posting.categories?.team ?? null,
commitment: posting.categories?.commitment ?? null,
description: buildDescription(posting),
};
}

Only published postings appear in this API. Confidential or internal-only roles are never returned.