{"componentChunkName":"component---src-gatsby-theme-chronoblog-templates-note-js","path":"/notes/workable-public-jobs-api-nodejs/","result":{"data":{"mdx":{"parent":{"__typename":"File","fields":{"gitLogLatestDate":"2026-07-01 22:50:32 +0200"}},"id":"e067d650-d327-52e7-a221-2321e526a876","excerpt":"Workable  is an ATS with a public careers layer you can read without authentication. The widget-style JSON endpoint powers Workable-hosted…","frontmatter":{"title":"Integration with Workable public jobs API","date":"2026-07-01 09:00:00 UTC","job_ad":null,"job_ad_id":null,"job_ad_url":null,"tags":["workable","ats","api","node"],"cover":{"childImageSharp":{"fluid":{"base64":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABQAAAAKCAYAAAC0VX7mAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAAf0lEQVQoz9WRXQ4DIQiEvf9N1T5UXFD8m4002xP40E5CwjcTCAkOh+WOL1xr4WSdv5BSR5EBVbUSGdgeUUet0zilDuYJrROZhmXC48uZus2oLrjgGUQNOWcQEdJb4f2FGAV8NaT04e0LN7xiQQhsvPPNMbB5pcx/eMrTPPq5L994MhdpOJi9JAAAAABJRU5ErkJggg==","aspectRatio":2.0869565217391304,"src":"/static/97ef65d781a329b224eebe14e00a9740/c4ecb/cover.png","srcSet":"/static/97ef65d781a329b224eebe14e00a9740/57ab0/cover.png 192w,\n/static/97ef65d781a329b224eebe14e00a9740/f4739/cover.png 384w,\n/static/97ef65d781a329b224eebe14e00a9740/c4ecb/cover.png 768w","srcWebp":"/static/97ef65d781a329b224eebe14e00a9740/dd090/cover.webp","srcSetWebp":"/static/97ef65d781a329b224eebe14e00a9740/ae504/cover.webp 192w,\n/static/97ef65d781a329b224eebe14e00a9740/fef30/cover.webp 384w,\n/static/97ef65d781a329b224eebe14e00a9740/dd090/cover.webp 768w","sizes":"(max-width: 768px) 100vw, 768px","presentationWidth":768,"presentationHeight":366},"resize":{"src":"/static/97ef65d781a329b224eebe14e00a9740/c4ecb/cover.png"}}}},"fields":{"slug":"/notes/workable-public-jobs-api-nodejs/","readingTime":{"text":"4 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 Workable public jobs API\",\n  \"date\": \"2026-07-01 09:00:00 UTC\",\n  \"cover\": \"./cover.png\",\n  \"tags\": [\"workable\", \"ats\", \"api\", \"node\"],\n  \"canonical_url\": \"https://sevic.dev/notes/workable-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.workable.com/\"\n  }), \"Workable\"), \" is an ATS with a public careers layer you can read without authentication. The widget-style JSON endpoint powers Workable-hosted career sites and embeddable job widgets.\"), mdx(\"p\", null, \"This post shows how to list published jobs for one Workable account, request full descriptions, and normalize location and remote fields. 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/greenhouse-public-jobs-api-nodejs/\"\n  }), \"Greenhouse\"), \", 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, \"Workable's authenticated \", mdx(\"a\", _extends({\n    parentName: \"p\"\n  }, {\n    \"href\": \"https://workable.readme.io/\"\n  }), \"REST API v3\"), \" (\", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"https://{subdomain}.workable.com/spi/v3/\"), \") requires a bearer token and is meant for HR integrations, not public job aggregation.\"), 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 Workable \", mdx(\"strong\", {\n    parentName: \"li\"\n  }, \"account slug\"), \" (see below)\"), mdx(\"li\", {\n    parentName: \"ul\"\n  }, \"No API key required for the public widget endpoint\")), mdx(\"h3\", {\n    \"id\": \"find-the-account-slug\"\n  }, \"Find the account slug\"), mdx(\"p\", null, \"Workable career pages use URLs like \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"https://apply.workable.com/{account_slug}/\"), \" or legacy \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"https://{account_slug}.workable.com/\"), \". The slug is the account identifier in API paths.\"), mdx(\"p\", null, \"Examples: \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"huggingface\"), \" for Hugging Face, \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"flosum\"), \" for Flosum.\"), 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  }), \"Jobs (with details)\"), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"GET https://www.workable.com/api/accounts/{slug}?details=true\"))), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Locations\"), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"GET .../accounts/{slug}/locations\"))), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Departments\"), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"GET .../accounts/{slug}/departments\"))), 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\")), 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(\"p\", null, \"Node \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"fetch\"), \" may follow a redirect to \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"https://apply.workable.com/api/v1/widget/accounts/{slug}?details=true\"), \"; both URLs return the same payload.\"), mdx(\"p\", null, \"Set \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"details=true\"), \" to include \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"description\"), \" and \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"full_description\"), \" on each job. Without it you get summary fields only.\"), 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  }, \"url\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"shortlink\")), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Apply links\")), 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(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"locations\")), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Structured location objects\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"experience\")), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Seniority label (for example \", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"Mid-Senior level\"), \")\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"published_on\"), \", \", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"created_at\")), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"Timestamps\")), mdx(\"tr\", {\n    parentName: \"tbody\"\n  }, mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"state\")), mdx(\"td\", _extends({\n    parentName: \"tr\"\n  }, {\n    \"align\": null\n  }), \"On public feeds this is often a \", mdx(\"strong\", {\n    parentName: \"td\"\n  }, \"region name\"), \", not listing status - prefer \", mdx(\"inlineCode\", {\n    parentName: \"td\"\n  }, \"published_on\"), \" to detect live roles\")))), mdx(\"h3\", {\n    \"id\": \"basic-integration\"\n  }, \"Basic integration\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"const accountSlug = process.env.WORKABLE_ACCOUNT_SLUG ?? 'huggingface';\\nconst url = new URL(\\n  `https://www.workable.com/api/accounts/${encodeURIComponent(accountSlug)}`,\\n);\\nurl.searchParams.set('details', 'true');\\n\\nconst response = await fetch(url);\\n\\nif (!response.ok) {\\n  throw new Error(`Workable 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?.location_str, '-', job.url);\\n}\\n\")), mdx(\"p\", null, \"Keep only published listings:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"function isPublished(job) {\\n  if (job.published_on?.trim()) return true;\\n  return !job.state || job.state === 'published';\\n}\\n\\nconst publicJobs = (data.jobs ?? []).filter(\\n  (job) => job.title && (job.url || job.shortlink) && isPublished(job),\\n);\\n\")), mdx(\"h3\", {\n    \"id\": \"locations-and-remote-detection\"\n  }, \"Locations and remote detection\"), mdx(\"p\", null, \"Prefer \", mdx(\"inlineCode\", {\n    parentName: \"p\"\n  }, \"location.location_str\"), \" when present. Otherwise build a label from structured fields:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"function stringifyLocation(entry) {\\n  const city = entry.city?.trim();\\n  const region = entry.state_code?.trim() || entry.region?.trim();\\n  const country = entry.country_name?.trim() || entry.country?.trim();\\n\\n  if (city && region && country) return `${city}, ${region}, ${country}`;\\n  if (city && country) return `${city}, ${country}`;\\n  return country || city || '';\\n}\\n\\nfunction resolveLocation(job) {\\n  const primary = job.location?.location_str?.trim();\\n  if (primary) return primary;\\n\\n  const parts = (job.locations ?? [])\\n    .map(stringifyLocation)\\n    .filter(Boolean);\\n\\n  return parts.join(' / ') || 'Unknown';\\n}\\n\\nfunction isRemoteJob(job, locationLabel) {\\n  const structured =\\n    Boolean(job.location?.telecommuting) ||\\n    job.location?.workplace_type?.toLowerCase() === 'remote' ||\\n    (job.locations ?? []).some(\\n      (loc) =>\\n        Boolean(loc.telecommuting) ||\\n        loc.workplace_type?.toLowerCase() === 'remote',\\n    );\\n\\n  return structured || /remote/i.test(locationLabel);\\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 normalizeWorkableJob(job, companyName) {\\n  const location = resolveLocation(job);\\n  const description = `${job.description ?? ''} ${job.full_description ?? ''}`.trim();\\n\\n  return {\\n    id: job.id ?? job.shortcode,\\n    title: job.title.trim(),\\n    company: companyName,\\n    location,\\n    isRemote: isRemoteJob(job, location),\\n    url: job.url || job.shortlink,\\n    postedAt: job.created_at\\n      ? new Date(job.created_at)\\n      : job.published_on\\n        ? new Date(job.published_on)\\n        : null,\\n    experience: job.experience ?? null,\\n    description,\\n  };\\n}\\n\")), mdx(\"p\", null, \"Optional companion calls enrich filters:\"), mdx(\"pre\", null, mdx(\"code\", _extends({\n    parentName: \"pre\"\n  }, {\n    \"className\": \"language-js\"\n  }), \"const [locationsRes, departmentsRes] = await Promise.all([\\n  fetch(`https://www.workable.com/api/accounts/${accountSlug}/locations`),\\n  fetch(`https://www.workable.com/api/accounts/${accountSlug}/departments`),\\n]);\\n\\nconst { locations } = await locationsRes.json();\\nconst { departments } = await departmentsRes.json();\\n\")));\n}\n;\nMDXContent.isMDXComponent = true;"}},"pageContext":{"id":"e067d650-d327-52e7-a221-2321e526a876"}},"staticQueryHashes":["1961101537","2542493696"]}