DEV Community

Cover image for Stop Applying to Ghost Jobs: Read Openings Straight From the Source
H. Abds
H. Abds

Posted on AI-assisted

Stop Applying to Ghost Jobs: Read Openings Straight From the Source

Imagine it's 11:47 PM.

You're on application number 47 tonight. Your tea went cold an hour ago. On your screen: Senior Software Engineer. Remote. Great culture. It's perfect. It's suspiciously perfect.

You polish your CV for this one. You write a cover letter with actual feelings in it. You click Apply.

Somewhere far away, a recruiter who left that company in March does not receive it. Nobody does. The job was filled months ago. The listing just… never left. It drifts from job board to job board, reposted and re-shared, like a little ghost waving a "We're hiring!" sign.

Congratulations. You've met a ghost job.

They come in a few flavours. Some are stale reposts. Some are real jobs that got filled weeks ago and nobody pressed delete. Some are "always hiring" posts that exist mostly to collect CVs. And at 11:47 PM, they all look exactly the same.

So let me tell you the part nobody at the job board will: most tech companies don't run their own careers page. They rent one from an applicant tracking system (ATS) like Greenhouse, Lever, Ashby or Workday. When a job closes, it closes there first. Everything else (aggregators, reposts, "Top Jobs For You" emails) is a copy of a copy.

The good news: these platforms hand out the job list as clean JSON to anyone who asks. And this isn't some hacky trick. Greenhouse, Lever and Ashby all document these as official public APIs, built exactly so job boards can show their listings. No login. No API key. No captcha asking you to find all the traffic lights.

So let's go knock on the door and ask nicely.

Step 1: Find out which ATS a company uses

Open the company's careers page and click any job. Look at the URL:

If the job URL looks like… The company uses… Its "slug" is…
job-boards.greenhouse.io/airbnb/... Greenhouse airbnb
careers.acme.com/jobs?gh_jid=123 Greenhouse in disguise usually the company name
jobs.lever.co/sysdig/... Lever sysdig
jobs.ashbyhq.com/ramp/... Ashby ramp
nvidia.wd5.myworkdayjobs.com/... Workday it's complicated (see below)

Older Greenhouse links use boards.greenhouse.io instead. Same thing.

That ?gh_jid= one is a good trick: lots of companies dress Greenhouse up in their own branding, but the job ID in the URL gives it away.

The slug is just the company's name on that platform. Write it down. That's your key to the door.

Step 2: Ask for the jobs

Here's the whole thing. One function per platform, plus a little helper that tells you how old each posting is. About 70 lines, and a good chunk of that is brackets:

import requests
from datetime import datetime, timezone

def get_json(url):
    r = requests.get(url, timeout=180)
    if r.status_code == 404:
        return None
    r.raise_for_status()
    return r.json()

def greenhouse(slug):
    url = f"https://boards-api.greenhouse.io/v1/boards/{slug}/jobs"
    data = get_json(url)
    if data is None:
        return None
    return [
        (j["title"], j["location"]["name"],
         j["first_published"], j["absolute_url"])
        for j in data["jobs"]
    ]

def lever(slug):
    for host in ("api.lever.co", "api.eu.lever.co"):
        data = get_json(f"https://{host}/v0/postings/{slug}?mode=json")
        if data is not None:
            return [
                (j["text"], j["categories"].get("location", "-"),
                 j["createdAt"], j["hostedUrl"])
                for j in data
            ]
    return None

def ashby(slug):
    url = f"https://api.ashbyhq.com/posting-api/job-board/{slug}"
    data = get_json(url)
    if data is None:
        return None
    return [
        (j["title"], j["location"], j["publishedAt"], j["jobUrl"])
        for j in data["jobs"]
    ]

def days_old(posted):
    if isinstance(posted, int):  # Lever counts in milliseconds
        posted = datetime.fromtimestamp(posted / 1000, timezone.utc)
    else:
        posted = datetime.fromisoformat(posted)
    return (datetime.now(timezone.utc) - posted).days

companies = [
    (greenhouse, "airbnb"),
    (lever, "sysdig"),
    (ashby, "ramp"),
]
keyword = "engineer"

for fetch, slug in companies:
    jobs = fetch(slug)
    if jobs is None:
        print(f"{slug}: not found (typo, or moved to another ATS)")
        continue
    if not jobs:
        print(f"{slug}: board exists, zero openings")
        continue
    for title, location, posted, link in jobs:
        if keyword not in title.lower():
            continue
        age = days_old(posted)
        ghost = "  <- still 'open'? hmm" if age > 180 else ""
        print(f"{slug} | {age}d | {title} | {location}{ghost}")
        print(f"    {link}")
Enter fullscreen mode Exit fullscreen mode

Run it (Python 3.11 or newer):

pip install requests
python ghost_jobs.py
Enter fullscreen mode Exit fullscreen mode

