# ATS Job Link Verifier — Greenhouse, Lever & Ashby (`kudomiya/ats-job-link-verifier`) Actor

Verify saved Greenhouse, Lever and Ashby job links. Detect missing listings and title changes, with timestamped source evidence and CSV/HTML reports. Duplicates and unknown results have no event fee.

- **URL**: https://apify.com/kudomiya/ats-job-link-verifier.md
- **Developed by:** [Kaito](https://apify.com/kudomiya) (community)
- **Categories:** Jobs, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$5.00 / 1,000 verified jobs

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## ATS Job Link Verifier

Recheck saved **Greenhouse, Lever and Ashby** job links before displaying or recommending them. Get a timestamped finding, source URL, title comparison, and downloadable CSV/HTML evidence report.

Designed for job-board maintainers, recruitment data pipelines and job-search automations that already have a list of links. Use it to review saved listings, maintain a job feed, or check links before recommending them.

### Results you can interpret

| Status | Meaning | Suggested next step |
| --- | --- | --- |
| `listed` | The exact posting ID appeared in the public source response. | Keep the link; inspect a title mismatch if reported. |
| `not_listed` | The ID was absent from a complete, nonempty public board response, or Ashby explicitly marked it unlisted. | Flag the entry for review. Do not automatically assert the role was filled. |
| `unknown` | The source failed, was empty/unverified, changed shape, or hit a limit. | Retry later or inspect manually. Preserve existing records. |
| `invalid_input` | The URL format/provider or job metadata is unsupported. | Supply a supported job-detail URL. |

A public listing can remain visible after hiring stops. An unlisted posting can still accept applications through a direct link. The Actor does **not** verify actual hiring activity, employer legitimacy, applicant eligibility, remote-country restrictions, salary, or application success. Responses are observations, not an atomic database snapshot.

### Input

```json
{
  "jobs": [
    {
      "url": "https://job-boards.greenhouse.io/canonical/jobs/6448444",
      "expectedTitle": "Associate Linux Support Engineer",
      "label": "Saved vacancy"
    }
  ],
  "maxBoards": 30,
  "maxRequests": 100,
  "maxDurationSecs": 120
}
```

The example is a real public URL, not a promise that this posting will remain listed. Any current result is valid evidence for the example.

`jobs` accepts URL strings or objects with `url`, optional `expectedTitle` and optional `label`. Input order is preserved; `inputIndex` is zero-based. The optional title comparison ignores case, Unicode compatibility differences and repeated whitespace. It is exact text comparison, not a semantic or AI match.

#### Supported URL shapes

- `https://job-boards.greenhouse.io/BOARD/jobs/NUMERIC_ID`
- `https://boards.greenhouse.io/BOARD/jobs/NUMERIC_ID`
- `https://jobs.lever.co/BOARD/UUID` (optional `/apply`)
- `https://jobs.eu.lever.co/BOARD/UUID` (optional `/apply`)
- `https://jobs.ashbyhq.com/BOARD/UUID` (optional `/application`)

Query strings and fragments are discarded. Custom employer domains, Greenhouse embed URLs, LinkedIn, Indeed, Workday, iCIMS and generic careers pages are not supported in this version. Unsupported URLs are never fetched.

### How checks work

The Actor derives a board and job ID from each supported URL, then reads the provider's documented public API. It does not fetch arbitrary input URLs, follow source redirects, log into accounts, solve CAPTCHAs, apply to jobs, or collect applicant information.

Inputs from the same company board share a fetch. Lever boards are paginated in batches of 100. A missing ID is classified `not_listed` only after successful collection of the full board. A malformed response, repeated IDs, inconsistent Greenhouse total, failed page or collection cap makes missing IDs `unknown`. Validated presence on an earlier Lever page can still be reported as `listed`.

Boards can change during pagination. There is no transactional snapshot guarantee. Empty boards are `unknown` because an empty response does not reliably distinguish an inactive board from a mistyped company identifier.

Ashby's `isListed: false` produces `not_listed` with `source_marks_unlisted`, even if the posting is still publicly accessible. This is different from absence and is visible in `reason`.

### Output

The default Dataset contains one evidence row per input, unless the spending limit stops delivery. Each row includes:

- `url`, `provider`, `board`, `jobId`, `inputIndex`, `label`;
- `status`, machine-readable `reason`, `checkedAt` in UTC;
- `title`, `location`, `expectedTitle`, nullable `titleMatches`;
- `evidenceUrl` pointing to the public board API;
- `boardComplete`, `boardJobCount`, nullable `httpStatus`;
- nullable `duplicateOf`, pointing to the first equivalent input index.

The key-value store also receives:

- `SUMMARY`: result counts, request count, transferred decoded bytes, duration and omissions;
- `RESULTS.csv`: flattened spreadsheet export with formula-like cells escaped;
- `REPORT.html`: self-contained, printable evidence report without remote assets or scripts.

Sources can change after the report is generated. API links in reports point to the live source, not an archived response. Full board payloads and job descriptions are not retained. Source metadata is limited to 1,000 characters per text field.

### Limits and cost controls

| Setting | Default | Maximum |
| --- | ---: | ---: |
| Input jobs | Required | 1,000 |
| `maxBoards` | 30 | 50 |
| `maxBoardJobs` | 5,000 | 10,000 |
| `maxRequests` | 100 | 200 |
| `concurrency` | 3 | 5 |
| `timeoutSecs` | 20 | 30 |
| `maxDurationSecs` | 120 | 300 |

Decoded responses are capped at 10 MiB each. Requests are paced to at most four starts per second across all sources, with no automatic retries. Rate limits stay `unknown`; rerun later. The source deadline excludes startup and storage/export time. Extra boards and interrupted checks produce explicit unknown rows.

### Pricing and spending limits

The paid event is **Verified job** (`job-verified`). One unique URL with a `listed` or `not_listed` result creates one event. Duplicates, unsupported inputs and `unknown` results create no paid event. Check the Store pricing panel for the current event price and whether platform usage is charged separately.

There is no paid startup event or paid dataset-row event. Set your maximum run cost before starting. If the event budget is exhausted, the Actor stops delivering results before the next paid row. The CSV and HTML contain only delivered rows. `SUMMARY.omittedDueToBudget` and `nextInputIndex` show where delivery stopped.

Source checks happen before result delivery. An event spending limit does not bound the work spent reading sources; use `maxRequests`, `maxBoards` and `maxDurationSecs` as well. A budget below the price of one verified job stops the run before source requests.

### Example finding

```json
{
  "inputIndex": 0,
  "provider": "greenhouse",
  "board": "canonical",
  "jobId": "6448444",
  "status": "listed",
  "reason": "id_present_in_public_board",
  "title": "Associate Linux Support Engineer",
  "titleMatches": true,
  "boardComplete": true,
  "duplicateOf": null
}
```

This is a shortened example from a successful check on 22 October 2026 (Asia/Almaty), not a guarantee about the current listing. Actual rows also include the source URL and UTC observation time.

### Common questions

**Does a missing listing mean the employer stopped hiring?** No. `not_listed` describes public board visibility. Review the posting before deleting it from your own system.

**Can I check a spreadsheet?** Supply its supported job URLs as the `jobs` JSON array. Download `RESULTS.csv` when the run finishes; each output keeps the original input index.

**Why did a link return unknown?** Inspect `reason`. Source errors, empty boards, rate limits and incomplete collection are kept separate from absence. Retry later or inspect manually.

**Can I resume a stopped run?** Start a fresh run with the remaining input tail. Resurrection of an already-started run is rejected to prevent duplicate delivery and charges. After a storage or billing failure, inspect the existing Dataset and charging log before retrying.

**Can I request another ATS?** Open an issue on this Actor with the provider name and a public example URL. Do not include account passwords or applicant information.

### Sources

- [Greenhouse public Job Board API](https://docs.greenhouse.io/job-board.html)
- [Lever Postings API](https://github.com/lever/postings-api)
- [Ashby public Job Postings API](https://developers.ashbyhq.com/docs/public-job-posting-api)

# Actor input Schema

## `jobs` (type: `array`):

JSON array of URL strings or objects with url, optional expectedTitle and label. Maximum 1,000 inputs. Duplicates are marked and share the observation. Tracking query strings are discarded.

## `maxBoards` (type: `integer`):

Extra boards return unknown/board_limit.

## `maxBoardJobs` (type: `integer`):

Protects memory. Missing jobs from incomplete boards stay unknown.

## `maxRequests` (type: `integer`):

Shared across all boards, including Lever pagination.

## `concurrency` (type: `integer`):

Requests are also paced to at most four starts per second in total.

## `timeoutSecs` (type: `integer`):

Network inactivity timeout per request.

## `maxDurationSecs` (type: `integer`):

Time for source checks, excluding report export. Unfinished boards return unknown.

## Actor input object example

```json
{
  "jobs": [
    {
      "url": "https://job-boards.greenhouse.io/canonical/jobs/6448444",
      "expectedTitle": "Associate Linux Support Engineer"
    }
  ],
  "maxBoards": 30,
  "maxBoardJobs": 5000,
  "maxRequests": 100,
  "concurrency": 3,
  "timeoutSecs": 20,
  "maxDurationSecs": 120
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `report` (type: `string`):

No description

## `csv` (type: `string`):

No description

## `summary` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "jobs": [
        {
            "url": "https://job-boards.greenhouse.io/canonical/jobs/6448444",
            "expectedTitle": "Associate Linux Support Engineer"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("kudomiya/ats-job-link-verifier").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "jobs": [{
            "url": "https://job-boards.greenhouse.io/canonical/jobs/6448444",
            "expectedTitle": "Associate Linux Support Engineer",
        }] }

# Run the Actor and wait for it to finish
run = client.actor("kudomiya/ats-job-link-verifier").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "jobs": [
    {
      "url": "https://job-boards.greenhouse.io/canonical/jobs/6448444",
      "expectedTitle": "Associate Linux Support Engineer"
    }
  ]
}' |
apify call kudomiya/ats-job-link-verifier --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kudomiya/ats-job-link-verifier"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/YDoOkjOhPAvGrDvCO/builds/SdkwLxj3W4JRx1zZJ/openapi.json
