US College Accreditation Action Delta
Pricing
$10.00 / 1,000 accreditation action detecteds
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
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
7 days ago
Last modified
Categories
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
- Each
watchis a combination ofactionCategory(adverseby default), optionalstate,institutionNameContains,agencyNameContains, andlookbackDays(default 365). - 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.
- 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_limitedand touches no baseline, no history, no watch binding and no billing. - 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, billedseverity 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:
DapipIdidentifies 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 (withactionDatenormalised to ISO first, so a cosmetic6/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
endDateor 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). lookbackDaysis a real window, not a nicety. An action older than the window is invisible to that watch. Schedule runs comfortably more often than yourlookbackDays, 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:
adverseover 365 days = 2,247 across the entire country,adverseover the maximum 1,095 days = 7,635 — both fit. OnlyactionCategory: "all"over long windows exceeds the cap (26,714 over 1,095 days). When that happens,coverage.watches[].windowFullandmatchedCountsay 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 underactionCategory: "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,
stateisnulland 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 itsrun_summaryrow) — but a default-dataset row does not by itself prove the row was billed: it is written beforeActor.charge()runs, so if charging then fails or returnscharge_unknown, the row is present but not confirmably paid.run_summary.eventsBilledand 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 itsactionDateis safely past the watch'slookbackDaysboundary. 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 reportssource_access_limitedfor 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 itsactionDateages 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 permanentsource_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)