US College Accreditation Action Delta avatar

US College Accreditation Action Delta

Pricing

$10.00 / 1,000 accreditation action detecteds

Go to Apify Store
US College Accreditation Action Delta

US College Accreditation Action Delta

Watches the US Department of Education DAPIP registry and reports only genuinely new college accreditation actions: probation, show cause, heightened monitoring, loss of accreditation, closure. Filter by state, institution or agency. For students, investors, lenders, compliance, journalists.

Pricing

$10.00 / 1,000 accreditation action detecteds

Rating

0.0

(0)

Developer

Radu Furtuna

Radu Furtuna

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

7 days ago

Last modified

Share

Durable monitor for DAPIP — the US Department of Education's Database of Accredited Postsecondary Institutions and Programs. It watches the official accreditation-action journal and reports only actions that are genuinely new since your last check: probation, show cause, heightened monitoring, warning, loss of accreditation, voluntary withdrawal, institution closure, additional-location closure, and the positive counterparts (initial accreditation, probation removed, renewal) if you want them.

Built for students and families checking a school before enrolling, investors and lenders exposed to education companies, edtech and OPM partners, compliance and vendor-risk teams, and journalists tracking college closures. No API key, no login, no captcha.

Source

POST https://ope.ed.gov/dapip/api/downloadFiles/accreditationDataFiles with body {"CSVChecked": true, "ExcelChecked": false} returns DAPIPData.zip (~3.7 MB), the same bulk export the DAPIP website itself offers. Verified live on 13.09.2026: HTTP 200, Content-Type: application/zip, no key, no captcha; GET on the same route returns 405 — it is POST-only. Inside are exactly three files; this actor reads AccreditationActions.csv (87,557 actions) and InstitutionCampus.csv (45,135 campuses, used to attach institution name, campus type, address and state to every action).

The archive is generated on every request, so each run sees the registry as of that moment. In the live snapshot there were 5,483 actions dated 2026 (roughly 600 a month), including 234 Heightened Monitoring or Focused Review, 233 Loss of Accreditation: Voluntary Withdrawal, and 113 Probation.

How it works

  1. Each watch is a combination of actionCategory (adverse by default), optional state, institutionNameContains, agencyNameContains, and lookbackDays (default 365).
  2. Every run downloads the ZIP once — cached for the whole run no matter how many watches you define — verifies it, and then filters that one shared snapshot per watch.
  3. Before a single action is classified as new, the snapshot must pass an integrity gate: ZIP CRC check, exactly the expected set of member files, decompressed-size limits, strict UTF-8 decoding, a minimum row count, a check against the row count of the last accepted snapshot, no conflicting duplicate campus ids, and 100% coverage of actions by the campus file (otherwise state/name filters would silently miss actions). If any of that fails, the entire run reports source_access_limited and touches no baseline, no history, no watch binding and no billing.
  4. Genuinely new actions are pushed to the dataset and billed at most once each (accreditation-action-detected). A run that finds nothing new costs nothing beyond the fixed platform run cost.

Input

{
"monitorId": "my-accreditation-watch",
"userAgentContact": "you@example.com",
"watches": [
{ "watchId": "california-adverse", "actionCategory": "adverse", "state": "CA", "lookbackDays": 365 },
{ "watchId": "wasc-agency", "actionCategory": "adverse", "agencyNameContains": "WASC" },
{ "watchId": "example-college", "actionCategory": "all", "institutionNameContains": "Example College" }
],
"notifyOn": "new_alerts",
"webhookUrl": "https://example.com/webhook"
}

Add more watches later under the same monitorId — each keeps its own independent history. A watchId is permanently bound to the filter it first saw (including lookbackDays); pointing the same watchId at a different filter later fails the run instead of silently mixing histories.

Output row (per action)

watchId, stableRowId, changeType ("new_action"|"action_revised"), dapipId, institutionName, parentName, locationType, state, address, agencyId, agencyName, programId, programName, sequentialId, actionDescription, severity, actionDate, endDate, justification, justificationOther, occurrence, contentHash, previousContentHash, sourceUrl, monitorId, runId, discoveredAt, eventId, billed

severity is one of critical, warning, adverse_other, positive, neutral, unclassified.

Billing

Pay-per-event: accreditation-action-detected — charged only for an action genuinely new since the previous check of that watch (a retroactive ED edit of an already-published action counts as one new action — see the identity note under Honest limits). The first check of a new watch establishes a baseline and is not charged. Failed or gate-blocked runs are never charged.

Important — read before relying on this

This is an informational monitor of what the Department of Education publishes to its DAPIP registry. It is not an assessment of the quality of any school, not a prediction that a school will close, and not legal, financial or enrolment advice. An accreditation action is a regulatory event with context that this registry does not carry; an institution on probation is not the same as an institution that has lost accreditation, and many monitoring statuses are later removed. ED publishes actions with a lag and does not guarantee completeness. Always confirm any single entry directly with DAPIP, the accrediting agency, or the institution before acting on it.

Delivery guarantee: at-most-once (not exactly-once)

For any one computed change, the row is delivered to the dataset and the event is charged no more than once. The right to do either is handed out by the only atomic primitive Apify offers — RequestQueue.addRequest(uniqueKey) — in a separate, named request queue (<storage prefix>-<monitorId>-claims) used purely as an append-only journal of granted rights: its requests are never handled and never deleted. Apify's key-value store has no compare-and-swap, conditional write or ETag, so it cannot be the source of that right; it only holds a diagnostic state machine for reporting. The same gate also decides, atomically, which filter a watchId is permanently bound to — that claim is taken only after the snapshot integrity gate passes, so a run that fails the gate still leaves no durable trace.

