{"id":6492,"date":"2026-07-03T18:16:58","date_gmt":"2026-07-03T11:16:58","guid":{"rendered":"https:\/\/daiilynews.cu.ma\/?p=6492"},"modified":"2026-07-03T18:16:58","modified_gmt":"2026-07-03T11:16:58","slug":"integration-with-workable-public-jobs-api","status":"publish","type":"post","link":"https:\/\/daiilynews.cu.ma\/?p=6492","title":{"rendered":"Integration with Workable public jobs API"},"content":{"rendered":"<p> <br \/>\n<br \/>\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.<\/p>\n<p>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 Ashby, Greenhouse, and Lever posts.<\/p>\n<p>Workable&#8217;s authenticated REST API v3 (https:\/\/{subdomain}.workable.com\/spi\/v3\/) requires a bearer token and is meant for HR integrations, not public job aggregation.<\/p>\n<p>  Prerequisites<\/p>\n<p>Node.js version 26<br \/>\nA company&#8217;s Workable account slug (see below)<br \/>\nNo API key required for the public widget endpoint<\/p>\n<p>  Find the account slug<\/p>\n<p>Workable career pages use URLs like https:\/\/apply.workable.com\/{account_slug}\/ or legacy https:\/\/{account_slug}.workable.com\/. The slug is the account identifier in API paths.<\/p>\n<p>Examples: huggingface for Hugging Face, flosum for Flosum.<\/p>\n<p>  API overview<\/p>\n<p>Item<br \/>\nValue<\/p>\n<p>Jobs (with details)<br \/>\nGET https:\/\/www.workable.com\/api\/accounts\/{slug}?details=true<\/p>\n<p>Locations<br \/>\nGET &#8230;\/accounts\/{slug}\/locations<\/p>\n<p>Departments<br \/>\nGET &#8230;\/accounts\/{slug}\/departments<\/p>\n<p>Auth<br \/>\nNone<\/p>\n<p>Format<br \/>\nJSON<\/p>\n<p>Node fetch may follow a redirect to https:\/\/apply.workable.com\/api\/v1\/widget\/accounts\/{slug}?details=true; both URLs return the same payload.<\/p>\n<p>Set details=true to include description and full_description on each job. Without it you get summary fields only.<\/p>\n<p>Common fields on each job in jobs():<\/p>\n<p>Field<br \/>\nDescription<\/p>\n<p>title<br \/>\nJob title<\/p>\n<p>url, shortlink<\/p>\n<p>Apply links<\/p>\n<p>location, locations<\/p>\n<p>Structured location objects<\/p>\n<p>experience<br \/>\nSeniority label (for example Mid-Senior level)<\/p>\n<p>published_on, created_at<\/p>\n<p>Timestamps<\/p>\n<p>state<br \/>\nOn public feeds this is often a region name, not listing status &#8211; prefer published_on to detect live roles<\/p>\n<p>  Basic integration<\/p>\n<p>const accountSlug = process.env.WORKABLE_ACCOUNT_SLUG ?? &#8216;huggingface&#8217;;<br \/>\nconst url = new URL(<br \/>\n  `https:\/\/www.workable.com\/api\/accounts\/${encodeURIComponent(accountSlug)}`,<br \/>\n);<br \/>\nurl.searchParams.set(&#8216;details&#8217;, &#8216;true&#8217;);<\/p>\n<p>const response = await fetch(url);<\/p>\n<p>if (!response.ok) {<br \/>\n  throw new Error(`Workable API ${response.status}: ${response.statusText}`);<br \/>\n}<\/p>\n<p>const data = await response.json();<\/p>\n<p>for (const job of data.jobs ?? ()) {<br \/>\n  console.log(job.title, &#8216;-&#8216;, job.location?.location_str, &#8216;-&#8216;, job.url);<br \/>\n}<\/p>\n<p>    Enter fullscreen mode<\/p>\n<p>    Exit fullscreen mode<\/p>\n<p>Keep only published listings:<\/p>\n<p>function isPublished(job) {<br \/>\n  if (job.published_on?.trim()) return true;<br \/>\n  return !job.state || job.state === &#8216;published&#8217;;<br \/>\n}<\/p>\n<p>const publicJobs = (data.jobs ?? ()).filter(<br \/>\n  (job) => job.title &#038;&#038; (job.url || job.shortlink) &#038;&#038; isPublished(job),<br \/>\n);<\/p>\n<p>    Enter fullscreen mode<\/p>\n<p>    Exit fullscreen mode<\/p>\n<p>  Locations and remote detection<\/p>\n<p>Prefer location.location_str when present. Otherwise build a label from structured fields:<\/p>\n<p>function stringifyLocation(entry) {<br \/>\n  const city = entry.city?.trim();<br \/>\n  const region = entry.state_code?.trim() || entry.region?.trim();<br \/>\n  const country = entry.country_name?.trim() || entry.country?.trim();<\/p>\n<p>  if (city &#038;&#038; region &#038;&#038; country) return `${city}, ${region}, ${country}`;<br \/>\n  if (city &#038;&#038; country) return `${city}, ${country}`;<br \/>\n  return country || city || &#8221;;<br \/>\n}<\/p>\n<p>function resolveLocation(job) {<br \/>\n  const primary = job.location?.location_str?.trim();<br \/>\n  if (primary) return primary;<\/p>\n<p>  const parts = (job.locations ?? ())<br \/>\n    .map(stringifyLocation)<br \/>\n    .filter(Boolean);<\/p>\n<p>  return parts.join(&#8216; \/ &#8216;) || &#8216;Unknown&#8217;;<br \/>\n}<\/p>\n<p>function isRemoteJob(job, locationLabel) {<br \/>\n  const structured =<br \/>\n    Boolean(job.location?.telecommuting) ||<br \/>\n    job.location?.workplace_type?.toLowerCase() === &#8216;remote&#8217; ||<br \/>\n    (job.locations ?? ()).some(<br \/>\n      (loc) =><br \/>\n        Boolean(loc.telecommuting) ||<br \/>\n        loc.workplace_type?.toLowerCase() === &#8216;remote&#8217;,<br \/>\n    );<\/p>\n<p>  return structured || \/remote\/i.test(locationLabel);<br \/>\n}<\/p>\n<p>    Enter fullscreen mode<\/p>\n<p>    Exit fullscreen mode<\/p>\n<p>  Normalize to a stable shape<\/p>\n<p>function normalizeWorkableJob(job, companyName) {<br \/>\n  const location = resolveLocation(job);<br \/>\n  const description = `${job.description ?? &#8221;} ${job.full_description ?? &#8221;}`.trim();<\/p>\n<p>  return {<br \/>\n    id: job.id ?? job.shortcode,<br \/>\n    title: job.title.trim(),<br \/>\n    company: companyName,<br \/>\n    location,<br \/>\n    isRemote: isRemoteJob(job, location),<br \/>\n    url: job.url || job.shortlink,<br \/>\n    postedAt: job.created_at<br \/>\n      ? new Date(job.created_at)<br \/>\n      : job.published_on<br \/>\n        ? new Date(job.published_on)<br \/>\n        : null,<br \/>\n    experience: job.experience ?? null,<br \/>\n    description,<br \/>\n  };<br \/>\n}<\/p>\n<p>    Enter fullscreen mode<\/p>\n<p>    Exit fullscreen mode<\/p>\n<p>Optional companion calls enrich filters:<\/p>\n<p>const (locationsRes, departmentsRes) = await Promise.all((<br \/>\n  fetch(`https:\/\/www.workable.com\/api\/accounts\/${accountSlug}\/locations`),<br \/>\n  fetch(`https:\/\/www.workable.com\/api\/accounts\/${accountSlug}\/departments`),<br \/>\n));<\/p>\n<p>const { locations } = await locationsRes.json();<br \/>\nconst { departments } = await departmentsRes.json();<\/p>\n<p>    Enter fullscreen mode<\/p>\n<p>    Exit fullscreen mode<\/p>\n<p>  Need help with your project?<\/p>\n<p>Get personalized advice on your architecture, code, or career in a 45-minute 1-on-1 consultation.<\/p>\n<p>\u2192 Book a consultation<\/p>\n<p><br \/>\n<br \/><a href=\"https:\/\/dev.to\/zsevic\/integration-with-workable-public-jobs-api-3nk4\">Source link <\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>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. 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 Ashby, [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":6493,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[676],"tags":[1087,2293,761,765,762,763,764,1092,760,2292],"class_list":["post-6492","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-tech-ai","tag-api","tag-ats","tag-coding","tag-community","tag-development","tag-engineering","tag-inclusive","tag-node","tag-software","tag-workable"],"_links":{"self":[{"href":"https:\/\/daiilynews.cu.ma\/index.php?rest_route=\/wp\/v2\/posts\/6492","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/daiilynews.cu.ma\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/daiilynews.cu.ma\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/daiilynews.cu.ma\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/daiilynews.cu.ma\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=6492"}],"version-history":[{"count":0,"href":"https:\/\/daiilynews.cu.ma\/index.php?rest_route=\/wp\/v2\/posts\/6492\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/daiilynews.cu.ma\/index.php?rest_route=\/wp\/v2\/media\/6493"}],"wp:attachment":[{"href":"https:\/\/daiilynews.cu.ma\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=6492"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/daiilynews.cu.ma\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=6492"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/daiilynews.cu.ma\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=6492"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}