Developers
Catalog API reference.
Read-only JSON over HTTPS at https://api.tuitioncovered.com. Two endpoints, one record shape, and limits you can see in every response.
Authentication
Bearer keys from your portal. Every /v1 request needs Authorization: Bearer tc_live_…. Owners and admins create keys under API → Keys in the partner portal while your company's access is live. A key is shown once: we keep only a hash, so a lost key is revoked and replaced.
curl https://api.tuitioncovered.com/v1/scholarships?limit=50 \ -H "Authorization: Bearer $TC_API_KEY"
Endpoints
| Endpoint | Returns |
|---|---|
| GET /v1/scholarships | Active scholarships, oldest first: { data, next_cursor } |
| GET /v1/scholarships/:id | One scholarship by id (its slug): { data }, or 404 |
| GET /v1/collections | Your saved searches, each with copy for its page: { data } |
| GET /v1/collections/:id/scholarships | A collection's records, paged like /v1/scholarships |
| GET /health | Liveness. No key needed |
List parameters
| Parameter | Meaning |
|---|---|
| limit | 1–100, default 50 |
| cursor | The previous response's next_cursor. Opaque: pass it back as it is |
| updated_since | ISO 8601 with an offset. Only records changed since then: use it to sync |
| tier | listed or certified. Leave it out for both |
| q | Words that must all appear in the title, provider or summary |
| state | A USPS code: scholarships with a rule for that state |
| level | Years in school (HS_12, UG_1) or a stage (high_school, undergraduate, graduate), comma-separated |
Syncing
Page with cursors, sync with updated_since. Lists come oldest record first with an opaque next_cursor; it's null on the last page. Keep a copy by asking for what changed since your last run, and wait out a 429 with Retry-After.
// Sync everything changed since your last run, a page at a time.
let cursor;
for (;;) {
const url = new URL("https://api.tuitioncovered.com/v1/scholarships");
url.searchParams.set("updated_since", lastRun);
url.searchParams.set("limit", "100");
if (cursor) url.searchParams.set("cursor", cursor);
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.TC_API_KEY}` },
});
if (res.status === 429) {
await sleep(Number(res.headers.get("Retry-After")) * 1000);
continue;
}
const page = await res.json();
await save(page.data);
if (!page.next_cursor) break;
cursor = page.next_cursor;
}The scholarship record
Every field is always present, null when unknown. Fields are added over time; a rename or removal would come with a new version. apply_url goes through TuitionCovered so the click is credited to your company, then on to the provider's application.
| Field | Holds |
|---|---|
| id, title, provider, summary | What it is and who funds it. id is a stable slug |
| official_url, apply_url | The provider's own page, and a tracked link to apply that credits your company |
| award | amount, amount_max, amount_type (fixed, range, varies, full_tuition), number_of_awards, renewable |
| dates | opens_on, deadline, deadline_precision (day, day_no_year, month), recurring |
| eligibility | criteria in the provider's words, plus education_levels, degree_types, citizenship, states, min_gpa, need_based, requires_fafsa, first_generation |
| no_essay | true when no essay is asked for |
| quality | tier, certified_at, last_verified_at, completeness_score (0–1) |
| updated_at | When the record or your copy of it last changed. Pairs with updated_since |
| content | Copy for your site in your voice: meta_title, meta_description, intro, who_its_for, canonical_url, og, json_ld. null until you publish Brand & voice |
{
"id": "riverbend-first-gen-stem-award",
"title": "First-Generation STEM Award",
"provider": "Riverbend Community Foundation",
"official_url": "https://riverbendcf.org/scholarships/stem",
"apply_url": "https://app.tuitioncovered.com/go/riverbend-first-gen-stem-award?p=k7q2m",
"award": {
"amount": 5000,
"amount_type": "fixed",
"number_of_awards": 4,
"renewable": true
},
"dates": {
"opens_on": "2026-11-01",
"deadline": "2027-02-15",
"deadline_precision": "day",
"recurring": true
},
"eligibility": {
"criteria": [
"Graduating high school senior in Ohio",
"First in your family to attend a four-year college",
"Minimum 3.0 GPA"
],
"education_levels": ["HS_12"],
"degree_types": ["BACHELOR"],
"states": ["OH"],
"min_gpa": 3.0,
"requires_fafsa": true,
"first_generation": true
},
"no_essay": false,
"quality": {
"tier": "listed",
"last_verified_at": "2026-10-12T09:14:00Z",
"completeness_score": 0.92
},
"updated_at": "2026-10-12T09:14:03Z"
}Limits
Per key a minute, per company a day. Each key has a per-minute request limit in fixed one-minute windows. Your company may also have a daily record limit across all its keys (UTC days): a list page is trimmed to what's left. Both come from your contract and show in the portal.
| Header | Meaning |
|---|---|
| X-RateLimit-Limit | Requests a minute for this key |
| X-RateLimit-Remaining | Requests left in this minute |
| X-Daily-Records-Remaining | Records left today across the company's keys (when there's a daily limit) |
| Retry-After | Seconds to wait, on 429s |
Errors
One error shape. Errors are JSON with a stable code and a message for people. Branch on the code.
| Status and code | When |
|---|---|
| 401 unauthorized | Missing, unknown or revoked key, or the company's access isn't live |
| 400 invalid_request | A bad parameter; the message names it |
| 404 not_found | No active scholarship with that id |
| 429 rate_limited | Over the per-minute limit. Retry-After says when to try again |
| 429 daily_limit_reached | The company's records for the day are used. Retry-After runs to UTC midnight |
| 500 internal | Our fault. Retry with backoff |
{
"error": {
"code": "invalid_request",
"message": "cursor: not a cursor from this API"
}
}Need a key?
Keys come with a partner company. Tell us what you're building and we'll set you up with a pilot.
Request API access