# AppliTrack Jobs API

> Query jobs from AppliTrack boards through one API with ?source=applitrack — plus how AppliTrack's Output.asp endpoint really behaves.

Source: https://hyperjobs.io/ats/applitrack

AppliTrack is Frontline Education's applicant tracking product, and it is what US
K-12 school districts hire through: teachers, aides, bus drivers, custodians,
coaches, cafeteria staff. Each district's board lives under
`applitrack.com/<district>/onlineapp`, where `<district>` is a short path segment
like `wvde` or `jcps`. It's 41,110 postings in this dataset — a number that moves
hourly, so read it from [`/v1/meta`](/docs/api-reference/meta) rather than
trusting this sentence.

It is also, by some distance, the cheapest source in this pool to fetch. One
unauthenticated GET returns an entire district's board **with the descriptions
already in it**. No pagination, no detail pass, no token.

## Try it

```bash
curl -G https://api.hyperjobs.io/v1/jobs \
  -H "Authorization: Bearer $HYPERJOBS_KEY" \
  --data-urlencode "source=applitrack" \
  --data-urlencode "time_frame=7d" \
  --data-urlencode "limit=2"
```

```json title="Illustrative — the shape, not a live response"
{
  "data": [
    {
      "id": "applitrack:wvde_48210",
      "title": "Special Education Teacher",
      "company": {
        "slug": "wvde",
        "name": "West Virginia Department of Education",
        "domain": "wvde.us",
        "website": "https://wvde.us",
        "logo": "https://logo.hyperjobs.io/wvde.us",
        "industry": "Education Administration Programs",
        "employee_count": null
      },
      "location": {
        "remote": false,
        "countries": ["United States"],
        "cities": null,
        "regions": null,
        "raw": ["United States"]
      },
      "employment_type": ["FULL_TIME"],
      "work_arrangement": null,
      "department": "Elementary School Teaching",
      "team": "Nimitz High School",
      "seniority": null,
      "salary": {
        "min": null,
        "max": null,
        "currency": null,
        "unit": null,
        "annual_min": null,
        "annual_max": null,
        "summary": null
      },
      "skills": ["special education", "iep"],
      "taxonomies": ["Education"],
      "apply_url": "https://www.applitrack.com/wvde/onlineapp/jobpostings/Output.asp?AppliTrackJobId=48210&AppliTrackLayoutMode=detail&AppliTrackViewPosting=1",
      "url": "https://www.applitrack.com/wvde/onlineapp/jobpostings/Output.asp?AppliTrackJobId=48210&AppliTrackLayoutMode=detail&AppliTrackViewPosting=1",
      "source": "applitrack",
      "posted_at": "2026-07-09T00:00:00.000Z",
      "expires_at": "2026-08-15T00:00:00.000Z",
      "updated_at": "2026-07-14T06:41:12.000Z"
    }
  ],
  "total": 2,
  "limit": 2,
  "offset": 0
}
```

