Company Job Openings
Open roles published directly by employers on their own careers pages, in one consistent schema.
1,678,727 rows in the dataset
Download the dataset
Get every row this scraper has collected as one CSV or JSONL file. If the data is older than maxAgeHours, the scraper is re-run with its configured settings first. The key needs the datasets:export scope.
DOWNLOAD_ID=$(curl -s -X POST "https://api.pandorlabs.com/v1/datasets/company-job-openings/downloads" \ -H "Authorization: Bearer $PANDORLABS_KEY" \ -H "Content-Type: application/json" \ -d '{"maxAgeHours":24,"format":"csv"}' | jq -r '.id')while true; do STATUS=$(curl -s "https://api.pandorlabs.com/v1/downloads/$DOWNLOAD_ID" \ -H "Authorization: Bearer $PANDORLABS_KEY" | jq -r '.status') [ "$STATUS" != "refreshing" ] && break sleep 5donecurl -s "https://api.pandorlabs.com/v1/downloads/$DOWNLOAD_ID/file" \ -H "Authorization: Bearer $PANDORLABS_KEY" \ -o dataset.csvAt its current 1,678,727 rows, this download costs 16,788 credits when the data is fresh enough, or 33,576 credits when it has to be refreshed first.
The rate is 1 credit per 100 rows cached and 2 credits live, charged on the rows in the dataset when the download becomes ready. A failed refresh is not charged. The file stays downloadable for 24 hours. See pricing.
Start a run
To choose the targets yourself, send the parameters below as input. The call returns 202 with the run; the key needs the scrapers:run scope and the account a positive credit balance.
curl -X POST "https://api.pandorlabs.com/v1/scrapers/company-job-openings/runs" \ -H "Authorization: Bearer $PANDORLABS_KEY" \ -H "Content-Type: application/json" \ -d '{ "input": { "boardSource": "targets", "targets": [ "greenhouse:stripe", "lever:spotify", "ashby:ramp", "smartrecruiters:smartrecruiters", "personio:personio" ], "keywords": [ "engineer", "data", "scraping" ], "remoteOnly": false } }'A run is billed at the live rate once it finishes: 2 credits per 100 rows it collected. A run that collects nothing costs nothing.
Parameters
Every parameter is optional. Anything you leave out uses the default shown here; an unknown name or an invalid value is rejected with a 400 that lists each problem.
Targeting
boardSourceenum
Boards to collect
The list below is one run against named employers. AI data & labeling companies is a maintained list of the data-annotation and model-training vendors (Surge AI, Appen, Cohere, xAI, Encord, …). The registry is every board discovery has ever found — tens of thousands, and where the volume is. Discovery sweeps the crawl index for new ones first, which adds an hour or so to the front of the run.
- Default
targets- Allowed values
targetsai-dataregistrydiscover
targetsstring[]
Boards
One entry per employer, as `board:token` or a pasted careers URL. Boards: greenhouse, lever, ashby, smartrecruiters, personio, workday, workable, breezy, recruitee, bamboohr, teamtailor (a Teamtailor careers site on its own domain is named by host, e.g. `teamtailor:careers.example.com`). Ignored unless the source above is set to this list.
- Default
["greenhouse:stripe","lever:spotify","ashby:ramp","smartrecruiters:smartrecruiters","personio:personio"]- Example
greenhouse:stripe, https://jobs.lever.co/spotify
keywordsstring[]
Title keywords
Keeps only postings whose title contains one of these. Empty keeps everything the board publishes.
- Default
- none
- Example
engineer, data, scraping
remoteOnlyboolean
Remote roles only
Only keeps postings the board explicitly marks remote. Boards that publish no remote flag contribute nothing under this.
- Default
false
postedWithinDaysnumber
Posted within (days)
Drop postings older than this. 0 keeps every open role. A low value turns the run into a "who started hiring this week" feed.
- Default
0- Range
- 0 to 3650
Collection
maxJobsPerBoardnumber
Max jobs per board
A safety stop, not a target. Set above the largest board you expect: a board that hits it is truncated, and the run says so by name.
- Default
25000- Range
- 1 to 100000
maxBoardsnumber
Max boards per run
0 collects every board the source offers. Set it to cap a first registry run, which otherwise reads tens of thousands of boards.
- Default
0- Range
- 0 to 1000000
includeDescriptionboolean
Store job descriptions
Off by default: one large employer's descriptions run to megabytes of repeated boilerplate. On, each is capped.
- Default
false
maxDescriptionCharsnumber
Description cap (characters)
- Default
5000- Range
- 200 to 100000
concurrencynumber
Concurrent boards
Boards are independent, and these are the employers’ own endpoints rather than an aggregator defending itself, so this can be raised further than a search scraper’s. A registry sweep of tens of thousands of boards wants the top of this range.
- Default
8- Range
- 1 to 50
requestDelayMsnumber
Delay between requests (ms)
Only applies between pages of a paged board; a one-shot board makes a single request.
- Default
500- Range
- 0 to 30000
Discovery
discoverSourcesenum[]
Indexes to sweep
Common Crawl is a periodic snapshot and answers in minutes. Wayback is twenty years of the same hosts and holds an order of magnitude more — 367 pages of Lever boards against Common Crawl’s one — but takes about an hour. Lever is only reachable through Wayback and older crawl editions, since it disallows crawling now.
- Default
["commoncrawl"]- Allowed values
commoncrawlwayback
discoverCrawlsnumber
Crawl editions to sweep
Each edition is a partial sample of the web, so boards keep accumulating as more are unioned. Reaching back several years is also the only way Lever boards are found at all — it disallows crawling now, so only older editions hold them.
- Default
4- Range
- 1 to 30
retryFailedboolean
Only retry boards that errored
Reads only the registry boards whose last read ended in an error rather than in an answer — the cheap pass after a big one, since a rate-limited board is unread rather than empty.
- Default
false
skipDeadAfternumber
Skip boards empty this many runs
Registry boards that keep coming back empty are employers who closed the board or changed ATS. Skipping them is what keeps a full sweep from spending most of its requests on the dead. 0 collects everything on file.
- Default
3- Range
- 0 to 100
Input as JSON Schema
The same schema the API validates against. Use it as a tool input schema or to generate a typed client.
{ "type": "object", "additionalProperties": false, "properties": { "boardSource": { "type": "string", "enum": [ "targets", "ai-data", "registry", "discover" ], "title": "Boards to collect", "description": "The list below is one run against named employers. AI data & labeling companies is a maintained list of the data-annotation and model-training vendors (Surge AI, Appen, Cohere, xAI, Encord, …). The registry is every board discovery has ever found — tens of thousands, and where the volume is. Discovery sweeps the crawl index for new ones first, which adds an hour or so to the front of the run.", "default": "targets" }, "targets": { "type": "array", "items": { "type": "string" }, "maxItems": 1000, "title": "Boards", "description": "One entry per employer, as `board:token` or a pasted careers URL. Boards: greenhouse, lever, ashby, smartrecruiters, personio, workday, workable, breezy, recruitee, bamboohr, teamtailor (a Teamtailor careers site on its own domain is named by host, e.g. `teamtailor:careers.example.com`). Ignored unless the source above is set to this list.", "default": [ "greenhouse:stripe", "lever:spotify", "ashby:ramp", "smartrecruiters:smartrecruiters", "personio:personio" ], "examples": [ "greenhouse:stripe, https://jobs.lever.co/spotify" ] }, "keywords": { "type": "array", "items": { "type": "string" }, "maxItems": 1000, "title": "Title keywords", "description": "Keeps only postings whose title contains one of these. Empty keeps everything the board publishes.", "default": [], "examples": [ "engineer, data, scraping" ] }, "remoteOnly": { "type": "boolean", "title": "Remote roles only", "description": "Only keeps postings the board explicitly marks remote. Boards that publish no remote flag contribute nothing under this.", "default": false }, "postedWithinDays": { "type": "number", "minimum": 0, "maximum": 3650, "title": "Posted within (days)", "description": "Drop postings older than this. 0 keeps every open role. A low value turns the run into a \"who started hiring this week\" feed.", "default": 0 }, "maxJobsPerBoard": { "type": "number", "minimum": 1, "maximum": 100000, "title": "Max jobs per board", "description": "A safety stop, not a target. Set above the largest board you expect: a board that hits it is truncated, and the run says so by name.", "default": 25000 }, "maxBoards": { "type": "number", "minimum": 0, "maximum": 1000000, "title": "Max boards per run", "description": "0 collects every board the source offers. Set it to cap a first registry run, which otherwise reads tens of thousands of boards.", "default": 0 }, "discoverSources": { "type": "array", "items": { "type": "string", "enum": [ "commoncrawl", "wayback" ] }, "uniqueItems": true, "title": "Indexes to sweep", "description": "Common Crawl is a periodic snapshot and answers in minutes. Wayback is twenty years of the same hosts and holds an order of magnitude more — 367 pages of Lever boards against Common Crawl’s one — but takes about an hour. Lever is only reachable through Wayback and older crawl editions, since it disallows crawling now.", "default": [ "commoncrawl" ] }, "discoverCrawls": { "type": "number", "minimum": 1, "maximum": 30, "title": "Crawl editions to sweep", "description": "Each edition is a partial sample of the web, so boards keep accumulating as more are unioned. Reaching back several years is also the only way Lever boards are found at all — it disallows crawling now, so only older editions hold them.", "default": 4 }, "retryFailed": { "type": "boolean", "title": "Only retry boards that errored", "description": "Reads only the registry boards whose last read ended in an error rather than in an answer — the cheap pass after a big one, since a rate-limited board is unread rather than empty.", "default": false }, "skipDeadAfter": { "type": "number", "minimum": 0, "maximum": 100, "title": "Skip boards empty this many runs", "description": "Registry boards that keep coming back empty are employers who closed the board or changed ATS. Skipping them is what keeps a full sweep from spending most of its requests on the dead. 0 collects everything on file.", "default": 3 }, "includeDescription": { "type": "boolean", "title": "Store job descriptions", "description": "Off by default: one large employer's descriptions run to megabytes of repeated boilerplate. On, each is capped.", "default": false }, "maxDescriptionChars": { "type": "number", "minimum": 200, "maximum": 100000, "title": "Description cap (characters)", "default": 5000 }, "concurrency": { "type": "number", "minimum": 1, "maximum": 50, "title": "Concurrent boards", "description": "Boards are independent, and these are the employers’ own endpoints rather than an aggregator defending itself, so this can be raised further than a search scraper’s. A registry sweep of tens of thousands of boards wants the top of this range.", "default": 8 }, "requestDelayMs": { "type": "number", "minimum": 0, "maximum": 30000, "title": "Delay between requests (ms)", "description": "Only applies between pages of a paged board; a one-shot board makes a single request.", "default": 500 } }}Run, poll, read rows
The run is asynchronous. Poll it until it reaches a terminal status and its billing is paid, then page through its rows. Until then the rows endpoint answers 409 (still queued or running) or 402 (payment required).
RUN_ID=$(curl -s -X POST "https://api.pandorlabs.com/v1/scrapers/company-job-openings/runs" \ -H "Authorization: Bearer $PANDORLABS_KEY" \ -H "Content-Type: application/json" \ -d '{ "input": { "boardSource": "targets", "targets": [ "greenhouse:stripe", "lever:spotify", "ashby:ramp", "smartrecruiters:smartrecruiters", "personio:personio" ], "keywords": [ "engineer", "data", "scraping" ], "remoteOnly": false } }' | jq -r '.id')while true; do RUN=$(curl -s "https://api.pandorlabs.com/v1/runs/$RUN_ID" \ -H "Authorization: Bearer $PANDORLABS_KEY") STATUS=$(echo "$RUN" | jq -r '.status') BILLING=$(echo "$RUN" | jq -r '.billing.status') case "$STATUS" in queued|running) ;; *) [ "$BILLING" != "pending" ] && break ;; esac sleep 5doneecho "$RUN" | jq '{status, rowCount, billing}'curl -s "https://api.pandorlabs.com/v1/runs/$RUN_ID/rows?page=1&limit=100" \ -H "Authorization: Bearer $PANDORLABS_KEY"See the API reference for pagination, cancelling and listing runs.
Output fields
Rows have no fixed schema; these are the fields observed in the rows collected so far. Coverage is the share of rows where the field is present.
| Field | Type | Coverage |
|---|---|---|
| additional_locations | array | 100% |
| apply_url | string | 70% |
| board | string | 100% |
| company | string | 99% |
| company_token | string | 100% |
| compensation_currency | string | 2% |
| compensation_max | number | 2% |
| compensation_min | number | 2% |
| compensation_text | string | 4% |
| country | string | 26% |
| department | string | 43% |
| employment_type | string | 48% |
| job_id | string | 100% |
| location | string | 98% |
| posting_key | string | 100% |
| remote | boolean | 96% |
| seniority | string | 18% |
| source_url | string | 92% |
| team | string | 10% |
| title | string | 100% |
| workplace_type | string | 10% |
| board_updated_at | date | 9% |
| created_at | date | 100% |
| first_seen_at | date | 100% |
| last_seen_at | date | 100% |
| posted_at | date | 45% |
| updated_at | date | 100% |
| _id | string | 100% |
