Greenhouse is a widely used ATS. Its public Job Board API returns published jobs, departments, and offices as JSON – no authentication for read access.
This post covers listing jobs for one company, loading descriptions, and handling location metadata. For other ATS public feeds, see the Ashby, Workable, and Lever posts.
Greenhouse also ships a private Harvest API for full recruiting workflows (candidates, pipelines, offers). That API requires credentials and is out of scope here.
Prerequisites
- Node.js version 26
- A company’s Greenhouse board token (see below)
- No API key required for GET job listings
Find the board token
Greenhouse career pages live at https://boards.greenhouse.io/{board_token}. The path segment after /boards/ is the token.
Examples: stripe → https://boards.greenhouse.io/stripe, API base https://boards-api.greenhouse.io/v1/boards/stripe/jobs.
API overview
Item Value List jobsGET https://boards-api.greenhouse.io/v1/boards/{board_token}/jobs
Single job
GET .../jobs/{job_id}
Auth (read)
None
Auth (apply)
HTTP Basic with Job Board API key – only for POST .../jobs/{id} applications
Useful query parameters on the list endpoint:
Parameter Effectcontent=true
Include HTML job descriptions in the list response
department_id, office_id
Filter by department or office
Each job typically includes id, title, location.name, absolute_url, updated_at, departments, offices, and optionally content and metadata (custom fields the employer exposed to the job board).
Basic integration
List published jobs with descriptions:
const boardToken = process.env.GREENHOUSE_BOARD_TOKEN ?? 'stripe';
const url = new URL(
`https://boards-api.greenhouse.io/v1/boards/${encodeURIComponent(boardToken)}/jobs`,
);
url.searchParams.set('content', 'true');
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Greenhouse API ${response.status}: ${response.statusText}`);
}
const data = await response.json();
for (const job of data.jobs ?? []) {
console.log(job.title, '-', job.location?.name, '-', job.absolute_url);
}
Enter fullscreen mode Exit fullscreen mode
Fetch one job by id when you need the full posting or application questions:
async function getGreenhouseJob(boardToken, jobId) {
const url = `https://boards-api.greenhouse.io/v1/boards/${encodeURIComponent(boardToken)}/jobs/${jobId}?questions=true`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Greenhouse API ${response.status}`);
}
return response.json();
}
Enter fullscreen mode Exit fullscreen mode
Location and metadata
Some boards store only work mode in location.name (Remote, Hybrid; In-Office) while the city and country live in a custom metadata field such as Job Posting Location:
const JOB_POSTING_LOCATION = 'Job Posting Location';
function extractPostingLocation(metadata) {
const item = metadata?.find((m) => m.name?.trim() === JOB_POSTING_LOCATION);
if (!item?.value) return null;
if (Array.isArray(item.value)) {
return item.value.map(String).join(', ');
}
return String(item.value).trim() || null;
}
function resolveLocation(job) {
const primary = job.location?.name?.trim() ?? '';
const fromMetadata = extractPostingLocation(job.metadata);
if (fromMetadata && (!primary || /^(remote|hybrid|on-?site|in-?office)$/i.test(primary))) {
return fromMetadata;
}
return primary || fromMetadata || 'Unknown';
}
Enter fullscreen mode Exit fullscreen mode
Detect remote roles from location text:
function isRemoteJob(job) {
const text = `${job.location?.name ?? ''} ${resolveLocation(job)}`.toLowerCase();
return text.includes('remote') || text.includes('anywhere');
}
Enter fullscreen mode Exit fullscreen mode
Normalize to a stable shape
function normalizeGreenhouseJob(job, companyName) {
const location = resolveLocation(job);
return {
id: String(job.id),
title: job.title.trim(),
company: companyName,
location,
isRemote: isRemoteJob(job),
url: job.absolute_url,
postedAt: job.updated_at ? new Date(job.updated_at) : null,
description: job.content ?? '',
departments: (job.departments ?? []).map((d) => d.name).filter(Boolean),
};
}
Enter fullscreen mode Exit fullscreen mode
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.
Need help with your project?
Get personalized advice on your architecture, code, or career in a 45-minute 1-on-1 consultation.
답글 남기기