---
title: "Greenhouse Job Board API at scale: one token per company"
description: "Greenhouse's Job Board API returns one company's published jobs per call, by board token, with no key and no endpoint that lists the boards."
canonical: https://jobspipe.dev/blog/greenhouse-api-for-many-companies
date_published: 2026-09-30
date_modified: 2026-09-30
author: Dvir Atias
---

# Greenhouse API for many companies: reading thousands of job boards

Greenhouse's Job Board API returns one company's published jobs per call, by board token, with no key and no endpoint that lists the boards. Reading many companies means finding every token, scheduling the calls without a published rate limit, normalising free-text fields and inferring closed jobs from what disappears. The Harvest API is private to one customer and does not help.

The Greenhouse Job Board API is public and keyless, and it reads one company at a time. How the jobs, departments and offices endpoints work, what it costs you in engineering to run them across thousands of boards, how Harvest differs, and when an aggregated jobs API is the simpler path.

Greenhouse’s Job Board API returns one company’s published jobs per call: `GET https://boards-api.greenhouse.io/v1/boards/{board_token}/jobs`, with no key. The `board_token` names one company’s board, and the documentation has no endpoint that lists the boards. Reading many companies therefore means finding each board token yourself and calling each board in turn. This guide covers how the endpoints work, what it takes to run them across thousands of boards, how the Harvest API differs, and when an aggregated jobs API is the simpler path.

## How does the Greenhouse Job Board API work?