Hit enter. Sip your cold tea. About half a minute later, you get every open engineering role at those three companies, straight from the source, with its age in days. If a job is in this list, it's open right now. If it's not, it doesn't matter how many times LinkedIn emails you about it.

Want the full job description too? Add ?content=true to the Greenhouse URL. Ashby already includes it, and so does Lever (in the description field).

Step 3: The fine print (a.k.a. things I learned the hard way)

"Open" doesn't always mean "sincere". The ATS tells you what's open, not what's real. That "always hiring" post will happily show up here too. That's why the script prints the age. (Remember that suspiciously perfect job from 11:47 PM? In our little story, it was posted 287 days ago.)

When I ran it in November 2026, 26 of the 107 engineering roles had been "open" for more than six months. Some of those are genuinely hard-to-fill senior roles. Some are… vibes. A job that has been open since last winter deserves a raised eyebrow before you spend an evening on a cover letter.

Lever is in no hurry. In my tests, Lever sent data at roughly dial-up speed. Sysdig's 38 jobs took 20 to 35 seconds. Palantir's board took almost nine minutes. That's why the code uses timeout=180. If the script looks frozen, it's not. Go make tea. Maybe bake something.

Lever has a European branch. Some companies live on api.eu.lever.co instead. The script already tries both, so a 404 on the first one isn't the end of the story.

A 404 doesn't mean the company is dead. Type airbnbb and all three platforms answer with a polite 404. Same answer if the company moved to a different ATS last year. And an empty list is a real company with zero openings (or a hiring freeze). The script tells these apart, so check which story you're in before you cross a company off.

Salaries are hiding in plain sight. Many US postings include a pay range (thanks, pay-transparency laws), but it's often buried inside the description text, not in a neat field. A small regex will get most of them.

Workday is the final boss. A lot of big employers (NVIDIA, Salesforce, banks, hospitals) use Workday. It also has a JSON endpoint, but you have to POST to it, it gives you 20 jobs at a time, and every company's URL has its own shape:

tenant, site = "nvidia", "NVIDIAExternalCareerSite"
url = (f"https://{tenant}.wd5.myworkdayjobs.com"
       f"/wday/cxs/{tenant}/{site}/jobs")
body = {"appliedFacets": {}, "limit": 20, "offset": 0,
        "searchText": "engineer"}
page = requests.post(url, json=body, timeout=60).json()
print(page["total"], [j["title"] for j in page["jobPostings"]])
Enter fullscreen mode Exit fullscreen mode

That wd5 and NVIDIAExternalCareerSite? You have to dig those out of each company's careers page by hand. Every. Single. One.

The actual hard part

It's now past midnight, and you're feeling powerful. You're probably thinking: "Cool, I'll just add 50 companies to the list."

And that's where it stops being a 70-line script.

The code was never the hard part. The list is. To search "remote data engineer" across the whole market, you first need to know which companies exist on each platform and what their slugs are. Nobody publishes that list. Greenhouse doesn't have a "show me every company" button. Neither do Lever, Ashby or Workday.

So you end up:

  • collecting thousands of slugs from wherever you can find them,
  • checking which ones are still alive (companies switch ATS, get acquired, or quietly die),
  • figuring out each Workday company's secret URL,
  • handling the slow ones, the EU ones and the ones that time out at 2am,
  • and running it all every day so you know when a job closes, not just when it opens.

It's doable. It's also a hobby now. Possibly a lifestyle. Your tea is officially a historical artifact.

Or skip the hard part

Full disclosure: this is exactly the rabbit hole I went down, and I came out the other side with a tool.

Career Site Jobs Scraper on Apify does all of the above with a directory of 12,000+ company boards across Greenhouse, Lever, Ashby and Workday, already mapped. You type "data engineer", "remote", "senior", and it reads every board live and gives you back clean jobs with:

  • title, company, location, remote / hybrid / on-site, seniority, post date
  • salary range (from the ATS field, or parsed out of the description)
  • the full description in Markdown
  • an "only new jobs" mode for daily alerts, plus a free notice when a job you saw earlier gets closed

You can also paste any company's careers link (including Workable, Recruitee, SmartRecruiters and Personio) and it figures out the platform for you.

You pay per job it finds (plus a tiny fee for the boards it checks), and Apify's free plan comes with monthly credit, so you can try a few searches without paying anything.

But honestly? If you only care about five companies, the script above is all you need. Copy it, change the slugs, run it every morning with your coffee.

Back to 11:47 PM

Same desk. Same cold tea. Same suspiciously perfect job on your screen.

But this time, before you write a single word of that cover letter, you check the source. Is it actually there? Was it posted last week? Then go for it, and good luck.

Not there? Or 287 days old? Close the tab. Wave goodbye to the ghost. Go to bed.

You've got better things to apply to.


If this saved you from one more "we've decided to move forward with other candidates" email, it was worth writing.

Top comments (0)