The trade-off is deliberate: if a run crashes after taking the claim, that change can be lost (it is closed as dataset_unknown / charge_unknown and never re-delivered) — but you are never charged twice for it. Two boundaries of the guarantee, stated honestly:

  • it holds for as long as the named claims queue exists. Anyone with account access can delete or recreate that queue through the Console/API, which starts the journal from scratch — an unavoidable limit of any durable storage, not a defect of the protocol;
  • it applies from the build in which the claim gate shipped onward. Older builds must not keep running against the same monitorId.

Honest limits

  • The DAPIP API is undocumented. The route used here (ope.ed.gov/dapip/api/...) is the one the DAPIP website's own front-end calls; ED publishes no contract for it and is under no obligation to keep its shape. If the response shape changes, this actor fails closed (structure_changed / source_access_limited, zero charges) rather than inventing data — but it will need a code update before it works again. This is a real risk, stated up front rather than buried.
  • The source gives its action rows no stable identifier. Verified by recounting all 87,557 live rows: DapipId identifies the institution, not the action (only 26,780 distinct values); even all eleven columns together leave 83,393 distinct rows, because an identical row can repeat up to 23 times. Identity is therefore synthesised deterministically: a full SHA-256 of the action's key fields (with actionDate normalised to ISO first, so a cosmetic 6/26/...06/26/... reformat by ED cannot mint a new id), plus the payload's content hash, plus an occurrence number counted only among fully identical rows. Occurrence is deliberately not a position in the sorted key group: numbering a whole group that way meant a legitimately appended row could take over an existing row's id and push that row onto a new one — one append billed as two events.
  • A retroactive edit by ED therefore reads as a new action, not as a "revision" of the old row. Because the payload hash is part of the identity, if ED later fills in an endDate or rewrites a justification on an already-published action, the old row id disappears and a new one appears; you are billed once, for one new action. This is the honest reading: with no source-side row id there is no way to prove that the edited row is the same row, and the previous scheme paid for that unprovable claim by letting edits disturb neighbouring rows' ids. The actor still tells apart such an in-place edit from ED actually deleting a row: it compares the size of the row's key group in the snapshot with the size it recorded last time (see the append-only guarantee below).
  • lookbackDays is a real window, not a nicety. An action older than the window is invisible to that watch. Schedule runs comfortably more often than your lookbackDays, or an outage longer than the window will let actions slip past.
  • Each watch is capped at 10,000 matching actions per run, newest-first. Live volumes for reference: adverse over 365 days = 2,247 across the entire country, adverse over the maximum 1,095 days = 7,635 — both fit. Only actionCategory: "all" over long windows exceeds the cap (26,714 over 1,095 days). When that happens, coverage.watches[].windowFull and matchedCount say so explicitly; data is never silently dropped.
  • A new action type ED invents will be shown, not hidden. All 47 action descriptions present in the live registry are classified by severity. Anything outside that vocabulary is labelled severity: "unclassified" and is included under actionCategory: "adverse" and "all" — the deliberate choice being that showing you an unrecognised action is better than hiding what might be the most serious one.
  • State comes from parsing the campus address, which succeeds for 44,896 of 45,135 campuses (99.5%). For the rest, state is null and a watch filtered by state will honestly not see them.
  • The durable dataset is a delivery-attempt log, not a guaranteed mirror of the default dataset. Each row is written to the durable dataset first, then mirrored to the run's default dataset before billing proceeds for that row. If the durable write succeeds but the mirror write fails, the item is marked dataset_unknown, billing for it is permanently blocked (fail-closed — we never charge for a row we can't confirm was delivered), and that exact row is not re-created. The durable dataset can therefore hold a small number of orphan rows that were never mirrored and never billed. The default dataset is the canonical log of rows successfully written to this run's output (see its run_summary row) — but a default-dataset row does not by itself prove the row was billed: it is written before Actor.charge() runs, so if charging then fails or returns charge_unknown, the row is present but not confirmably paid. run_summary.eventsBilled and Apify's own billing ledger are the source of truth for confirmed payment, not the presence of a row in either dataset.
  • No server-side filtering exists for this export (verified live: the endpoint accepts only {CSVChecked, ExcelChecked}). All filters are applied on our side over one shared snapshot per run; they reduce the rows delivered to you, never the network cost.
  • Append-only is enforced per record, not by row count. History is kept per action (not a capped seen-ids list): each action's latest content hash, its actionDate, its key-group hash and a persistent change-sequence counter are stored per watch. A record is dropped from that index only once its actionDate is safely past the watch's lookbackDays boundary. If a record that is still inside the window vanishes from a snapshot, and that disappearance is not explained by an in-place payload edit (its key group did not shrink) or by the 10,000-row window cap, the watch reports source_access_limited for that run: nothing is pruned, nothing is delivered, nothing is charged. A whole-file row-count gate cannot do this job — ED could drop one old row and add a new one, leaving the count flat, and the dropped row would later be re-billed as new when it came back.
  • An explained disappearance is remembered, not forgotten. When a record's disappearance is explained by an in-place payload edit, its id is not deleted from the index — it is frozen there under a tombstone (firstMissingAt / lastSeenAt / missingRuns) until its actionDate ages out of the window naturally. So if ED later publishes both the original row and the edited one, the original arrives as something we already know: zero dataset rows and zero charges for it. Because a tombstoned id no longer counts towards its key group, a legitimate payload edit also never wedges the watch into a permanent source_access_limited.
  • If any required field is empty, a nullable field contains only whitespace, a column disappears, a date stops parsing, or a row's column count is wrong, the run reports it honestly and charges nothing, instead of building a delta on data whose completeness can't be trusted.

Author: OmniCoder (https://t.me/OmniCoder)