The [Job Board API documentation](https://developers.greenhouse.io/job-board.html) describes it as “a simple JSON representation of your company’s offices, departments, and published jobs”, built so that a company can design its own careers page. Reading needs no credential: “Job Board data is publicly available, so authentication is not required for any GET endpoints.” Only submitting an application, a POST to the job’s URL, needs a key.

Every read is scoped to one board. These are the requests that matter for job data:

| Request | What it returns |
| --- | --- |
| GET /v1/boards/{board_token}/jobs | Every published job post on one board: id, title, location, updated_at and the posting URL |
| GET /v1/boards/{board_token}/jobs?content=true | The same list with each post's full description, departments and offices |
| GET /v1/boards/{board_token}/jobs/{job_id} | One job post; ?questions=true adds the application questions, ?pay_transparency=true adds pay ranges |
| GET /v1/boards/{board_token}/departments | The company's departments, each with its jobs; render_as=tree nests them |
| GET /v1/boards/{board_token}/offices | The company's offices, each with its departments and jobs; render_as=tree nests them |
| GET /v1/boards/{board_token} | The board's name and its introduction text |

The list call is the workhorse. Without parameters it returns a light record per job: `id`, `internal_job_id`, `title`, `updated_at`, `requisition_id`, a `location` object with a free-text `name`, the `absolute_url` of the posting, a `language` and a `metadata` field, plus a `meta.total` count. Add `?content=true` and each job also carries its full description in `content` and its `departments` and `offices`:

```
curl "https://boards-api.greenhouse.io/v1/boards/{board_token}/jobs?content=true"

{
  "jobs": [
    {
      "id": 127817,
      "internal_job_id": 144381,
      "title": "Vault Designer",
      "updated_at": "2016-01-14T10:55:28-05:00",
      "location": { "name": "NYC" },
      "absolute_url": "https://boards.greenhouse.io/vaulttec/jobs/127817",
      "content": "This is the job description. ...",
      "departments": [{ "id": 13583, "name": "Department of Departments" }],
      "offices": [{ "id": 8787, "name": "New York City", "location": "New York, NY, United States" }]
    }
  ],
  "meta": { "total": 1 }
}
```

That response is the example from Greenhouse’s documentation, shortened. Two details matter later. The jobs list documents no paging parameters, so a board comes back as one response, and with `content=true` a large employer’s response is large. And the `id` is the job post, while `internal_job_id` is the job behind it: one job can have more than one post.

## What do the departments and offices endpoints add?

`GET /v1/boards/{board_token}/departments` returns “a list of your organization’s departments and jobs”, and `GET /v1/boards/{board_token}/offices` returns the offices with their departments and jobs. Both accept `render_as`: the default, `list`, gives flat objects with `parent_id` and `child_ids`, and `tree` nests children inside parents.

For a careers page these endpoints give you the navigation: Engineering, then Mobile Development, then the jobs. For a data pipeline they are mostly redundant, because `?content=true` on the jobs list already attaches each job’s departments and offices. They earn a call when you want the whole hierarchy, including departments that have no open job today, or when you want to group one company’s jobs by office without parsing location text.

Treat both as hints, not as a taxonomy. Department and office names are whatever each company typed. One company’s “R & D” is another’s “Engineering”, and an office can be a city, a country or a label such as “East Coast”.

## How do you find board tokens for thousands of companies?

This is the first real cost. The documentation defines `board_token` only as the “Job Board URL token”, the segment of a company’s board address, and it describes no request that lists tokens or searches companies. Each token has to come from somewhere else, and you need one per employer.

In practice you are building and maintaining a list. The work looks like this:

-   **Collecting tokens.** The token appears in the address of a Greenhouse-hosted board and in the requests an embedded board makes from a company’s own careers page. For each employer you care about, you find its careers page and read the token from it. A token is often the company name, but not reliably, so guessing produces misses and false matches.
-   **Mapping tokens to companies.** The jobs list does not name the employer; the single-job response carries a `company_name`, and `GET /v1/boards/{board_token}` returns the board’s name. You still have to tie that name to a company record with a domain, and a group with several brands may run several boards.
-   **Keeping the list current.** Companies adopt Greenhouse, leave it, rename their board and get acquired. A token that answered last month can stop answering, and new boards appear that your list has never seen. The list needs its own refresh schedule and its own monitoring.
-   **Following postings home.** The `absolute_url` in the documentation’s own examples is sometimes a Greenhouse address and sometimes the employer’s own careers URL with a `gh_jid` parameter. If you deduplicate or link by URL, you need to handle both.

None of this is difficult for ten companies. For a named list of fifty employers you can collect the tokens in an afternoon. The cost grows with the word “every”: a product that promises every company hiring on Greenhouse is promising a complete and current token list, which is a data product in its own right. Our [companies using Greenhouse](https://jobspipe.dev/blog/companies-using-greenhouse) post shows what that population looks like.

## How fast can you call it, and what does the documentation say about limits?

The Job Board API documentation publishes no rate limit for its GET endpoints. It does not state a number of requests per second, a daily quota or a caching rule. That is not the same as having no limit, and you should not plan around a number you read on a forum. Plan around the absence instead:

-   Set your own ceiling on concurrent requests to the host and keep it modest.
-   Treat a 429 or a 5xx response as a signal to slow down for the whole host, not for one board.
-   Spread a full pass across the day instead of starting every board at the top of the hour.
-   Fetch the light list first and request `content=true` or a single job only when `updated_at` has changed, so that most passes move little data.

The arithmetic is simple and worth doing before you start. One pass is one request per board, or two if you also want pay ranges for changed jobs. How often you repeat the pass sets how stale your data can be: a pass every six hours over several thousand boards is a steady trickle of requests around the clock, plus retries. The engineering is in the scheduler, the retry policy and the alerting, not in the HTTP call.

For contrast, the limits Greenhouse does document are on the Harvest API. Its [documentation](https://developers.greenhouse.io/harvest.html) says requests “are limited to the amount specified in the returned X-RateLimit-Limit header, per 10 seconds”, that exceeding it returns an HTTP 429, and that the response carries a `Retry-After` header. Those rules belong to Harvest. Do not assume they describe the Job Board API.

## How do you handle schema drift across boards?

Every board answers in the same JSON shape, which is the good news. The drift is inside the values, because each company fills them in its own way:

-   **Location is free text.** `location.name` is a string such as “NYC” or “San Francisco, CA” in the documentation’s examples. Turning it into a city, a region, a country and a remote flag is your parser’s job, and companies write the same place many ways.
-   **The description arrives encoded.** The documentation notes that HTML in `content` is “converted into corresponding HTML entities”, so you decode it before you can strip tags or extract skills. Where a company has default descriptions on its board, the content also includes the board-level introduction and conclusion joined to the post.
-   **Pay is a separate request.** Pay ranges are not in the list. The single-job endpoint returns `pay_input_ranges`, with `min_cents`, `max_cents` and `currency_type`, only when you add `pay_transparency=true`, and only for posts where the company entered a range. Otherwise pay is a sentence in the description, or absent.
-   **Custom fields differ per company.** `metadata` holds “any job custom fields you have selected to be exposed”. One board uses it for employment type, another for a team code, most leave it null. You cannot rely on a field being present across boards.
-   **Not every post is a job.** Prospect posts, the “don’t see a job you like?” entries, come back with a null `internal_job_id`, and the sections endpoints list them. Filter them out unless you want them.
-   **Language varies.** Each post has a `language`, and international employers publish the same job in more than one.

A normalising layer has to make these decisions once and apply them to every board: a location resolver, a salary parser for descriptions, a rule for duplicates across languages and offices. When you later add a second applicant tracking system, its feed makes different choices, and the layer grows. Our notes on [how ATS API integrations are structured](https://jobspipe.dev/blog/ats-api-integration) and on [which ATS APIs are public and which are gated](https://jobspipe.dev/blog/public-vs-gated-ats-apis) cover the wider picture.

## How do you detect a closed job on Greenhouse?

The Job Board API returns published job posts, and it has no status field and no event for a job that closes. A job that is filled or withdrawn is simply not in the next response. Closed-job detection is therefore something you infer by comparing one pass with the next, and the inference has traps:

-   **A failed request is not a closure.** If a board times out or returns an error, every job on it is missing from that pass. Mark jobs closed only after a successful response that does not contain them, and consider requiring two such responses in a row.
-   **An empty board is ambiguous.** A company that paused hiring, a board that was renamed and a token that no longer exists can all produce no jobs. A rule that closes everything when a board goes empty will sooner or later close a company’s whole listing by mistake. Add a guard for sudden large drops and look at them before acting.
-   **Reposts look like new jobs.** A company that closes a post and opens a fresh one for the same role gives you a new `id`. The `internal_job_id` helps tie posts to the job behind them within one board.
-   **Dates need your own clock.** The list gives `updated_at`, and the single-job response adds `first_published` and, in the documentation’s example, an `application_deadline`. None of these tells you when a job closed. Store the time you first saw each post and the time you last saw it, and derive the closing time from those.

The quality of this step decides whether your product shows dead listings. How long postings stay up, and how to check an old one, is covered in [finding expired job postings](https://jobspipe.dev/blog/find-expired-job-postings).

## Greenhouse Job Board API vs Harvest API: which one do you need?

The two are often confused because both are called the Greenhouse API. They answer different questions for different people:

| What is compared | Job Board API | Harvest API |
| --- | --- | --- |
| Built for | Showing a company's published jobs on a careers page | A customer exporting and updating its own recruiting data |
| Scope of one credential | None needed to read; one board token names one company's board | One Greenhouse organisation, never another company's |
| Authentication | None for GET requests; Basic Auth only to submit an application | OAuth credentials created inside the customer's account |
| What you can read | Published job posts, departments, offices and the board introduction | Candidates, applications, jobs, offers, users and more |
| Documented rate limit | None published | The X-RateLimit-Limit response header, counted over ten-second windows |

Harvest is the API for a company’s own account. Its documentation says it “was designed to allow our customers to export their data from Greenhouse”, and it warns that a Harvest credential can read all the data behind the endpoints it is granted. A Harvest credential is created by an administrator inside one Greenhouse organisation and reaches that organisation only. No Harvest credential reads another company’s jobs, so Harvest is not a route to data across companies however many keys you hold.

Harvest is also changing. The documentation page carries a notice that “the Harvest v1/v2 API is deprecated and will be removed on August 31, 2026” and points to Harvest v3, whose [authentication guide](https://harvestdocs.greenhouse.io/docs/authentication) uses OAuth 2.0: client credentials for a customer’s own integration and the authorization code flow for partners. If you are integrating with one customer’s recruiting data, read the v3 documentation, not an older tutorial. If you want job postings from many companies, Harvest is the wrong API and the Job Board API is the right one.

## When is an aggregated jobs API the simpler path?

Call Greenhouse directly when you need a known, short list of employers, or only your own company’s board. It is free, it is documented, and nothing sits between you and the source. The [Greenhouse source page](https://jobspipe.dev/sources/greenhouse) has the official documentation links and a request you can run.

Consider an aggregated API when the requirement contains “every” or “and also”: every company on Greenhouse, and also the employers on other applicant tracking systems and the job boards. At that point the token list, the scheduler, the normalising layer and the closed-job logic above are most of the project, and they repeat for each new source. These are the published prices and coverage claims of APIs that include postings from company career sites, checked September 2026:

| API | What the data is | Price entry point | Free tier or trial | Published coverage | Checked |
| --- | --- | --- | --- | --- | --- |
| JobsPipe · Pricing ↗ | Aggregated postings | $49/month for 25,000 jobs | 1,000 jobs to start | Greenhouse boards as one of 30+ sources, selected with a source filter | September 2026 |
| Fantastic.jobs · Pricing ↗ | Aggregated postings | $95/mo for 20,000 jobs, up to $900/mo for 500,000 | 7-day free trial | 200,000+ career sites plus 10M+ board jobs a month | September 2026 |
| TheirStack · Pricing ↗ | Postings plus company data | $49/mo for 1,500 credits, up to $5,500/mo for 5M | Free plan: 50 company and 200 API credits a month | 225M job records, 305k new a day | September 2026 |
| Coresignal · Pricing ↗ | Postings plus company data | API plans $49 to $5,000/month | 7-day free trial, 2,000 credits | 482M+ job postings | September 2026 |
| JobDataLake · Pricing ↗ | Aggregated postings | $50 for 100,000 credits, up to $400 for 4M | 1,000 free credits | 1.8M+ jobs from 25,000+ companies | September 2026 |

The coverage column is each vendor’s own description of its corpus, not a count of Greenhouse boards: Fantastic.jobs describes jobs from 200,000+ career sites, plus over 10 million jobs a month from LinkedIn, Wellfound and Y Combinator, and none of these vendors publishes a Greenhouse-only figure. Ask each one for the companies you care about and compare the answers with the boards themselves. The wider field is in the [jobs API comparison](https://jobspipe.dev/blog/best-jobs-api-2026).

## How do you get Greenhouse jobs from the JobsPipe API?

JobsPipe is one such API. It serves postings from 30+ sources through one endpoint in one record shape, and Greenhouse is one of the sources you can select. The sandbox shows the shape with no key:

Try it without a key

```
curl -d '{"limit":3}' https://api.jobspipe.dev/v1/sandbox/jobs/search
```

Only `/v1/sandbox/*` needs no key.

With a free key, `source_or` keeps only postings whose source is Greenhouse. This asks for product manager roles in the United States posted in the last two weeks:

```
curl https://api.jobspipe.dev/v1/jobs/search \
  -H "Authorization: Bearer jp_live_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "source_or": ["greenhouse"],
    "job_title_or": ["product manager"],
    "job_country_code_or": ["US"],
    "posted_at_max_age_days": 14,
    "limit": 25
  }'
```

Each record carries `job_title`, `company`, `company_domain`, `url`, `date_posted`, `last_seen_at`, `status` with `closed_at` once a posting closes, `cities` and `state_code`, and `min_annual_salary_usd` and `max_annual_salary_usd` when the posting states a range. You do not pass a board token; to narrow to one employer, add `company_name_partial_match_or`. The free plan includes 1,000 jobs to start.

## Methodology: how this guide was checked

JobsPipe wrote this guide, and JobsPipe sells a jobs API that appears in the comparison table. Every statement about Greenhouse’s endpoints, fields, authentication and limits was read from Greenhouse’s own documentation on September 30, 2026, and the example response is Greenhouse’s. Where the documentation is silent, as it is on Job Board rate limits, this guide says so instead of quoting a number. Vendor prices and coverage claims come from each vendor’s published pages, stored with the wording and the date checked. The sections on token lists, scheduling and closed-job detection describe the work any team reading many boards has to do; they are engineering guidance, not a description of any vendor’s system.

## Sources

-   [Greenhouse Job Board API documentation](https://developers.greenhouse.io/job-board.html)
-   [Greenhouse Harvest API documentation](https://developers.greenhouse.io/harvest.html)
-   [Greenhouse Harvest v3 authentication](https://harvestdocs.greenhouse.io/docs/authentication)
-   [Fantastic.jobs API page](https://fantastic.jobs/api)
-   [TheirStack pricing](https://theirstack.com/en/pricing)
-   [Coresignal pricing](https://coresignal.com/pricing/)
-   [JobDataLake](https://jobdatalake.com/)

## Frequently Asked Questions

### Does Greenhouse have one API for all companies' jobs?

No. The Job Board API reads one board at a time: every request carries a board token that names one company's board, and the documentation describes no request that lists boards or searches across companies. To read many companies you call each board separately, which means you first need each company's token.

### What is a Greenhouse board token?

It is the identifier of one company's job board, described in Greenhouse's documentation as the Job Board URL token. It appears in the address of a Greenhouse-hosted board and goes into the API path, as in https://boards-api.greenhouse.io/v1/boards/{board\_token}/jobs. It is often the company's name in lowercase, but not always.

### Does the Greenhouse Job Board API need an API key?

Not for reading. Greenhouse's documentation says Job Board data is publicly available and authentication is not required for any GET endpoint. Only submitting an application through the API needs a Job Board API key, sent with Basic Auth.

### What is the rate limit of the Greenhouse Job Board API?

Greenhouse's Job Board API documentation publishes no rate limit for its GET endpoints. The limits Greenhouse documents belong to the Harvest API, which returns its allowance in an X-RateLimit-Limit header counted over ten-second windows. For the Job Board API, set your own modest ceiling and back off when you receive a 429 or a server error.

### Can the Harvest API read jobs from many companies?

No. A Harvest credential is created inside one Greenhouse customer's account and reaches that organisation's data only. It is the right API for a company integrating with its own recruiting data and the wrong one for reading postings across companies.

### How do I know when a Greenhouse job has closed?

The Job Board API returns published posts and has no closed status, so a closed job simply stops appearing. Record when you first and last saw each post, and mark a job closed only after a successful response that no longer contains it. Do not treat a failed request or a suddenly empty board as closures without checking.

### Is there a free way to get Greenhouse jobs from many companies?

Yes, in two ways. The Job Board API itself is free for every board whose token you know. For many companies without managing tokens, JobsPipe's free plan includes 1,000 free credits, one time, and its sandbox needs no key.

Related

## Keep reading

-   [Guide · Mar 12, 2026Companies that use Greenhouse: a notable customers list (2026)](https://jobspipe.dev/blog/companies-using-greenhouse)
-   [Guide · Feb 15, 2026ATS API: one integration across applicant tracking systems](https://jobspipe.dev/blog/ats-api-integration)
-   [Comparison · Jul 16, 2026Which ATS APIs are actually public? The 2026 job-data reference](https://jobspipe.dev/blog/public-vs-gated-ats-apis)
-   [Comparison · Jul 16, 2026The best jobs APIs in 2026: 15 options compared](https://jobspipe.dev/blog/best-jobs-api-2026)
-   [Guide · Feb 5, 2026How to find old job postings: 6 ways to dig up an expired Indeed or LinkedIn listing](https://jobspipe.dev/blog/find-expired-job-postings)


---
Try it with no key: `curl -d '{"limit":3}' https://api.jobspipe.dev/v1/sandbox/jobs/search`. AI agents: the full machine-readable index of this site is https://jobspipe.dev/llms.txt?src=md-twin - API quickstart, MCP server, pricing. Free key (1,000 jobs to start): https://jobspipe.dev/signup