SpaceX (spacex), Anduril (andurilindustries) and Databricks (databricks) post their jobs on Greenhouse, and Greenhouse publishes every board through a public JSON API, with no key:
curl -s "https://boards-api.greenhouse.io/v1/boards/spacex/jobs" | jq '.meta.total, (.jobs | length)'
That printed 2832 and 2832: the whole board in one response, no paging. Five things to know, measured on 2026-10-27 with a few seconds between requests.
1. The token is not the company's name
for t in anduril andurilindustries xai; do
printf "%s " $t
curl -s -o /dev/null -w "%{http_code}\n" "https://boards-api.greenhouse.io/v1/boards/$t/jobs"
sleep 2
done
That printed 404, 200 and 200. Anduril's board is andurilindustries; anduril does not exist. A 404 here means no such board, and a 200 with an empty list means a board with nothing open, so the two can be told apart.
2. Pay ranges need a flag
The list has no pay field unless you ask with ?pay_transparency=true. Then every job carries pay_input_ranges, in the same response:
curl -s "https://boards-api.greenhouse.io/v1/boards/andurilindustries/jobs?pay_transparency=true" \
| jq '[.jobs[] | select(.pay_input_ranges | length > 0)] | length, .[0].pay_input_ranges[0]'
2,342 of Anduril's 2,520 jobs had a range, the first {"min_cents": 9000000, "max_cents": 11000000, "currency_type": "USD", "title": "US Salary Range"}. The amounts are in cents, a job can carry several ranges ("Zone 2 Pay Range"), and the title does not reliably say the period: 12 ranges titled "HOURLY POSITION" topped out at 60,000. Across the 60 largest Greenhouse boards, 32 publish ranges, on 14,208 of 53,750 jobs. SpaceX publishes none there; 1,518 of its 2,832 jobs write "Pay Range" into the description instead.
3. content=true is twelve times the bytes, escaped twice
The description, departments and offices come only with ?content=true. SpaceX's board is 2.7 MB without it and 32 MB with it, and the HTML arrives entity-encoded:
curl -s "https://boards-api.greenhouse.io/v1/boards/databricks/jobs?content=true" \
| jq -r '.jobs[0].content[0:60]'
That printed <p>Req ID: .... Unescape before stripping tags, or the tag stripper finds no tags and the reader gets <p>.
4. A posting is not a job
Each posting has its own id, and internal_job_id names the job behind it:
curl -s "https://boards-api.greenhouse.io/v1/boards/spacex/jobs" \
| jq '(.jobs | length), ([.jobs[].internal_job_id] | unique | length)'
That printed 2832 and 2125. One SpaceX job is advertised under 13 different titles, all in Hawthorne. Anduril's 2,520 postings are 1,527 jobs, one of them posted 14 times across 7 titles and 5 places. Count openings by internal_job_id, list postings by id.
5. location is one string, offices is a list
Anduril writes up to nine places into one location.name, joined by "; " (196 of its 2,520 postings), while offices lists them separately with its own codes, such as "Costa Mesa, CA (OC-00)". metadata holds each company's own fields: "Employment Type" is "Full-time" at Anduril and "Regular" at SpaceX, next to SpaceX's "Discipline".
Hosted
Greenhouse Jobs Scraper on Apify takes a board token or a company domain, always asks for pay_input_ranges and turns the cents into salaryMin, salaryMax and a period, and unescapes the description. Company Career Page Jobs Scraper takes SpaceX or Anduril by name next to employers on Workday, Oracle and the rest. Every other system's public endpoint, with a tested curl, is in job-board-apis.
Top comments (0)