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 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:

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 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.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →