DEV Community

Cover image for Integration with Ashby public jobs API
Željko Šević
Željko Šević

Posted on • Originally published at sevic.dev

Integration with Ashby public jobs API

Ashby is an applicant tracking system (ATS) used by many startups and scale-ups. Ashby exposes a lightweight, unauthenticated Job Postings API so you can list published openings on a custom careers page or aggregate jobs into a job board.

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 Greenhouse, Workable, and Lever posts.

Prerequisites

  • Node.js version 26
  • A company's Ashby job board name (see below)
  • No API key required for the public posting API

Find the job board name

Open the company's Ashby-hosted careers page. The URL looks like https://jobs.ashbyhq.com/{JOB_BOARD_NAME}. The last path segment is the identifier you pass to the API.

Examples: notion for Notion (https://jobs.ashbyhq.com/notion), ashby for Ashby itself.

Ashby also offers authenticated endpoints (for example 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.

API overview

Item Value
Base URL https://api.ashbyhq.com/posting-api/job-board/{JOB_BOARD_NAME}
Auth None for read access
Format JSON
Optional query includeCompensation=true adds salary bands when the employer exposes them

Common fields on each job in jobs[]:

Field Description
title Job title
location Primary location label
secondaryLocations Additional offices or regions
isRemote, workplaceType Remote / hybrid / on-site hints
isListed When false, the role is unlisted and should not appear on a public board
jobUrl, applyUrl Hosted Ashby pages
descriptionPlain Plain-text description (when provided)
publishedAt Publish timestamp

Basic integration

Fetch all published jobs for one board:

const boardName = process.env.ASHBY_BOARD_NAME ?? 'notion';
const url = new URL(
  `https://api.ashbyhq.com/posting-api/job-board/${encodeURIComponent(boardName)}`,
);
url.searchParams.set('includeCompensation', 'true');

const response = await fetch(url);

if (!response.ok) {
  throw new Error(`Ashby API ${response.status}: ${response.statusText}`);
}

const data = await response.json();
const jobs = data.jobs ?? [];

for (const job of jobs) {
  console.log(job.title, '-', job.location, '-', job.jobUrl);
}
Enter fullscreen mode Exit fullscreen mode

Filter to roles that are safe to show publicly and have a link:

const publicJobs = jobs.filter(
  (job) => (job.isListed ?? true) && job.title && (job.jobUrl || job.applyUrl),
);
Enter fullscreen mode Exit fullscreen mode

Normalize to a stable shape

Different ATS products use different field names. Map Ashby jobs into a schema your app owns:

function normalizeAshbyJob(job, companyName) {
  const locations = [job.location, ...(job.secondaryLocations ?? []).map((s) => s.location)]
    .map((value) => value?.trim())
    .filter(Boolean);

  const isRemote =
    Boolean(job.isRemote) ||
    job.workplaceType?.toLowerCase() === 'remote' ||
    locations.some((loc) => /remote/i.test(loc));

  return {
    id: job.id ?? `${job.title}:${job.publishedAt}`,
    title: job.title.trim(),
    company: companyName,
    location: locations.join(' | ') || 'Unknown',
    isRemote,
    url: job.jobUrl || job.applyUrl,
    postedAt: job.publishedAt ? new Date(job.publishedAt) : null,
    description: job.descriptionPlain ?? '',
    compensation: job.compensation ?? null,
  };
}
Enter fullscreen mode Exit fullscreen mode

When the primary location is generic (Remote, Hybrid) but postal data includes a country, append the country so filters still work:

function enrichLocation(location, country) {
  const label = location?.trim();
  const c = country?.trim();
  if (!label || !c) return label ?? '';
  if (/^(remote|hybrid|on-?site)$/i.test(label) && !label.toLowerCase().includes(c.toLowerCase())) {
    return `${label}, ${c}`;
  }
  return label;
}
Enter fullscreen mode Exit fullscreen mode

Ashby may return "European Union" in address.postalAddress.addressCountry; treat that as a region hint, not a country code.

Need help with your project?

Get personalized advice on your architecture, code, or career in a 45-minute 1-on-1 consultation.

Book a consultation

Top comments (0)