Verified university program data, kept current for you
Open Learning Feed is designed to collect the program and tuition feeds that universities publish on their own .edu domains. The plan is one API for all of them, and a notification within the hour when a school changes a price, a deadline or a program's status.
A working sample API runs at /v1 on this site with no key required. Everything marked Planned below (API keys and the dashboard, pricing for large-scale use, subscriptions and notifications, the bulk export, the embed and WordPress plugin, the MCP server and the api.olfeed.org domain) describes how it is designed to work; none of it is available yet. Examples use Example University, a fictional school; every figure for it is invented.
Quickstart
-
Get an API key Planned
Keys will come from your dashboard once accounts open. The sample API on this site needs no key.
-
Pull a school's programs
Ask for one school by its domain. The response lists every program accepting applications (add
?status=allfor the rest), with its prices, source and verified date.curl https://api.olfeed.org/v1/institutions/example.edu/programs \ -H "Authorization: Bearer $OLF_API_KEY"
const res = await fetch( "https://api.olfeed.org/v1/institutions/example.edu/programs", { headers: { Authorization: `Bearer ${process.env.OLF_API_KEY}` } } ); const { data } = await res.json();import os, requests res = requests.get( "https://api.olfeed.org/v1/institutions/example.edu/programs", headers={"Authorization": f"Bearer {os.environ['OLF_API_KEY']}"}, ) programs = res.json()["data"] -
Subscribe to changes Planned
Register a URL, and Open Learning Feed will send it a signed notification whenever a field you care about changes, so your pages update without re-scraping. Until subscriptions open, poll
/changes.curl -X POST https://api.olfeed.org/v1/subscriptions \ -H "Authorization: Bearer $OLF_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://yoursite.example/hooks/olf", "institutions": ["example.edu"], "events": ["field.changed", "program.status_changed"], "fields": ["tuition.*"] }'
Authentication and pricing Planned
Once keys launch, you'll send yours as a bearer token on every request. Keys are planned to be scoped to one account and rotatable from the dashboard without downtime. The sample API ignores the header.
Authorization: Bearer olf_live_xxxxxxxxxxxxxxxxxxxx
The data stays open. Anyone can read a school's feed directly from its .edu domain, and affiliate and comparison sites pay nothing to use Open Learning Feed. Schools are the paying side: the more sites read their feeds, the more the feeds are worth to them.
Free
Affiliate and comparison sites, with no plan to charge- Every school's feed, read directly or through the API
- Change notices for every school you list
- Price history and the change stream
- Embed and WordPress plugin
- The "Verified by the institution" badge
Priced separately
Large aggregators and AI companies- Nightly bulk export
- MCP server for AI assistants
- Uptime commitments and dedicated support
Only uses far beyond a typical affiliate site. Prices are not set yet.
Finding schools
Every participating school publishes one file at the same address: https://<school-domain>/olf.json, served as application/json. It is plain JSON that follows schema.org (JSON-LD), so any JSON parser reads it. Consumers should also accept the earlier name /olf.jsonld: try /olf.json first, then that. There is nothing else to discover. To check a school directly, fetch that address and follow any redirect. To find schools at scale, use the directory at GET /institutions.
curl -L https://www.example.edu/olf.json
The feed's publisher.contactPoint says who to contact at the school.
API reference
All responses are JSON. Prices are numbers in the stated currency and unit. Every tuition rate, fee and discount carries a source and a verifiedDate. Add ?format=json to a school's programs endpoint or to one program to get the school's own feed entries (plain JSON that follows schema.org) instead.
| Method | Path | Returns |
|---|---|---|
| GET | / | The API's name, version and a link to these docs |
| GET | /institutions | Directory of participating schools, with feed status, version and program count |
| GET | /institutions/{domain} | One school: name, accreditation and feed status |
| GET | /institutions/{domain}/programs | Every program at a school, with current prices |
| GET | /programs/{id} | One program in full |
| GET | /institutions/{domain}/tuition/history | Past rates with valid-from and valid-through dates. Add ?field= for one rate |
| GET | /changes?since={cursor} | Every change across subscribed schools since a cursor. ?limit= up to 500 |
| GET | /validate?url={feed} | Fetch a feed by its https address and check it against the specification |
| POST | /validate | Check the feed sent as the request body |
| POST | /subscriptions | Create a notification subscription (not available yet: the sample API answers 501) |
| DELETE | /subscriptions/{id} | Remove a subscription (not available yet: the sample API answers 501) |
These requests go to the working sample API on this site, which needs no key yet. It serves sample data for Example University, a fictional school.
Query parameters for /programs
| Parameter | Type | Description |
|---|---|---|
level | string | associate, bachelor, master, doctoral, certificate |
mode | string | online, onsite, hybrid |
status | string | Defaults to accepting_applications. Use all to include discontinued programs. |
cip | string | CIP 2020 code or prefix, such as 43.0401 or 43 |
audience | string | Return only the prices and discounts one group can use: rates for everyone plus rates for that group, such as active_duty_military or veteran |
fields | string | Comma-separated top-level fields to keep, such as tuition,transferCredit. id and name are always included |
Bulk export and change stream
Large consumers load everything once, then follow changes. The nightly export (planned) is to be one gzipped JSON Lines file with a record per program. The change stream, which the sample API already serves, returns every change after a cursor, oldest first, so you can replay anything you missed.
curl "https://api.olfeed.org/v1/changes?since=c_2026-01-13&limit=500" \ -H "Authorization: Bearer $OLF_API_KEY"
cursor = load_saved_cursor() # your own storage
while True:
page = requests.get(f"{BASE}/changes", params={"since": cursor, "limit": 500},
headers=AUTH).json()
for change in page["data"]:
apply_change(change) # update your database
cursor = page["nextCursor"]
save_cursor(cursor)
if not page["hasMore"]:
break
Change notifications Planned
As designed, Open Learning Feed checks each feed hourly. When a school publishes a new version, it compares it with the last one and sends a notification for each subscribed change. Your endpoint must answer with any 2xx status within 10 seconds. Failed deliveries retry with backoff for 72 hours, and every event stays available in the change stream.
Each change names the value with a path such as tuition.undergraduate.standard: the field, the program level and the rate. The same paths are used by /changes, by ?field= on the history endpoint, by subscription filters (tuition.* matches every rate) and by the embed.
| Event | Sent when |
|---|---|
field.changed | Any subscribed value changes, such as a tuition rate or an application deadline |
program.created | A school adds a program to its feed |
program.status_changed | A program moves to paused, teach-out or discontinued. Remove it from your listings. |
feed.invalid | A school's new feed fails validation. Open Learning Feed keeps serving the last valid version. |
feed.unreachable | A school's feed has not answered for 48 hours |
Headers
| Header | Contains |
|---|---|
OLF-Event-Id | Unique event ID. Store it and ignore repeats. |
OLF-Timestamp | Unix seconds when Open Learning Feed signed the event |
OLF-Signature | v1= followed by an HMAC-SHA256 of timestamp.body using your signing secret |
Verifying notifications
Check the signature before you trust a notification. Reject events older than five minutes to block replays. For extra assurance, fetch the school's own feed and confirm the new value appears there.
import crypto from "node:crypto";
export function verify(rawBody, headers, secret) {
const ts = headers["olf-timestamp"];
const sig = headers["olf-signature"];
if (!ts || !sig || !/^\d+$/.test(ts)) return false;
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // too old
const expected = Buffer.from(crypto
.createHmac("sha256", secret)
.update(`${ts}.${rawBody}`)
.digest("hex"));
const sent = Buffer.from(sig.replace("v1=", ""));
if (sent.length !== expected.length) return false; // timingSafeEqual throws on unequal lengths
return crypto.timingSafeEqual(sent, expected);
}
import hmac, hashlib, time
def verify(raw_body: bytes, headers: dict, secret: str) -> bool:
ts = headers.get("OLF-Timestamp", "")
sig = headers.get("OLF-Signature", "")
if not ts.isdigit() or not sig:
return False
if abs(time.time() - int(ts)) > 300: # too old
return False
expected = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body,
hashlib.sha256).hexdigest()
return hmac.compare_digest(sig.removeprefix("v1=").encode(), expected.encode())
whsec_demo_example
Embed and WordPress Planned
If you run a content site and don't want to store data at all, you'll be able to embed a live block instead. It is designed to read Open Learning Feed each time the page loads, so the numbers on your page stay current. A WordPress plugin will wrap the same block in a shortcode.
<div data-olf-school="example.edu"
data-olf-show="tuition.undergraduate.standard,tuition.masters.standard,tuition.masters.military"></div>
<script src="https://embed.olfeed.org/v1/block.js" async></script>
[olf_tuition school="example.edu" show="tuition.undergraduate.standard,tuition.masters.standard,tuition.masters.military"]
AI assistants (MCP) Planned
A planned Open Learning Feed MCP server will let AI companies and agent builders connect an assistant directly to verified data. Model Context Protocol is a standard way to give an AI assistant tools it can call. Answers would then come from the school's own feed, with the source and date attached, instead of from whatever affiliate page a crawler found.
| Tool | What the assistant can ask |
|---|---|
search_programs | Find programs by field of study, level, mode and price range |
get_tuition | Current rates for a school, including audience-specific prices and discounts |
compare_programs | Side-by-side values for two to five programs, each with its source |
get_change_history | When and how a school's published values changed |
{
"mcpServers": {
"olf": {
"url": "https://mcp.olfeed.org/v1",
"headers": { "Authorization": "Bearer ${OLF_API_KEY}" }
}
}
}Display and badge rules
Sites that follow these rules may show the "Verified by the institution" badge. Open Learning Feed plans to audit badge sites, and a site that breaks a rule would lose the badge until it is fixed.
Update within 24 hours
Apply every subscribed change within 24 hours of the notification, or poll the change stream at least daily.
Show the verified date and source
Display the
verifiedDateand link to the school'ssourcepage next to any price.Never edit values
Show the number exactly as the school published it. Rounding, "starting at" rewording or mixing in third-party figures without labeling them removes the badge.
Remove discontinued programs
Take down or clearly mark a program within 24 hours of
program.status_changed.Label audience prices
Never show a military, veteran or first-responder rate as the general price.
Field reference
Each API field maps to a schema.org term or to the olf extension. Required fields must be present for a school's feed to pass validation. School feeds never include salary, career, job or occupation data, so the API doesn't return them. The table lists the main fields. tuition[] and fees[] list current and announced rates; earlier rates come from the history endpoint. Responses also carry timeToComplete, prerequisites, transferCredit.sources, accreditation[], the program's source page and updated date; each discount's percent or amount and combinable; each fee's type; and each schedule's every and description.
| Field | Type | Tier | Maps to | Meaning |
|---|---|---|---|---|
program.name | string | Required | name | Program name as the school publishes it |
program.status | enum | Required | olf:programStatus | accepting_applications, launching, paused, teach_out, discontinued |
program.level | enum | Required | educationalCredentialAwarded.educationalLevel | Credential level awarded |
program.mode | enum | Required | educationalProgramMode | Online, onsite (in person) or hybrid |
tuition[].amount | number | Required | price | Price in currency per basis |
tuition[].basis | enum | Required | olf:tuitionBasis | per_credit_hour, per_course, per_term, per_academic_year, flat_rate_program, subscription_term |
tuition[].source | url | Required | olf:sourceUrl | Page on the school's site where the value appears |
tuition[].verifiedDate | date | Required | olf:verifiedDate | Date the school last confirmed the value |
tuition[].audience | enum[] | Recommended | olf:eligibleAudience | Groups the price applies to. Empty means everyone. |
discounts[] | object[] | Recommended | olf:TuitionDiscount | Grants such as a 10% Veteran Grant, with eligibility and stacking rules |
fees[] | object[] | Recommended | priceComponentType | Technology, materials, application and graduation fees |
program.credits | integer | Recommended | numberOfCredits | Credits required to complete |
transferCredit.max | integer | Recommended | olf:maxTransferCredits | Most credits a student can transfer in |
program.nextStart, program.applicationDeadline | date | Recommended | startDate, applicationDeadline | Next start date and the deadline to apply for it |
program.admissionTestPolicy | enum | Recommended | olf:admissionTestPolicy | test_required, test_optional, test_not_required, waiver_available |
program.completion[] | object[] | Recommended | olf:completionTime | Typical and fastest time to finish by enrollment (full_time, part_time, accelerated) and assumed transfer credits |
program.schedule[] | object[] | Recommended | olf:classSchedule | When classes meet: daytime, evening, weekend, synchronous_online or asynchronous_online, with days, times and time zone |
program.focusAreas[] | object[] | Optional | olf:focusArea | Concentrations, specializations or tracks. type carries the school's own word. |
program.cip | object | Optional | programType | CIP 2020 classification as { code, name }, or null |
Errors and limits
Errors return JSON as { "error": { "code", "message" } }. These are the codes the sample API returns today. When API keys launch, three more are planned: 401 invalid_key, 403 plan_required (for the separately priced bulk and AI access) and 429 rate_limited (wait the seconds in Retry-After).
| Status | Code | What to do |
|---|---|---|
| 400 | missing_url, invalid_url | Validation by address needs ?url= with an https feed address, or send the feed as the POST body. |
| 404 | institution_not_found | The school has no feed yet. Check GET /v1/institutions for participating schools. |
| 404 | program_not_found, field_not_found | Check the program id, or the rate name for ?field=; the error lists the available names. |
| 404 | not_found | No such endpoint. Check the path against the table above. |
| 405 | method_not_allowed | Use GET, except for POST /validate. Subscription calls answer 501 until they launch. |
| 413 | feed_too_large | The feed, sent as the POST body or fetched by ?url=, is over 2 MB. Split a large institution into feeds listed with olf:includesFeed. |
| 501 | not_available_yet | Subscriptions aren't available yet. Use /changes in the meantime. |
| 502 | feed_unreachable | The school's server didn't answer or returned an error. Try again later. |
| 500 | internal_error | Something went wrong on our side. Try again. |
Sandbox school
Planned: a sandbox domain, sandbox.example.edu, will step through Example University's rate history every hour and send you the matching field.changed events, then start over. Until it launches, the sample API serves example.edu, and the notification demo above shows the events it will send.
| Step | Captured | Undergraduate | Master's | Military, master's |
|---|---|---|---|---|
| 1 | May 2022 | $310 | $420 | $250 |
| 2 | Jun 2023 | $335 | $445 | $250 |
| 3 | Jun 2024 | $350 | $465 | $290 |
| 4 | Dec 2024 | $350 | $465 | $250 |
| 5 | Apr 2026 | $365 | $480 | $250 |
Rates per credit hour for the fictional sandbox school. The military undergraduate rate was $250 at every step.
Changelog
| Date | Version | Change |
|---|---|---|
| Oct 10, 2026 | v1 draft | First draft of the developer documentation |