EdJoin.org Scraper - Education Jobs, Salaries & Districts avatar

EdJoin.org Scraper - Education Jobs, Salaries & Districts

Under maintenance

Pricing

from $1.00 / 1,000 results

Go to Apify Store
EdJoin.org Scraper - Education Jobs, Salaries & Districts

EdJoin.org Scraper - Education Jobs, Salaries & Districts

Under maintenance

Scrape EdJoin.org K-12 and higher-ed jobs by keyword, location, filters or URL. Extract districts, locations, salaries, openings, deadlines, requirements and full descriptions, with optional contact and address details.

Pricing

from $1.00 / 1,000 results

Rating

0.0

(0)

Developer

Abot API

Abot API

Maintained by Community

Actor stats

1

Bookmarked

2

Total users

1

Monthly active users

2 hours ago

Last modified

Share

EdJoin Scraper: Education Job Listings, Salary Ranges & Detail Enrichment

EdJoin Scraper turns EdJoin, the K-12 and higher-education hiring board used by school districts and county offices, into structured job data. Search by keyword, location and filters, or paste EdJoin search and job-posting links. Every job comes back as one flat record with district, location, salary range, category, openings and deadlines, plus optional contact details, full address, requirements and full descriptions. Export to JSON, CSV or Excel, or pull the results straight into your app through the API.

Why This Scraper?

  • Two ways in. Search by keyword, location and filters, or paste EdJoin search and job-posting links directly. No need to build query strings by hand.
  • One flat record per job. Every listing merges the search-result card and, optionally, its detail page into a single record: about 40 fields on a listing-only run, more once detail enrichment is on.
  • Full detail enrichment on demand. Contact name and email, complete street address, education and experience requirements, skills, the full job summary and description, employer logo and the offsite apply link, one extra request per job.
  • Filters that narrow the search at the source. Job type, category, online-application-only, posted-within-days and sort order are all applied server-side, not filtered out after the fact.
  • Walks a whole search, or an entire catalogue, forward automatically. Set a hard job or page cap, or let it run until results run out.
  • Built for monitoring. Incremental mode tracks a recurring search over time and classifies every job NEW, UPDATED, UNCHANGED, REAPPEARED or EXPIRED, so a scheduled run bills only for what changed.
  • Fails loudly, not quietly. If EdJoin refuses every request in a run, the run fails with a clear message instead of returning an empty dataset that looks like "no jobs."

Use Cases

  • School HR and staffing teams: track new certificated and classified openings across districts to prioritize sourcing and outreach.
  • Recruiting and HR-tech platforms: mirror EdJoin postings into your own job board or ATS without sending users to EdJoin directly.
  • Compensation and market-rate teams: collect posted salary ranges and requirements to benchmark against your own pay bands.
  • Education researchers and journalists: build a dataset of job postings, salary ranges and hiring activity across school districts over time.
  • Districts and agencies monitoring their own postings: schedule a recurring pull with Incremental mode to know the moment a listing changes or is taken down.

Data You Get

Sample shape: values are illustrative placeholders, not from a live posting.

FieldExample
id20000001
url"https://www.edjoin.org/Home/JobPosting/20000001"
title"Elementary Teacher, 1.0 FTE, 2026-2027"
districtName"Example Unified School District"
city"Exampleton"
countyName / countyFullName"Example" / "Example County, CA"
stateName"California"
zip"90000"
location"Exampleton, California"
jobType / jobTypeId"Teacher - K-6" / 48
categoryName / categoryId"Certificated" / 1
salaryDisplay"$60,000 - $117,000 Annually"
payRangeFrom / payRangeTo"$60,000" / "$117,000"
employmentType"Full Time"
numberOpenings2
onlineApptrue
datePosted"2026-01-01T00:00:00.000Z"
applicationDeadline"2026-02-01T00:00:00.000Z"
contactName / contactEmail"Jane Example" / "jobs@example.org"
streetAddress"100 Example Blvd."
educationRequirements"Valid California Teaching Credential"
description"Full job description text appears here when detail enrichment is on."
employerLogolink to the district's logo image
offsiteUrllink to apply, when the district uses its own application system
scrapedAt"2026-01-01T00:00:00.000Z"

Listing-only runs (Fetch full job details off) still return about 40 fields. The detail-only fields (contactName, contactEmail, streetAddress, addressLocality, addressRegion, postalCode, addressCountry, educationRequirements, experienceRequirements, skills, jobSummary, description, workYearLength, employerName, employerUrl, employerLogo, offsiteUrl, attachmentName) are null until Fetch full job details is on. In Incremental mode, records also carry changeType, changedFields, firstSeenAt and lastSeenAt; an EXPIRED row repeats the fields from that job's last successful scrape.

How to Use

  1. Pick a mode: search (keyword + filters) or url (paste EdJoin search or job-posting links).
  2. Fill in the fields for that mode, and turn on Fetch full job details if you want contact info, full address, requirements and the full description.
  3. Set Max jobs (and Max pages per search if you want an explicit ceiling) to control run size and cost, then click Start.
  4. Download the dataset as JSON, CSV or Excel, or read it through the API.

Basic keyword search:

{
"mode": "search",
"keywords": "teacher",
"maxListings": 20,
"fetchDetails": true
}

Filtered search (special-education teaching jobs that take an online application, posted in the last week):

{
"mode": "search",
"keywords": "special education",
"jobTypeIds": ["9"],
"onlineApplicationOnly": true,
"postedWithinDays": 7,
"maxListings": 100
}

Browse a whole category, walking the full catalogue:

{
"mode": "search",
"keywords": "",
"categoryId": 2,
"maxListings": 0,
"maxPages": 0,
"fetchDetails": false
}

Paste URLs (a search page and a single job posting):

{
"mode": "url",
"urls": [
"https://www.edjoin.org/Home/Jobs?keywords=principal&location=&searchType=all",
"https://www.edjoin.org/Home/JobPosting/20000002"
],
"fetchDetails": true
}

Run it from your code

Python:

from apify_client import ApifyClient
client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/edjoin-scraper").call(run_input={"mode": "search", "keywords": "teacher", "maxListings": 20})
for job in client.dataset(run["defaultDatasetId"]).iterate_items():
print(job["title"], job["districtName"], job["salaryDisplay"])

JavaScript:

import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/edjoin-scraper').call({ mode: 'search', keywords: 'teacher', maxListings: 20 });
const { items } = await client.dataset(run.defaultDatasetId).listItems();

Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the Integrations tab.

Caps: Max jobs and Max pages

Max jobs (maxListings) is the overall stop: the run ends as soon as this many jobs have been collected across every search or URL in the input, and it is split evenly across multiple searches so each one gets representation. Max pages per search (maxPages), at its default of 0, walks every page of a search until a page comes back empty or holds no job the run has not already seen, or until Max jobs is reached, whichever happens first. Set Max pages to a specific number only if you want an explicit ceiling below that natural stop.

Known limitation when pasted search URLs overlap. The "no new job on this page" check compares each page against every job the whole run has already seen, not just the current search's own jobs. So if a later search URL hits a page made up entirely of jobs an earlier search URL already returned, that later search stops right there and its deeper pages are never requested, even with Max pages at 0. This can happen on its very first page, or midway after it has already returned new jobs. A page with even one job the run has not seen yet does not trigger it. Example, with Max jobs at 0: URL A (keywords=teacher) returns jobs 1 to 3 on its only page. URL B (keywords=elementary teacher) would serve jobs 1 to 3 again on page 1 and new jobs 10 and 11 on page 2. Because page 1 of URL B holds only jobs URL A already returned, URL B stops on page 1 and jobs 10 and 11 are never collected. The same happens if URL B's page 1 is new but its page 2 repeats URL A's jobs: its page 3 is never reached. Raising Max pages does not help. Workaround: when your search URLs are likely to share results (similar keywords, a broad search plus a narrower one inside it, nearby locations), run each one as its own run, for example one saved task per URL.

Known limitation with Max jobs across several searches. When a search's share of Max jobs is smaller than the page it just read (a page holds up to 200 jobs), the jobs past that share are skipped but still counted as seen for the rest of the run. Two consequences: a job-posting URL pasted in the same run for one of those skipped jobs is silently dropped (no row, no detail request), and a later search whose page consists only of those skipped jobs stops early as described above. Example: Max jobs 6 with two search URLs gives each a share of 3. URL A's page lists jobs 1 to 10, so jobs 1 to 3 are returned and 4 to 10 are skipped; a pasted /Home/JobPosting/7 in the same run then returns nothing. Workaround: put job-posting URLs in their own run, or set Max jobs to 0 (or high enough to cover every search) when mixing searches and posting URLs.

Resume and recurring updates

  • Resume from a prior run (resumeFromRunId): paste a prior run or dataset ID and the actor loads every job ID already saved there before scraping starts, so those jobs are never pushed or billed again. The run also keeps its own checkpoint of the job IDs it has saved, so a platform migration or a Resurrect of a failed run of the same input does not push those jobs twice.
  • Incremental mode (incrementalMode) is for scheduled runs over the same search or URL set. Each job is classified NEW, UPDATED (with changedFields), UNCHANGED (suppressed and not billed unless emitUnchanged is on), REAPPEARED or EXPIRED (only after a run that reached the natural end of every tracked search, did not stop at Max jobs and was not a resume, and only with emitExpired on). stateKey names or shares the stored baseline. With Incremental mode off, output is exactly as before.

Known limitation: a resumed walk restarts from page 1 and can stop there. Resume (and a migrated or Resurrected run reloading its checkpoint) does not remember which page the earlier run reached; it walks the search again from its first page and relies on the "no new job on this page" stop described under Caps. On a newest-first search that has not changed much since the interrupted run, page 1 consists only of jobs that run already saved, so the walk stops on page 1. Example: a walk-all pull of a 4-page search is interrupted after page 2. A resumed run makes exactly one page request, pushes zero jobs (the dataset holds only a single "No jobs found" note row), and pages 3 and 4 are never collected, although no run ever read them. The same happens if page 1 has a few new postings but page 2 is fully saved already: the walk stops on page 2. Workaround: resume through URL mode instead. Build the search as an EdJoin search URL with the same filters, add &page=N, where N is the last Page N number in the interrupted run's log (the actor's pages hold up to 200 jobs each), and paste the prior run's ID into Resume from a prior run as usual. The walk then starts at page N, already-saved jobs are still skipped, and it continues to the end. If that run stops at once with "Page N: 0 new jobs", use &page=N+1. Jobs posted at the top of the search since the interrupted run are not picked up by this; a normal or Incremental run catches them.

Expired-job reporting caveats. With several search URLs in one run: if one of them stops early because of the overlap limitation above, the run does not count as a complete scan, so no EXPIRED rows are reported that run (nothing is wrongly expired, but real removals are not reported either). And when a search returns fewer jobs than its page holds because its share of Max jobs ran out while the run as a whole stayed under Max jobs, the jobs skipped on that page are not checked, so previously tracked jobs among them can be reported EXPIRED although they are still live. Example: Max jobs 6, two search URLs, URL A's single page grows from 5 to 8 jobs as 3 new postings appear at the top; its share of 3 returns only the 3 new ones, URL B returns 1, and URL A's previously tracked jobs 1 to 3 come back as EXPIRED. For reliable EXPIRED rows, use one search per Incremental run (or non-overlapping search URLs) with Max jobs at 0.

Send results into your apps (MCP connectors)

Optionally pipe the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step after the scrape - the Apify dataset is never changed.

What gets written to the connector: a condensed, human-readable summary of each record - not the full JSON. Each item becomes one entry with a title and its key fields flattened to plain text. The complete record always stays in the Apify dataset.

  1. Authorize a connector once under Apify -> Settings -> Integrations (Notion, Linear, Airtable, or Apify).
  2. Select it in the "Pipe results into your apps" input field. (If the picker is empty, you haven't authorized a connector yet.)
  3. For Notion, also set notionParentPageUrl to the page where items should be created.

The connection is mediated by Apify's MCP proxy, so this actor never sees your third-party credentials. Leave the field empty to skip.

Input Parameters

ParameterTypeDefaultDescription
modestringsearchsearch = keyword + filters; url = paste EdJoin search or job-posting links.
keywordsstring(empty; prefilled teacher)Job title, keyword or organization to search for (search mode). Leave empty to list everything.
locationstring(empty)Free-text location to narrow the search: a city, county or ZIP. Leave empty for statewide / all.
jobTypeIdsarray(empty)Restrict to specific EdJoin job types by numeric ID (see jobTypeId in the output). Multiple IDs are OR-combined.
categoryIdinteger(empty)One EdJoin category: 1 Certificated, 2 Classified, 3 Management, 4 Other. Leave empty for all.
stateIdinteger(empty)Retired: the live source no longer honors a state filter, so any value is accepted for saved-task compatibility and then ignored.
onlineApplicationOnlybooleanfalseOnly return jobs that accept an EdJoin online application.
postedWithinDaysinteger0Only jobs posted within the last N days. 0 = no date limit.
urlsarray(prefilled sample search URL)EdJoin search or job-posting URLs to paste (url mode). A page= value in a search URL makes the walk start at that page (pages hold up to 200 jobs).
sortBystringnewestnewest or oldest first. Applies to both modes.
fetchDetailsbooleantrueOn: each job also gets contact name and email, full street address, education and experience requirements, skills, job summary, full description, employer logo, offsite apply link and attachment (one extra request per job). Off: listing-card fields only.
maxListingsinteger20Overall cap on jobs across every search or URL in the run. 0 = unlimited.
maxPagesinteger0Cap on pages walked per search. 0 = walk until a page comes back empty or holds no job the run has not already seen (including jobs from another search in the same run, or from a resumed run), or Max jobs is reached, whichever comes first.
proxyobjectApify datacenterConnection settings. Datacenter is the default and is enough for EdJoin; residential is optional.
maxResidentialRequestsinteger0Cap on requests that may use residential proxy in one run. 0 = unlimited. Only relevant if residential proxy is enabled above.
mcpConnectorsarray(none)Optional: send a summary of each job to apps you authorized under Integrations.
notionParentPageUrlstring(none)Notion connector only: page under which job pages are created.
maxNotifyListingsinteger50Cap on items written to each connector per run. Does not affect the dataset.
resumeFromRunIdstring(empty)Paste a previous run ID or dataset ID to continue a large walk-all pull: jobs already saved by that run are loaded before scraping starts and skipped, so this run only appends new jobs. The walk restarts from page 1 and can stop there; see the resume limitation under How to Use for the page= workaround.
incrementalModebooleanfalseOn: remembers this search/URL set's results between scheduled runs and classifies each job as NEW / UPDATED / UNCHANGED / REAPPEARED / EXPIRED. Off: every run is a plain fresh pull.
stateKeystring(empty)Optional label for the incremental-mode baseline. Leave empty to derive it automatically from your search/URL settings.
emitUnchangedbooleanfalseOn: also return (and bill) jobs unchanged since the last run. Only applies when Incremental mode is on.
emitExpiredbooleanfalseOn: also return (and bill) a row for each previously tracked job no longer found. Only emitted after a run that reached the natural end of every search, did not stop at Max jobs and was not a resume. See the expired-job caveats under How to Use for multi-search runs. Only applies when Incremental mode is on.

Output Example

Sample shape: values are illustrative placeholders, not from a live posting.

{
"id": 20000001,
"url": "https://www.edjoin.org/Home/JobPosting/20000001",
"title": "Elementary Teacher, 1.0 FTE, 2026-2027",
"districtName": "Example Unified School District",
"city": "Exampleton",
"countyName": "Example",
"countyFullName": "Example County, CA",
"stateName": "California",
"stateId": 24,
"zip": "90000",
"location": "Exampleton, California",
"jobType": "Teacher - K-6",
"jobTypeId": 48,
"categoryName": "Certificated",
"categoryId": 1,
"salaryInfo": "See attached certificated salary schedule",
"salaryDisplay": "$60,000 - $117,000 Annually",
"payRangeFrom": "$60,000",
"payRangeTo": "$117,000",
"payRangeUnit": "Annually",
"employmentType": "Full Time",
"numberOpenings": 2,
"onlineApp": true,
"datePosted": "2026-01-01T00:00:00.000Z",
"applicationDeadline": "2026-02-01T00:00:00.000Z",
"workYearLength": "2026-2027 School Year",
"contactName": "Jane Example",
"contactEmail": "jobs@example.org",
"streetAddress": "100 Example Blvd.",
"addressLocality": "Exampleton",
"addressRegion": "California",
"postalCode": "90000-0000",
"addressCountry": "US",
"employerName": "Example Unified School District",
"employerUrl": "https://www.edjoin.org/example",
"employerLogo": "https://example.org/logos/example-000000.png",
"educationRequirements": "Valid California Teaching Credential",
"experienceRequirements": "Online application required; incomplete applications are screened out.",
"skills": "BA/BS plus subject-matter competence",
"jobSummary": "Full job summary text appears here when detail enrichment is on.",
"description": "See attachment on original job posting",
"offsiteUrl": "https://www.example.org/apply",
"attachmentName": "Job-Description-00000000.pdf",
"searchQuery": "teacher",
"scrapedAt": "2026-01-01T00:00:00.000Z"
}

Plan Requirement

This actor runs on any Apify plan and is priced pay per event, not a monthly rental: a small charge when a run starts, a charge per job pushed to the dataset, and a smaller charge for each pushed job whose detail page was actually fetched and read (only when Fetch full job details is on; listing-only runs and suppressed unchanged jobs never see that charge). The Pricing tab has the current numbers.

The default connection is Apify datacenter proxy, which is enough for EdJoin and is the cheapest tier available. Residential proxy is optional: turn it on under Connection if you ever see rejections, and use the residential request budget to cap how many requests may use it in one run. A backup connection is used automatically if Apify proxy itself is unavailable.

FAQ

How much does it cost?

You pay per event: once when a run starts, once for each job pushed to the dataset, and a small extra amount for each job whose detail page is fetched. The Pricing tab shows the current rates. Use Max jobs to cap the cost of any run.

This actor collects only publicly available job listing data. You are responsible for how you use it: follow EdJoin's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution.

Can I get only new or changed jobs on a schedule?

Yes. Schedule the actor from the Schedules tab and turn on Incremental mode. Each run then returns only new, updated, reappeared or expired jobs for the same search, and unchanged ones are not billed unless you turn on Emit unchanged.

What's the difference between Resume and Incremental mode?

Resume from a prior run continues ONE interrupted walk-all pull using a pasted run or dataset ID: a one-time continuation that skips every job that run already saved (pair it with a search URL carrying &page=N, as described above, so the walk does not stop on already-saved first pages). Incremental mode remembers a search's baseline itself between scheduled runs, so a recurring run only returns what changed, with no ID to paste. They solve different problems and can be combined to seed Incremental mode's baseline from a prior resume pull.

Why did a resumed run, or my second search URL, return far fewer jobs than expected?

A search stops as soon as it reads a page holding only jobs the run has already seen, and that includes jobs from another search URL in the same run and jobs loaded from a resumed run. So a resumed walk-all pull can stop on page 1 with zero jobs, and a second search URL that overlaps the first can stop before its own new jobs. Run overlapping search URLs as separate runs, and resume a large pull through a search URL with &page=N as described under Resume and recurring updates above. A job-posting URL pasted next to searches can also be dropped if one of those searches skipped that job because of its share of Max jobs; paste posting URLs in their own run.

Why did my run fail instead of returning an empty dataset?

If EdJoin refuses every request a run makes, the run stops with a clear message so "no jobs match this search" is never confused with "nothing could be read." Run it again in a few minutes. A search that genuinely comes back empty still succeeds and returns a note instead of a job.

Can I use it with AI agents or MCP?

Yes. Call it from any Apify integration or MCP client, and use the connector field to push results into Notion, Linear or Airtable.

πŸ”— Want more jobs data?

Pair this actor with these related scrapers from the same team:

🧩 Naukri.com Scraper
Scrape Naukri.com jobs by keyword or URL. Extract job title, company, skills, experience...
πŸ’Ό IrishJobs.ie Scraper
Scrape job listings from IrishJobs.ie by keyword, location, job type, salary, recency, or...
πŸ’Ό CareerBuilder Scraper
Scrape CareerBuilder.com job listings by search or URL. Returns title, company profile...
πŸ’Ό Jobs.cz
Scrape Czech job listings from Jobs.cz. Search builder (keyword, location, field, salary...
πŸ’Ό HH.ru Jobs Scraper
From $1/1K. Scrape HH.ru job listings with 50+ structured fields. Search by filters or...
πŸ’Ό Hiring.Cafe Jobs
From $1/1K. Unlock powerful Hiring.Cafe job data extraction. Use advanced filters or URLs...

πŸ‘‰ Browse all abotapi scrapers

πŸ’¬ Support

  • 🐞 Found a bug or a missing field? Open a ticket on the Issues tab. We usually reply within hours.
  • πŸ› οΈ Need another site, extra fields or a private build? Email contact@abotapi.com.
  • ⭐ Enjoying it? A quick review on the actor page helps other users find it.