# How the Job Posting Liveness Checker Verifies If a Position Is Still Open

> Discover how the job posting liveness checker confirms open positions. santifer/career-ops uses a two-step liveness ladder with API checks and browser inspection for accurate verification.

- Repository: [Santiago Fernández de Valderrama/career-ops](https://github.com/santifer/career-ops)
- Tags: how-to-guide
- Published: 2026-07-03

---

**Career-Ops uses a two-step "liveness ladder" that first attempts a zero-token ATS API check for supported platforms, then falls back to a Playwright-based browser inspection if the API returns inconclusive results or the URL is not a known applicant-tracking system.**

The `santifer/career-ops` repository provides a robust **job posting liveness checker** that determines whether a position remains open without requiring API authentication. This open-source tool combines deterministic API queries for major applicant-tracking systems with intelligent browser automation to distinguish active listings from expired ones.

## The Two-Step Liveness Verification Architecture

The verification system implements a cascading strategy that prioritizes speed and accuracy before resorting to resource-intensive browser automation.

### Step 1: Zero-Token ATS API Verification

For postings hosted on known applicant-tracking systems (ATS) such as **Greenhouse**, **Lever**, or **Ashby**, the checker extracts a deterministic API endpoint directly from the URL.

In `liveness-api.mjs`, the function `checkLivenessViaApi` orchestrates this process:

- It calls `resolveAtsApi` to map the posting URL to a provider-specific API endpoint.
- Requests execute with a short **8-second timeout** (extended for slower providers).
- HTTP status codes drive the initial classification:
  - **404** or **410**: The posting is marked **expired**.
  - **200**: Considered **active** for per-job APIs (Greenhouse, Lever).
  - **429**, **5xx**, or network errors: Treated as *inconclusive*, triggering the browser fallback.

For organization-level APIs like Ashby, the response body undergoes additional parsing. The `classifyAshbyBoard` function confirms the exact job ID is still present in the board listing before returning an active status.

### Step 2: Playwright Browser Fallback

When the URL is not a recognized ATS link or the API check proves inconclusive, the system launches a headless Chromium instance via Playwright.

The core routine `checkUrlLivenessWithFallback` in `liveness-browser.mjs` executes this layer:

1. **Navigation and Validation**: The `checkUrlLiveness` function navigates to the URL after `rejectPrivateOrInvalid` guards against private or malformed hosts.
2. **Signal Collection**: After navigation, the system captures the HTTP status, final URL, page text, and visible "apply" controls (buttons, links, inputs).
3. **Classification**: These signals feed into `classifyLiveness` (defined in `liveness-core.mjs`), which returns **active**, **expired**, or **uncertain** based on the presence of application elements and page content.

If a headless run encounters an anti-bot challenge (e.g., Cloudflare), the system optionally invokes `createHeadedPageProvider` to launch a headed Chromium browser with a realistic User-Agent. The headed attempt supersedes the headless result, though the system never upgrades a blocked page to *expired*—only to *uncertain* if the challenge cannot be bypassed.

## Orchestration and CLI Entry Point

The script `check-liveness.mjs` wires these two layers together into a cohesive workflow:

- It parses command-line URLs or reads from a file.
- For each URL, it first invokes `checkLivenessViaApi`.
- If the API returns `null` (unknown ATS or ambiguous response), it ensures a browser is launched via `ensureBrowser` and executes `checkUrlLivenessWithFallback`.
- Results are logged with visual indicators (✅ active, ❌ expired, ⚠️ uncertain) and a summary is printed upon completion.

Optional flags support `--throttle` for rate-limiting and headed-browser fallback for circumventing bot detection.

## Usage Examples

### Command-Line Verification

Check a single Greenhouse posting:

```bash
node check-liveness.mjs https://boards.greenhouse.io/example/jobs/12345

```

Batch-process URLs from a file with a 5-second throttle between browser checks:

```bash
node check-liveness.mjs --throttle=5000 --file urls.txt

```

### Programmatic Integration

Import the verification functions directly into your own scripts:

```javascript
import { checkLivenessViaApi } from './liveness-api.mjs';
import { checkUrlLivenessWithFallback, createHeadedPageProvider } from './liveness-browser.mjs';
import { chromium } from 'playwright';

async function verify(url) {
  // Attempt the cheap ATS API check first
  const apiResult = await checkLivenessViaApi(url);
  if (apiResult) return apiResult; // Returns "active" or "expired"

  // Fallback to Playwright browser automation
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();
  const headed = createHeadedPageProvider(chromium);
  
  const result = await checkUrlLivenessWithFallback(page, url, { 
    getHeadedPage: headed.get 
  });
  
  await browser.close();
  return result;
}

```

## Summary

- **Two-layer architecture**: The checker prioritizes zero-token ATS API calls before falling back to browser automation.
- **File locations**: API logic resides in `liveness-api.mjs`, browser automation in `liveness-browser.mjs`, classification rules in `liveness-core.mjs`, and orchestration in `check-liveness.mjs`.
- **ATS support**: Native API verification for Greenhouse, Lever, and Ashby without authentication tokens.
- **Smart fallbacks**: HTTP 404/410 responses mark instant expiration, while anti-bot challenges trigger headed-browser retries.
- **Flexible execution**: Use as a CLI tool with throttling and batch processing, or import functions for programmatic use.

## Frequently Asked Questions

### Which applicant-tracking systems does the liveness checker support via API?

The system supports **Greenhouse**, **Lever**, and **Ashby** through direct API endpoint resolution. For Greenhouse and Lever, the checker queries individual job endpoints and interprets HTTP status codes. For Ashby, it queries the organization-level board API and parses the response body to confirm the specific job ID remains listed.

### How does the checker handle anti-bot protections like Cloudflare?

When the headless Chromium instance encounters a bot challenge, the `createHeadedPageProvider` function in `liveness-browser.mjs` launches a headed browser with a realistic User-Agent to retry the navigation. If the headed browser succeeds, that result supersedes the headless failure. However, if both attempts fail, the posting is marked **uncertain** rather than expired to avoid false positives.

### What timeout values does the API verification layer use?

The `checkLivenessViaApi` function applies a default **8-second timeout** for API requests, with extended durations configured for slower providers. This ensures rapid feedback for the majority of requests while accommodating occasional latency from specific ATS platforms.

### Can I use the liveness checker as a library in my own Node.js application?

Yes. While `check-liveness.mjs` provides a CLI interface, you can import the core functions directly. Import `checkLivenessViaApi` from `liveness-api.mjs` and `checkUrlLivenessWithFallback` from `liveness-browser.mjs` to integrate the verification logic into custom workflows, supply your own Playwright browser instances, and handle results programmatically.