Two things in there are unusual and both come straight from the platform.
`expires_at` is populated: districts post a hard application deadline, and most
ATS expose none at all. And `cities` is null while `countries` is `["United
States"]` — that is not a gap in our geocoding, it's the honest answer. See
[below](#location-is-a-building-never-a-place).

## How AppliTrack boards actually work

One GET, no auth, no pagination, the whole board **including every description**:

```bash
curl "https://www.applitrack.com/<district>/onlineapp/jobpostings/Output.asp?all=1"
```

`<district>` is the path segment from the board URL. That's the entire fetch
surface. `?all=1` is the whole thing — there is no page two, and there is no
detail request, because the bodies are already inline.

<Note title="The best jobs-per-request source we have">
  Measured 2026-07: `wvde` returned **1,078 jobs in 9.4MB from one call**;
  `jcps` returned 32 from one call. Every other platform on this site costs you
  either pagination, an N+1 detail pass, or both. AppliTrack costs you one
  request per district and nothing else.

  Budget for the size, though: a big district's response is ~9MB, so a 20-second
  timeout is not enough. Ours allows 90.
</Note>

### The response is not HTML

This is the one real wrinkle, and it stops a naive parser dead. `Output.asp`
does not return a document. It returns a **JavaScript program** whose markup is
embedded inside `document.write('…')` string literals, which means every quote
in the payload arrives backslash-escaped:

```
document.write('<ul class=\'postingsList\' id=\'p48210_\'>…');
```

The board's own `view.asp` bootstraps this by letting the browser execute it. You
don't need to. Unescape `\'` and `\"` and you have ordinary markup you can parse
with ordinary tools — no JS engine, no headless browser. But feed the raw
response to an HTML parser and you get a document containing one `<script>` and
zero jobs, at HTTP 200.

### Fields are label/value span pairs

Inside each `<ul class='postingsList' id='p<JOBID>_'>` block, the structured
fields are pairs:

```html
<span class="label">Location:</span><br/>&nbsp;<span class="normal">Nimitz High School</span>
```

| Label | What it becomes |
| --- | --- |
| `Position Type` | The district's own category — `Elementary School Teaching`, `Substitute`, `District` |
| `Date Posted` | The posting date, US `M/D/YYYY`, no zone |
| `Closing Date` | A real application deadline |
| `Location` | The **school or building**, not a place |
| `Additional Information` | The description body |

Read those by matching `label` → `normal` span pairs. The tempting shortcut is a
flat text scan that grabs everything after a label until the *next known* label,
and it bleeds badly — see below.

## What bites you

### 77% of postings have a description, and that is the ceiling

This is the number to know about AppliTrack, and it is not a parse bug we haven't
got round to fixing. It's the platform.

Plenty of districts don't type the posting into the form. They attach it as a
**PDF** and leave the body reading `"Please click the link to view the job
description. Attachment(s): Licensed Journeyman Plumber.pdf"`. At `aldineisd`,
**199 of 290 postings** are that. The per-job detail page carries no body either
— we checked, it's PDF-only there too. There is nothing to parse, so a null
description is the correct answer rather than a missing one.

<Warning title="A single district will flatter you">
  Sampled on two small districts, AppliTrack looks like 100% descriptions —
  `jcps` is 32 of 32. Across a real 24-board rotation it is **919 jobs, 712 with
  a body: 77%**.

  If you build this and measure it on the first district you find, you will
  believe you have a perfect source, ship it, and discover the gap in
  production. Measure on a rotation, not on a board.
</Warning>

Recovering the rest means running a PDF text extractor over the attachments.
That's a real option and not a wall, but it is a different project from scraping
a board, and until someone does it, 77% is what AppliTrack is.

### "Location" is a building, never a place

The field is labelled `Location:` and it never contains one. Across three
districts, all **76 distinct values** were school and facility names — `Nimitz
High`, `Special Education Building`, `District-Wide`, `Various`. **0 of 115**
postings geocoded. Feeding those to a geocoder produced 3% country fill and 8,979
unresolved rows.

So the school goes in `team`, where it's true, and the location field gets the
one geographic fact the platform actually supports: the country. AppliTrack is
Frontline's **US K-12** product. Of 1,486 discovered districts, none carries a
non-US signal and 352 use US-exclusive terms (`ISD` = Independent School
District, `USD` = Unified School District). The country is asserted. The city is
**not invented** — the posting names no city, state or country anywhere, and the
board page carries no district address either.

### Districts invent their own labels

There is no fixed label vocabulary. Districts add their own: `Date Available:`,
`SALARY/STIPEND:`, `Attachment(s):`, `STARTING DATE:`. A parser that scans flat
text and stops at the next label it *knows about* will sail straight past an
unlisted one and swallow it, producing locations like:

```
"District Date Available: 09/01/2026"
```

Reading `label` → `normal` span pairs needs no list of labels at all, which is
why that's what we do. Quoting and padding vary per district too (`'label'` vs
`"label"`, `" Closing Date: "`), so the match has to tolerate both.

### JobIDs are district-local

`JobID: 5` exists on more than one board. Key a posting by the bare id and
districts silently overwrite each other's jobs. The key has to carry the
district: `wvde_48210`.

### Dates are bare US calendar dates

`Date Posted` and `Closing Date` are `M/D/YYYY` with no zone, in district-local
time. `Date.parse` reads them at *your* local midnight, which shifts them a day
in either direction depending on where your server is. They need a bare-date
parser, not a timestamp parser.

## Or use ours

The fetch is genuinely one line. It's everything around it that isn't: the
`document.write` unescape, the label-pair reader that survives a district
inventing `SALARY/STIPEND:`, the school going to `team` instead of poisoning the
geocoder, the district-scoped key, and the bare-date handling.

You get all of that, plus [dedup](/docs/guides/deduplication) against the same
role on [LinkedIn](/ats/linkedin), and `expires_at` fed from the district's real
closing date rather than waiting for a board diff to notice the job is gone.

<Note title="Where we're honest about the join">
  The [company join](/docs/objects/company) is **partial** on AppliTrack — around
  21% of postings resolve to an employer domain, well below what we manage on
  platforms that publish one. A district posting names no website and no domain, and
  "Jefferson County" is a name that belongs to a dozen places. We resolve what
  we can and leave the rest rather than guess a school district's identity.
</Note>

And the two numbers that matter: **77% descriptions**, and **100% country, 0%
city**. Both are the platform's ceilings, not ours, and we would rather you knew
them before you query than after.

Query it with `?source=applitrack`.
[Quickstart](/docs/quickstart), then [pricing](/pricing).
