How the Job Posting Liveness Checker Verifies If a Position Is Still Open
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
resolveAtsApito 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:
- Navigation and Validation: The
checkUrlLivenessfunction navigates to the URL afterrejectPrivateOrInvalidguards against private or malformed hosts. - Signal Collection: After navigation, the system captures the HTTP status, final URL, page text, and visible "apply" controls (buttons, links, inputs).
- Classification: These signals feed into
classifyLiveness(defined inliveness-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 viaensureBrowserand executescheckUrlLivenessWithFallback. - 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:
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:
node check-liveness.mjs --throttle=5000 --file urls.txt
Programmatic Integration
Import the verification functions directly into your own scripts:
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 inliveness-browser.mjs, classification rules inliveness-core.mjs, and orchestration incheck-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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →