All platforms

AppliTrack (Frontline Education) Jobs API.

Reach thousands of US K-12 school-district job boards through one interface — Jobo unwraps AppliTrack's JavaScript posting feed into clean jobs with titles, locations, and full descriptions.

Get API access
AppliTrack (Frontline Education)
Live
<3haverage discovery time
1hrefresh interval
Developer tools

Try the API.

Test Jobs and Feed endpoints against https://connect.jobo.world with live request/response examples, then copy ready-to-use curl commands.

What's in every response.

Data fields, real-world applications, and the companies already running on AppliTrack (Frontline Education).

Data fields
  • Job Title
  • Full Job Description
  • School / District Location
  • Position Type
  • Date Posted
  • Direct Apply URL
Use cases
  1. 01K-12 Job Aggregation
  2. 02Education-Sector Hiring Trends
  3. 03District Vacancy Monitoring
  4. 04Public-Sector Talent Mapping
DIY GUIDE

How to scrape AppliTrack (Frontline Education).

Step-by-step guide to extracting jobs from AppliTrack (Frontline Education)-powered career pages—endpoints, authentication, and working code.

HTMLintermediate500ms between requests, single connection (scraper default)No auth

Resolve the tenant and build the feed URL

Every AppliTrack district lives at /{tenant}/onlineapp on www.applitrack.com. The machine-readable feed of all open postings is jobpostings/Output.asp?all=1, so derive the tenant slug from any onlineapp URL first.

Step 1: Resolve the tenant and build the feed URL
import re
import requests
from urllib.parse import quote

HOST = "www.applitrack.com"

def tenant_from_url(url: str) -> str | None:
    # Matches /{tenant}/onlineapp in the path (e.g. /mcminnville/onlineapp).
    m = re.search(r"/([^/]+)/onlineapp(?:/|$)", url, re.IGNORECASE)
    return m.group(1) if m else None

def listings_url(tenant: str) -> str:
    return f"https://{HOST}/{tenant}/onlineapp/jobpostings/Output.asp?all=1"

tenant = tenant_from_url("https://www.applitrack.com/mcminnville/onlineapp/default.aspx?all=1")
print(listings_url(tenant))

Fetch Output.asp and unwrap the document.write() payload

The feed is served as text/javascript: a run of document.write('...') calls whose concatenated, JS-unescaped strings form the full postings HTML. Extract every payload and stitch it back together before parsing.

Step 2: Fetch Output.asp and unwrap the document.write() payload
DOC_WRITE = re.compile(r"document\.write\('((?:\\.|[^'\\])*)'\)", re.DOTALL)
UNESCAPE = {"n": "\n", "r": "\r", "t": "\t", "'": "'", '"': '"', "\\": "\\", "/": "/"}

def js_unescape(s: str) -> str:
    out, i = [], 0
    while i < len(s):
        if s[i] == "\\" and i + 1 < len(s):
            out.append(UNESCAPE.get(s[i + 1], s[i + 1]))
            i += 2
        else:
            out.append(s[i])
            i += 1
    return "".join(out)

resp = requests.get(listings_url(tenant), timeout=20)
resp.raise_for_status()
html = "".join(js_unescape(m.group(1)) for m in DOC_WRITE.finditer(resp.text))

Split the HTML into per-posting blocks

Each posting is wrapped in <ul class="postingsList" id="p{jobId}_"> where the numeric id is the AppliTrackJobId. Slice the concatenated HTML on that marker so the nested tables don't re-nest across postings.

Step 3: Split the HTML into per-posting blocks
POSTING = re.compile(
    r"<ul\s+class=['\"]postingsList['\"]\s+id=['\"]p(\d+)_['\"]\s*>",
    re.IGNORECASE,
)

matches = list(POSTING.finditer(html))
blocks = []
for i, m in enumerate(matches):
    job_id = m.group(1)  # numeric AppliTrackJobId
    if not job_id.isdigit():
        continue
    end = matches[i + 1].start() if i + 1 < len(matches) else len(html)
    blocks.append((job_id, html[m.start():end]))

print(f"{len(blocks)} open postings")

Parse each posting and build its URLs

Read the title from table.title td#wrapword, pull Position Type / Date Posted / Location from the labeled <li> blocks, and lift the applyFor('jobId','firstChoice','specialty') arguments off the Apply button to construct the canonical listing and apply URLs.

Step 4: Parse each posting and build its URLs
from bs4 import BeautifulSoup

APPLY_ARGS = re.compile(
    r"applyFor\(\s*'([^']*)'\s*,\s*'([^']*)'\s*,\s*'([^']*)'\s*\)"
)

def parse_block(tenant: str, job_id: str, chunk: str) -> dict:
    soup = BeautifulSoup(chunk, "html.parser")

    title_cell = soup.select_one("table.title td#wrapword")
    title = title_cell.get_text(" ", strip=True) if title_cell else None

    fields = {}
    for li in soup.select("li"):
        label = li.select_one("span.label")
        if not label:
            continue
        key = label.get_text(strip=True).rstrip(":").strip()
        value = "/".join(
            n.get_text(" ", strip=True)
            for n in li.select("span.normal")
            if n.get_text(strip=True)
        )
        fields[key] = value or None

    first_choice = specialty = ""
    apply_btn = soup.select_one("input.ApplyButton, input[onclick*='applyFor']")
    if apply_btn:
        args = APPLY_ARGS.search(apply_btn.get("onclick", ""))
        if args:
            first_choice, specialty = args.group(2), args.group(3)

    return {
        "external_id": job_id,
        "title": title,
        "position_type": fields.get("Position Type"),
        "date_posted": fields.get("Date Posted"),
        "location": fields.get("Location"),
        "description": soup.get_text(" ", strip=True),
        "listing_url": f"https://{HOST}/{tenant}/onlineapp/jobpostings/view.asp?AppliTrackJobId={job_id}",
        "apply_url": (
            f"https://{HOST}/{tenant}/onlineapp/_application.aspx"
            f"?posJobCodes={job_id}"
            f"&posFirstChoice={quote(first_choice)}"
            f"&posSpecialty={quote(specialty)}"
        ),
    }

for job_id, chunk in blocks:
    job = parse_block(tenant, job_id, chunk)
    print(job["external_id"], job["title"], job["location"])

Handle status codes and throttle politely

Map 404 to an unknown tenant, 401 to the auth-only path (the public feed should never return it), and 403/429 to throttling. Keep to one connection with a ~500ms delay — AppliTrack hosts thousands of small district tenants on shared infrastructure.

Step 5: Handle status codes and throttle politely
import time

def fetch_postings(tenant: str) -> str:
    resp = requests.get(listings_url(tenant), timeout=20)
    if resp.status_code == 404:
        raise LookupError(f"Tenant '{tenant}' not found")
    if resp.status_code == 401:
        # Only the private /onlineapp/api/jobs endpoint requires auth;
        # the public Output.asp feed should not return 401.
        raise PermissionError(f"Tenant '{tenant}' requires authentication")
    if resp.status_code in (403, 429):
        raise RuntimeError(f"Tenant '{tenant}' throttled or blocked (HTTP {resp.status_code})")
    resp.raise_for_status()
    time.sleep(0.5)  # single connection, ~500ms between requests
    return resp.text
Common issues
criticalOutput.asp returns JavaScript, not HTML or JSON

The feed ships as text/javascript document.write('...') calls — a plain HTML or JSON parse yields nothing. Extract each document.write payload, JS-unescape it, and concatenate the results before parsing markup.

highThe /onlineapp/api/jobs endpoint returns HTTP 401

That path is authenticated-only and was the sole JSON candidate observed in recon. Read postings from the public jobpostings/Output.asp?all=1 feed instead.

mediumNested tables re-nest when the whole feed is parsed at once

An HTML5 parser re-parents the per-posting <table>/<div> blocks. Slice the raw HTML on the <ul class="postingsList" id="p{jobId}_"> boundary and parse each posting chunk in isolation.

mediumBlocks with non-numeric or empty ids leak into results

Keep only blocks whose id (the digits in p{jobId}_) are all-numeric; that value is the AppliTrackJobId used for the view.asp and apply URLs.

lowPosition Type, Date Posted, or Location missing on some postings

These come from optional labeled <li> blocks and are not always present. Treat them as nullable and fall back to the tenant/title fields rather than skipping the posting.

Best practices
  1. 1Read jobs from jobpostings/Output.asp?all=1, never the private /onlineapp/api/jobs endpoint (returns 401).
  2. 2Unwrap the document.write('...') payloads and JS-unescape them before parsing any markup.
  3. 3Split the concatenated HTML on the <ul class="postingsList" id="p{jobId}_"> boundary so nested tables don't re-nest.
  4. 4Keep only postings whose id is all-numeric — that value is the AppliTrackJobId.
  5. 5Throttle to one connection with a ~500ms delay; AppliTrack serves thousands of small district tenants.
  6. 6Cache per tenant — district postings change slowly, usually daily at most.
Or skip the complexity

One endpoint. All AppliTrack (Frontline Education) jobs. No scraping, no sessions, no maintenance.

Get API access
cURL
curl "https://connect.jobo.world/api/jobs?sources=applitrack (frontline education)" \
  -H "X-Api-Key: YOUR_KEY"
Ready to integrate

Access AppliTrack (Frontline Education)
job data today.

One API call. Structured data. No scraping infrastructure to build or maintain — start with the free tier and scale as you grow.

99.9%API uptime
<200msAvg response
50M+Jobs processed