homeresume
 
   

Integration with Ashby public jobs API

Published June 29, 2026Last updated July 1, 20263 min read

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

ItemValue
Base URLhttps://api.ashbyhq.com/posting-api/job-board/{JOB_BOARD_NAME}
AuthNone for read access
FormatJSON
Optional queryincludeCompensation=true adds salary bands when the employer exposes them

Common fields on each job in jobs[]:

FieldDescription
titleJob title
locationPrimary location label
secondaryLocationsAdditional offices or regions
isRemote, workplaceTypeRemote / hybrid / on-site hints
isListedWhen false, the role is unlisted and should not appear on a public board
jobUrl, applyUrlHosted Ashby pages
descriptionPlainPlain-text description (when provided)
publishedAtPublish 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);
}

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),
);

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,
};
}

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;
}

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