{"componentChunkName":"component---src-gatsby-theme-chronoblog-templates-note-js","path":"/notes/greenhouse-public-jobs-api-nodejs/","result":{"data":{"mdx":{"parent":{"__typename":"File","fields":{"gitLogLatestDate":"2026-07-01 22:50:32 +0200"}},"id":"b9dbac34-1aa7-58da-b39b-a783fdc1a7a2","excerpt":"Greenhouse  is a widely used ATS. Its public  Job Board API  returns published jobs, departments, and offices as JSON - no authentication…","frontmatter":{"title":"Integration with Greenhouse public jobs API","date":"2026-06-30 09:00:00 UTC","job_ad":null,"job_ad_id":null,"job_ad_url":null,"tags":["greenhouse","ats","api","node"],"cover":{"childImageSharp":{"fluid":{"base64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAAgElEQVQoz+WRSw7DIAxEc/+jFogam5+B8KpQqSdgV0uzsJ81Y8kHm+vYbjjnZKf2XyhXp5aBmdGaUcpA5VFHddDsJmpfM6s3OX1Z/LGBakekYzY53CsTY0NEUBVUKt5nvEuEUFZAeHpfyKlzvQ3nEudZFztDWbveZWq5//Ipuw0/4pAX1bOxgxEAAAAASUVORK5CYII=","aspectRatio":2.0869565217391304,"src":"/static/273ddf67c1a00b3de0762a4933f34e88/c4ecb/cover.png","srcSet":"/static/273ddf67c1a00b3de0762a4933f34e88/57ab0/cover.png 192w,\n/static/273ddf67c1a00b3de0762a4933f34e88/f4739/cover.png 384w,\n/static/273ddf67c1a00b3de0762a4933f34e88/c4ecb/cover.png 768w","srcWebp":"/static/273ddf67c1a00b3de0762a4933f34e88/dd090/cover.webp","srcSetWebp":"/static/273ddf67c1a00b3de0762a4933f34e88/ae504/cover.webp 192w,\n/static/273ddf67c1a00b3de0762a4933f34e88/fef30/cover.webp 384w,\n/static/273ddf67c1a00b3de0762a4933f34e88/dd090/cover.webp 768w","sizes":"(max-width: 768px) 100vw, 768px","presentationWidth":768,"presentationHeight":366},"resize":{"src":"/static/273ddf67c1a00b3de0762a4933f34e88/c4ecb/cover.png"}}}},"fields":{"slug":"/notes/greenhouse-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 Greenhouse public jobs API\",\n  \"date\": \"2026-06-30 09:00:00 UTC\",\n  \"cover\": \"./cover.png\",\n  \"tags\": [\"greenhouse\", \"ats\", \"api\", \"node\"],\n  \"canonical_url\": \"https://sevic.dev/notes/greenhouse-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.greenhouse.io/\"\n  }), \"Greenhouse\"), \" is a widely used ATS. Its public \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://developers.greenhouse.io/job-board.html\"\n  }), \"Job Board API\"), \" returns published jobs, departments, and offices as JSON - no authentication for read access.\"), mdx(\"p\", null, \"This post covers listing jobs for one company, loading descriptions, and handling location metadata. For other ATS public feeds, see the \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://sevic.dev/notes/ashby-public-jobs-api-nodejs/\"\n  }), \"Ashby\"), \", \", 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(\"p\", null, \"Greenhouse also ships a private \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Harvest API\"), \" for full recruiting workflows (candidates, pipelines, offers). That API requires credentials and is out of scope here.\"), 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 Greenhouse \", mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"board token\"), \" (see below)\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"No API key required for GET job listings\")), mdx(\"h3\", {\n    \"id\": \"find-the-board-token\"\n  }, \"Find the board token\"), mdx(\"p\", null, \"Greenhouse career pages live at \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"https://boards.greenhouse.io/{board_token}\"), \". The path segment after \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"/boards/\"), \" is the token.\"), mdx(\"p\", null, \"Examples: \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"stripe\"), \" \\u2192 \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"https://boards.greenhouse.io/stripe\"), \", API base \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"https://boards-api.greenhouse.io/v1/boards/stripe/jobs\"), \".\"), 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  }), \"List jobs\"), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"GET https://boards-api.greenhouse.io/v1/boards/{board_token}/jobs\"))), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Single job\"), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"GET .../jobs/{job_id}\"))), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Auth (read)\"), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"None\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Auth (apply)\"), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"HTTP Basic with Job Board API key - only for \", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"POST .../jobs/{id}\"), \" applications\")))), mdx(\"p\", null, \"Useful query parameters on the list endpoint:\"), 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  }), \"Parameter\"), mdx(\"th\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Effect\"))), 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  }, \"content=true\")), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Include HTML job descriptions in the list response\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"department_id\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"office_id\")), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Filter by department or office\")))), mdx(\"p\", null, \"Each job typically includes \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"id\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"title\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"location.name\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"absolute_url\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"updated_at\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"departments\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"offices\"), \", and optionally \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"content\"), \" and \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"metadata\"), \" (custom fields the employer exposed to the job board).\"), mdx(\"h3\", {\n    \"id\": \"basic-integration\"\n  }, \"Basic integration\"), mdx(\"p\", null, \"List published jobs with descriptions:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"const boardToken = process.env.GREENHOUSE_BOARD_TOKEN ?? 'stripe';\\nconst url = new URL(\\n  `https://boards-api.greenhouse.io/v1/boards/${encodeURIComponent(boardToken)}/jobs`,\\n);\\nurl.searchParams.set('content', 'true');\\n\\nconst response = await fetch(url);\\n\\nif (!response.ok) {\\n  throw new Error(`Greenhouse API ${response.status}: ${response.statusText}`);\\n}\\n\\nconst data = await response.json();\\n\\nfor (const job of data.jobs ?? []) {\\n  console.log(job.title, '-', job.location?.name, '-', job.absolute_url);\\n}\\n\")), mdx(\"p\", null, \"Fetch one job by id when you need the full posting or application questions:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"async function getGreenhouseJob(boardToken, jobId) {\\n  const url = `https://boards-api.greenhouse.io/v1/boards/${encodeURIComponent(boardToken)}/jobs/${jobId}?questions=true`;\\n  const response = await fetch(url);\\n  if (!response.ok) {\\n    throw new Error(`Greenhouse API ${response.status}`);\\n  }\\n  return response.json();\\n}\\n\")), mdx(\"h3\", {\n    \"id\": \"location-and-metadata\"\n  }, \"Location and metadata\"), mdx(\"p\", null, \"Some boards store only work mode in \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"location.name\"), \" (\", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"Remote\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"Hybrid; In-Office\"), \") while the city and country live in a custom metadata field such as \", mdx(\"strong\", {\n    parentName: \"p\"\n  }, \"Job Posting Location\"), \":\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"const JOB_POSTING_LOCATION = 'Job Posting Location';\\n\\nfunction extractPostingLocation(metadata) {\\n  const item = metadata?.find((m) => m.name?.trim() === JOB_POSTING_LOCATION);\\n  if (!item?.value) return null;\\n  if (Array.isArray(item.value)) {\\n    return item.value.map(String).join(', ');\\n  }\\n  return String(item.value).trim() || null;\\n}\\n\\nfunction resolveLocation(job) {\\n  const primary = job.location?.name?.trim() ?? '';\\n  const fromMetadata = extractPostingLocation(job.metadata);\\n  if (fromMetadata && (!primary || /^(remote|hybrid|on-?site|in-?office)$/i.test(primary))) {\\n    return fromMetadata;\\n  }\\n  return primary || fromMetadata || 'Unknown';\\n}\\n\")), mdx(\"p\", null, \"Detect remote roles from location text:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"function isRemoteJob(job) {\\n  const text = `${job.location?.name ?? ''} ${resolveLocation(job)}`.toLowerCase();\\n  return text.includes('remote') || text.includes('anywhere');\\n}\\n\")), mdx(\"h3\", {\n    \"id\": \"normalize-to-a-stable-shape\"\n  }, \"Normalize to a stable shape\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"function normalizeGreenhouseJob(job, companyName) {\\n  const location = resolveLocation(job);\\n\\n  return {\\n    id: String(job.id),\\n    title: job.title.trim(),\\n    company: companyName,\\n    location,\\n    isRemote: isRemoteJob(job),\\n    url: job.absolute_url,\\n    postedAt: job.updated_at ? new Date(job.updated_at) : null,\\n    description: job.content ?? '',\\n    departments: (job.departments ?? []).map((d) => d.name).filter(Boolean),\\n  };\\n}\\n\")), mdx(\"p\", null, \"Cache responses when polling. Greenhouse does not publish hard rate limits for the job board API, but aggressive hammering can get blocked - poll on a schedule (for example every few hours) rather than on every page view.\"));\n}\n;\nMDXContent.isMDXComponent = true;"}},"pageContext":{"id":"b9dbac34-1aa7-58da-b39b-a783fdc1a7a2"}},"staticQueryHashes":["1961101537","2542493696"]}