{
  "$type": "site.standard.document",
  "bskyPostRef": {
    "cid": "bafyreiesai3nzibriluarmob37ywb7kuu5gsm642lxfv7744ni7ottlfge",
    "uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3mptnfnw4hhl2"
  },
  "coverImage": {
    "$type": "blob",
    "ref": {
      "$link": "bafkreievif5sntbeomupn4g4uxv4zonlpmkkwyef2galg2xe4qbfrzap7y"
    },
    "mimeType": "image/webp",
    "size": 18392
  },
  "path": "/zsevic/integration-with-lever-public-jobs-api-2mnn",
  "publishedAt": "2026-07-04T17:43:06.000Z",
  "site": "https://dev.to",
  "tags": [
    "lever",
    "ats",
    "api",
    "node",
    "Postings API",
    "Ashby",
    "Greenhouse",
    "Workable",
    "Book a consultation"
  ],
  "textContent": "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.\n\nThis 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.\n\nLever's authenticated **Data API** (`https://api.lever.co/v1/...`) manages candidates and pipeline data and is separate from the public postings feed.\n\n###  Prerequisites\n\n  * Node.js version 26\n  * A company's Lever **site slug** (see below)\n  * No API key required for the Postings API\n\n\n\n###  Find the site slug\n\nLever-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.\n\nExamples: `unlimit` → `https://jobs.lever.co/unlimit/`, API `https://api.lever.co/v0/postings/unlimit`.\n\nSome accounts are hosted in the EU region and answer on `https://api.eu.lever.co/v0/postings/{site_slug}` instead.\n\n###  API overview\n\nItem | Value\n---|---\nUS base | `https://api.lever.co/v0/postings/{site_slug}`\nEU base | `https://api.eu.lever.co/v0/postings/{site_slug}`\nAuth | None\nFormat | JSON array (`mode=json`)\n\nCommon query parameters:\n\nParameter | Description\n---|---\n`mode=json` | Return JSON instead of HTML\n`skip`, `limit` | Pagination (`limit` defaults to 100)\n`team`, `department`, `location`, `commitment` | Filters; repeat a key for multiple values\n`group` | Group results by `location`, `team`, or `commitment`\n\nEach posting includes `id`, `text` (title), `hostedUrl`, `applyUrl`, `categories` (team, department, location), `workplaceType`, `country`, and plain-text description fields.\n\n###  Basic integration\n\nFetch one page of postings:\n\n\n\n    const siteSlug = process.env.LEVER_SITE_SLUG ?? 'unlimit';\n    const url = new URL(`https://api.lever.co/v0/postings/${encodeURIComponent(siteSlug)}`);\n    url.searchParams.set('mode', 'json');\n    url.searchParams.set('limit', '100');\n\n    const response = await fetch(url);\n\n    if (!response.ok) {\n      throw new Error(`Lever API ${response.status}: ${response.statusText}`);\n    }\n\n    const postings = await response.json();\n\n    for (const posting of postings) {\n      console.log(posting.text, '-', posting.categories?.location, '-', posting.hostedUrl);\n    }\n\n\nPaginate until a page returns fewer rows than your limit:\n\n\n\n    const PAGE_SIZE = 100;\n\n    async function fetchAllLeverPostings(siteSlug, baseUrl = 'https://api.lever.co/v0/postings') {\n      const all = [];\n      let skip = 0;\n\n      for (;;) {\n        const url = new URL(`${baseUrl}/${encodeURIComponent(siteSlug)}`);\n        url.searchParams.set('mode', 'json');\n        url.searchParams.set('skip', String(skip));\n        url.searchParams.set('limit', String(PAGE_SIZE));\n\n        const response = await fetch(url);\n        if (!response.ok) {\n          throw new Error(`Lever API ${response.status}`);\n        }\n\n        const page = await response.json();\n        if (!Array.isArray(page) || page.length === 0) break;\n\n        all.push(...page);\n        if (page.length < PAGE_SIZE) break;\n        skip += PAGE_SIZE;\n      }\n\n      return all;\n    }\n\n\nIf the US host returns `404`, retry against the EU host:\n\n\n\n    const EU_BASE = 'https://api.eu.lever.co/v0/postings';\n    const US_BASE = 'https://api.lever.co/v0/postings';\n\n    async function fetchPostingsWithRegionFallback(siteSlug) {\n      try {\n        return await fetchAllLeverPostings(siteSlug, US_BASE);\n      } catch (error) {\n        if (String(error.message).includes('404')) {\n          return fetchAllLeverPostings(siteSlug, EU_BASE);\n        }\n        throw error;\n      }\n    }\n\n\n###  Filter at the source\n\nRequest only roles in a team or location:\n\n\n\n    url.searchParams.append('team', 'Engineering');\n    url.searchParams.append('location', 'Berlin');\n    url.searchParams.append('location', 'Remote');\n\n\nFetch a single posting:\n\n\n\n    const detailUrl = `https://api.lever.co/v0/postings/${siteSlug}/${postingId}?mode=json`;\n\n\n###  Normalize to a stable shape\n\n\n    function buildDescription(posting) {\n      const parts = [\n        posting.descriptionPlain,\n        posting.openingPlain,\n        posting.descriptionBodyPlain,\n        posting.additionalPlain,\n      ].filter(Boolean);\n\n      for (const list of posting.lists ?? []) {\n        if (list.text && list.content) {\n          parts.push(`${list.text}\\n${list.content.replace(/<[^>]+>/g, ' ')}`);\n        }\n      }\n\n      return parts.join('\\n\\n');\n    }\n\n    function normalizeLeverPosting(posting, companyName) {\n      const primary = posting.categories?.location?.trim() ?? '';\n      const extras = (posting.categories?.allLocations ?? []).filter(\n        (loc) => loc.trim().toLowerCase() !== primary.toLowerCase(),\n      );\n      const location = [primary, ...extras].filter(Boolean).join(' | ') || 'Unknown';\n      const isRemote =\n        posting.workplaceType?.toLowerCase() === 'remote' ||\n        /remote/i.test(location);\n\n      return {\n        id: posting.id ?? `${posting.text}:${posting.createdAt}`,\n        title: posting.text.trim(),\n        company: companyName,\n        location,\n        country: posting.country ?? null,\n        isRemote,\n        url: posting.hostedUrl || posting.applyUrl,\n        postedAt: posting.createdAt ? new Date(posting.createdAt) : null,\n        team: posting.categories?.team ?? null,\n        commitment: posting.categories?.commitment ?? null,\n        description: buildDescription(posting),\n      };\n    }\n\n\nOnly **published** postings appear in this API. Confidential or internal-only roles are never returned.\n\n###  Need help with your project?\n\nGet personalized advice on your architecture, code, or career in a 45-minute 1-on-1 consultation.\n\n→ Book a consultation",
  "title": "Integration with Lever public jobs API"
}