Skip to content

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.

Your first request
curl https://api.tuitioncovered.com/v1/scholarships?limit=50 \
  -H "Authorization: Bearer $TC_API_KEY"

Endpoints

EndpointReturns
GET /v1/scholarshipsActive scholarships, oldest first: { data, next_cursor }
GET /v1/scholarships/:idOne scholarship by id (its slug): { data }, or 404
GET /v1/collectionsYour saved searches, each with copy for its page: { data }
GET /v1/collections/:id/scholarshipsA collection's records, paged like /v1/scholarships
GET /healthLiveness. No key needed

List parameters

ParameterMeaning
limit1–100, default 50
cursorThe previous response's next_cursor. Opaque: pass it back as it is
updated_sinceISO 8601 with an offset. Only records changed since then: use it to sync
tierlisted or certified. Leave it out for both
qWords that must all appear in the title, provider or summary
stateA USPS code: scholarships with a rule for that state
levelYears 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.js
// 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.

FieldHolds
id, title, provider, summaryWhat it is and who funds it. id is a stable slug
official_url, apply_urlThe provider's own page, and a tracked link to apply that credits your company
awardamount, amount_max, amount_type (fixed, range, varies, full_tuition), number_of_awards, renewable
datesopens_on, deadline, deadline_precision (day, day_no_year, month), recurring
eligibilitycriteria in the provider's words, plus education_levels, degree_types, citizenship, states, min_gpa, need_based, requires_fafsa, first_generation
no_essaytrue when no essay is asked for
qualitytier, certified_at, last_verified_at, completeness_score (0–1)
updated_atWhen the record or your copy of it last changed. Pairs with updated_since
contentCopy 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
GET /v1/scholarships/:id → data
{
  "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.

HeaderMeaning
X-RateLimit-LimitRequests a minute for this key
X-RateLimit-RemainingRequests left in this minute
X-Daily-Records-RemainingRecords left today across the company's keys (when there's a daily limit)
Retry-AfterSeconds 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 codeWhen
401 unauthorizedMissing, unknown or revoked key, or the company's access isn't live
400 invalid_requestA bad parameter; the message names it
404 not_foundNo active scholarship with that id
429 rate_limitedOver the per-minute limit. Retry-After says when to try again
429 daily_limit_reachedThe company's records for the day are used. Retry-After runs to UTC midnight
500 internalOur fault. Retry with backoff
400 Bad Request
{
  "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