{"componentChunkName":"component---src-gatsby-theme-chronoblog-templates-note-js","path":"/notes/ashby-public-jobs-api-nodejs/","result":{"data":{"mdx":{"parent":{"__typename":"File","fields":{"gitLogLatestDate":"2026-07-01 22:50:32 +0200"}},"id":"13f5c37d-d0b6-5f9d-8893-9d7fc8b530aa","excerpt":"Ashby  is an applicant tracking system (ATS) used by many startups and scale-ups. Ashby exposes a lightweight,  unauthenticated   Job…","frontmatter":{"title":"Integration with Ashby public jobs API","date":"2026-06-29 09:00:00 UTC","job_ad":null,"job_ad_id":null,"job_ad_url":null,"tags":["ashby","ats","api","node"],"cover":{"childImageSharp":{"fluid":{"base64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAAe0lEQVQoz9VRWwrDMAzr/W+adB+1XcdzXipNF9gB8rEJBJYljEEbFmNbfrD3jpVc/yFTgaUKdx80q2Aug/5uSFpxZ5K2oYUf/1uLfPLescWgYMoQETATmAx7VMSoSFpAhyOEE3RkqGa89jS8e6/no2ferP1DKXOY+LmWL4TPF3APlLClAAAAAElFTkSuQmCC","aspectRatio":2.0869565217391304,"src":"/static/0aef0cdcde597a27191254ce06dc5240/c4ecb/cover.png","srcSet":"/static/0aef0cdcde597a27191254ce06dc5240/57ab0/cover.png 192w,\n/static/0aef0cdcde597a27191254ce06dc5240/f4739/cover.png 384w,\n/static/0aef0cdcde597a27191254ce06dc5240/c4ecb/cover.png 768w","srcWebp":"/static/0aef0cdcde597a27191254ce06dc5240/dd090/cover.webp","srcSetWebp":"/static/0aef0cdcde597a27191254ce06dc5240/ae504/cover.webp 192w,\n/static/0aef0cdcde597a27191254ce06dc5240/fef30/cover.webp 384w,\n/static/0aef0cdcde597a27191254ce06dc5240/dd090/cover.webp 768w","sizes":"(max-width: 768px) 100vw, 768px","presentationWidth":768,"presentationHeight":366},"resize":{"src":"/static/0aef0cdcde597a27191254ce06dc5240/c4ecb/cover.png"}}}},"fields":{"slug":"/notes/ashby-public-jobs-api-nodejs/","readingTime":{"text":"3 min read"}},"body":"function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }\n\nfunction _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; }\n\nfunction _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; }\n\n/* @jsxRuntime classic */\n\n/* @jsx mdx */\nvar _frontmatter = {\n  \"title\": \"Integration with Ashby public jobs API\",\n  \"date\": \"2026-06-29 09:00:00 UTC\",\n  \"cover\": \"./cover.png\",\n  \"tags\": [\"ashby\", \"ats\", \"api\", \"node\"],\n  \"canonical_url\": \"https://sevic.dev/notes/ashby-public-jobs-api-nodejs/\"\n};\nvar layoutProps = {\n  _frontmatter: _frontmatter\n};\nvar MDXLayout = \"wrapper\";\nreturn function MDXContent(_ref) {\n  var components = _ref.components,\n      props = _objectWithoutProperties(_ref, [\"components\"]);\n\n  return mdx(MDXLayout, _extends({}, layoutProps, props, {\n    components: components,\n    mdxType: \"MDXLayout\"\n  }), mdx(\"p\", null, mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://www.ashbyhq.com/\"\n  }), \"Ashby\"), \" is an applicant tracking system (ATS) used by many startups and scale-ups. Ashby exposes a lightweight, \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"unauthenticated\"), \" \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://developers.ashbyhq.com/docs/public-job-posting-api\"\n  }), \"Job Postings API\"), \" so you can list published openings on a custom careers page or aggregate jobs into a job board.\"), mdx(\"p\", null, \"This post shows how to fetch public job listings from Ashby with Node.js, normalize the response, and filter to listed roles only. For other ATS public feeds, see the \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://sevic.dev/notes/greenhouse-public-jobs-api-nodejs/\"\n  }), \"Greenhouse\"), \", \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://sevic.dev/notes/workable-public-jobs-api-nodejs/\"\n  }), \"Workable\"), \", and \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://sevic.dev/notes/lever-public-jobs-api-nodejs/\"\n  }), \"Lever\"), \" posts.\"), mdx(\"h3\", {\n    \"id\": \"prerequisites\"\n  }, \"Prerequisites\"), mdx(\"ul\", null, mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"Node.js version 26\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"A company's Ashby \", mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"job board name\"), \" (see below)\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"No API key required for the public posting API\")), mdx(\"h3\", {\n    \"id\": \"find-the-job-board-name\"\n  }, \"Find the job board name\"), mdx(\"p\", null, \"Open the company's Ashby-hosted careers page. The URL looks like \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"https://jobs.ashbyhq.com/{JOB_BOARD_NAME}\"), \". The last path segment is the identifier you pass to the API.\"), mdx(\"p\", null, \"Examples: \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"notion\"), \" for Notion (\", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"https://jobs.ashbyhq.com/notion\"), \"), \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"ashby\"), \" for Ashby itself.\"), mdx(\"p\", null, \"Ashby also offers authenticated endpoints (for example \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"jobPosting.list\"), \") for customers who need filters or private data. The public API documented here is read-only and scoped to one job board at a time.\"), mdx(\"h3\", {\n    \"id\": \"api-overview\"\n  }, \"API overview\"), mdx(\"table\", null, mdx(\"thead\", {\n    parentName: \"table\"\n  }, mdx(\"tr\", {\n    parentName: \"thead\"\n  }, mdx(\"th\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Item\"), mdx(\"th\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Value\"))), mdx(\"tbody\", {\n    parentName: \"table\"\n  }, mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Base URL\"), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"https://api.ashbyhq.com/posting-api/job-board/{JOB_BOARD_NAME}\"))), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Auth\"), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"None for read access\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Format\"), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"JSON\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Optional query\"), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"includeCompensation=true\"), \" adds salary bands when the employer exposes them\")))), mdx(\"p\", null, \"Common fields on each job in \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"jobs[]\"), \":\"), mdx(\"table\", null, mdx(\"thead\", {\n    parentName: \"table\"\n  }, mdx(\"tr\", {\n    parentName: \"thead\"\n  }, mdx(\"th\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Field\"), mdx(\"th\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Description\"))), mdx(\"tbody\", {\n    parentName: \"table\"\n  }, mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"title\")), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Job title\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"location\")), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Primary location label\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"secondaryLocations\")), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Additional offices or regions\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"isRemote\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"workplaceType\")), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Remote / hybrid / on-site hints\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"isListed\")), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"When \", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"false\"), \", the role is unlisted and should not appear on a public board\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"jobUrl\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"applyUrl\")), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Hosted Ashby pages\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"descriptionPlain\")), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Plain-text description (when provided)\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"publishedAt\")), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Publish timestamp\")))), mdx(\"h3\", {\n    \"id\": \"basic-integration\"\n  }, \"Basic integration\"), mdx(\"p\", null, \"Fetch all published jobs for one board:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"const boardName = process.env.ASHBY_BOARD_NAME ?? 'notion';\\nconst url = new URL(\\n  `https://api.ashbyhq.com/posting-api/job-board/${encodeURIComponent(boardName)}`,\\n);\\nurl.searchParams.set('includeCompensation', 'true');\\n\\nconst response = await fetch(url);\\n\\nif (!response.ok) {\\n  throw new Error(`Ashby API ${response.status}: ${response.statusText}`);\\n}\\n\\nconst data = await response.json();\\nconst jobs = data.jobs ?? [];\\n\\nfor (const job of jobs) {\\n  console.log(job.title, '-', job.location, '-', job.jobUrl);\\n}\\n\")), mdx(\"p\", null, \"Filter to roles that are safe to show publicly and have a link:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"const publicJobs = jobs.filter(\\n  (job) => (job.isListed ?? true) && job.title && (job.jobUrl || job.applyUrl),\\n);\\n\")), mdx(\"h3\", {\n    \"id\": \"normalize-to-a-stable-shape\"\n  }, \"Normalize to a stable shape\"), mdx(\"p\", null, \"Different ATS products use different field names. Map Ashby jobs into a schema your app owns:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"function normalizeAshbyJob(job, companyName) {\\n  const locations = [job.location, ...(job.secondaryLocations ?? []).map((s) => s.location)]\\n    .map((value) => value?.trim())\\n    .filter(Boolean);\\n\\n  const isRemote =\\n    Boolean(job.isRemote) ||\\n    job.workplaceType?.toLowerCase() === 'remote' ||\\n    locations.some((loc) => /remote/i.test(loc));\\n\\n  return {\\n    id: job.id ?? `${job.title}:${job.publishedAt}`,\\n    title: job.title.trim(),\\n    company: companyName,\\n    location: locations.join(' | ') || 'Unknown',\\n    isRemote,\\n    url: job.jobUrl || job.applyUrl,\\n    postedAt: job.publishedAt ? new Date(job.publishedAt) : null,\\n    description: job.descriptionPlain ?? '',\\n    compensation: job.compensation ?? null,\\n  };\\n}\\n\")), mdx(\"p\", null, \"When the primary location is generic (\", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"Remote\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"Hybrid\"), \") but postal data includes a country, append the country so filters still work:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"function enrichLocation(location, country) {\\n  const label = location?.trim();\\n  const c = country?.trim();\\n  if (!label || !c) return label ?? '';\\n  if (/^(remote|hybrid|on-?site)$/i.test(label) && !label.toLowerCase().includes(c.toLowerCase())) {\\n    return `${label}, ${c}`;\\n  }\\n  return label;\\n}\\n\")), mdx(\"p\", null, \"Ashby may return \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"\\\"European Union\\\"\"), \" in \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"address.postalAddress.addressCountry\"), \"; treat that as a region hint, not a country code.\"));\n}\n;\nMDXContent.isMDXComponent = true;"}},"pageContext":{"id":"13f5c37d-d0b6-5f9d-8893-9d7fc8b530aa"}},"staticQueryHashes":["1961101537","2542493696"]}