Developer API — Preview

Start building with Magisterial

Everything you need to integrate college sports data into your product — from your first API call to production.

Base URL https://api.magisterial.ai/v1

curl https://api.magisterial.ai/v1/players/search \
  -H "Authorization: Bearer mag_live_xxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "sport": "soccer",
    "gender": "men",
    "division": "D1"
  }'

Quick start

Get an API key from the developer console, then make your first request. Keys are created and managed from your account — sign in and the console opens straight to key management.

1Create an API key

Open the console, create a key, and copy it — the full token is shown only once.

2Make your first request
curl https://api.magisterial.ai/v1/sports \
  -H "Authorization: Bearer mag_live_xxxxxxxxxxxxxxxxxxxx"
3Explore the API reference

Every endpoint, parameter, and response schema — with runnable requests and generated code samples — lives in the interactive API reference.

Introduction

The API is organized around predictable, resource-oriented URLs. It accepts JSON request bodies, returns JSON responses, and uses standard HTTP verbs, status codes, and authentication. Every response you receive is scoped to exactly the sports and divisions your plan includes — the same data you see inside the Magisterial app.

Authentication

Authenticate with an API key sent as a bearer token in the Authorization header. Keys are created and rotated from your account settings. Live keys are prefixed mag_live_ and test keys mag_test_. Test keys call the free read endpoints for real and are blocked from usage-billed endpoints, so they are safe to experiment with; live keys carry your plan’s full access. Treat keys like passwords, and never embed a live key in client-side code.

curl https://api.magisterial.ai/v1/players/search \
  -H "Authorization: Bearer mag_live_xxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{ "sport": "soccer", "gender": "men", "division": "D1" }'

Requests without a valid key return 401 Unauthorized.

Scoping & access

Access is governed per sport and per division by your subscription tier. Every request must declare a scope — a sport, gender, and division — and results never span beyond what your plan entitles you to. A request for a division you don’t hold returns 403 Forbidden with the required tier, rather than silently returning nothing.

Multi-division scope. Any sport-valid divisions can be combined into one query — pass "D1,NAIA,NJCAA-D1" to search across associations at once. The division reference lists atomic codes; clients compose the non-empty subset they need.

Data sources & freshness

Magisterial holds no licensed NCAA, conference, or vendor feed. Every record is compiled from publicly available sources — each program’s official athletics website on the Sidearm, Presto, and WMT platforms, stats.ncaa.org, TFRRS, NCAA transfer-portal listings, and federal IPEDS / College Scorecard data for institution profiles and the UNITID — and from the people the records describe: coaches and athletes who claim their profile, recruiting questionnaires, and in-product use. Contact data is exposed only under the contact capabilities described in Managed athletes below. Headshots and logos are the schools’ own published assets, served by reference — we claim no rights in them.

Freshness is a property of each record, not of the API as a whole. Team, roster, and coaching-staff responses each carry a freshness object (observed_at, checked_at, changed_at, next_check_at), and GET /v1/coverage lists the last run of every ingest lane. The refresh cadence follows the sport calendar:

DataInside its churn windowOtherwise
Rostersevery 24 hoursevery 7 days
Coaching staffsevery 7 days (Mar 1 to Jun 30)every 14 days
Season statisticsevery 7 days in seasonevery 14 days
Schedules and box scoresevery 24 hours, sport lanes every 4 to 6 hours in seasondaily
Movementsobserved at scrape time, identity resolution every 6 hourseditorial publish

Movement events carry both observed_at (when the roster or staff page change was observed) and published_at (when the event cleared identity resolution and entered the curated feed). Free and Developer keys read the curated published tier on GET /v1/movements; Enterprise keys can additionally read the resolved and observed resolver tiers directly.

Plans & pricing

Every key inherits its owner’s plan. Free is for students, hobbyists, and quick scripts. Developer is for ambitious personal projects and indie products. Enterprise is a per-customer agreement for companies that integrate the data into their own product.

Free
$0
Free forever

Students, hobbyists, and quick scripts

  • •Every free read endpoint: players, season stats, teams, rosters, coaches, games, careers, transfers
  • •10 requests per minute, 1,000 requests per month
  • •Live and test keys, webhooks, official SDKs
  • •No usage-billed endpoints
Developer
$99.99 / month
Billed monthly, cancel anytime

Ambitious personal projects and indie products

  • •Everything in Free at 300 requests per minute, 250,000 requests per month
  • •Usage-billed endpoints: transfer portal, natural-language query, alerts, bulk exports
  • •Bulk exports up to 500,000 rows per job
  • •Up to 25 transfer-portal alerts
Enterprise
Contact sales
Per-customer agreement

Companies integrating the data into their own product

  • •Custom rate limits and no monthly request cap
  • •Custom export ceilings and alert counts
  • •Data-license agreement covering attribution and redistribution
  • •Managed athletes: coach contacts on behalf of athletes who authorize your platform (250 seats by default)
  • •Coach directory access on custom plans
  • •Direct support channel with a named contact
  • •Invoiced usage billing on request
LimitFreeDeveloperEnterprise
Read requests per minute103001,000 (custom)
Requests per calendar month1,000250,000Unlimited
Usage-billed endpointsNot availableAvailableAvailable
Query runs per minute / in flight—10 / 330 / 10
Alert and export writes per minute—3060
Transfer-portal alerts—25250
Export rows per job / jobs in flight—500,000 / 25,000,000 / 5

Usage-billed endpoints are billed on top of the plan at list prices, under a monthly budget cap you set in the console. Buying Developer enables usage billing on the same subscription. Test keys can never create charges. Full plan and price details: Developer API pricing.

EndpointPriceNotes
GET /v1/portal$0.05 per requestAlso requires a verified Max coach on the account and the portal scope on the key.
POST /v1/queryModel cost × 3Billed by token usage once the run completes; the model is fixed server-side.
POST /v1/alerts$5 per alert per monthPer active transfer-portal alert; matches push to your webhook.
POST /v1/exports$0.10 per 10,000 rowsPer started block, billed only when the job succeeds.

A Free key calling a usage-billed endpoint receives 403 plan_required with required_plan: "developer".

Rate limits & quotas

Limits follow the plan and are shared by all of an account’s keys: a per-minute window on every endpoint, a calendar-month request quota on Free (1,000) and Developer (250,000), and tighter per-minute windows on the query and write lanes. Each response reports where you stand:

X-RateLimit-Limit: 300
X-RateLimit-Remaining: 299
X-RateLimit-Reset: 1751990460
X-Magisterial-Plan: developer
X-Monthly-Quota-Limit: 250000
X-Monthly-Quota-Remaining: 249871

X-RateLimit-Reset is the Unix time (seconds) when the per-minute window resets. Exceeding it returns 429 Too Many Requests with a Retry-After header (seconds to wait) and the standard error body:

{
  "error": {
    "type": "rate_limited",
    "code": "rate_limit_exceeded",
    "message": "Rate limit exceeded (300 per 1 minute). Wait for the window to reset and retry."
  }
}

Once a capped plan’s monthly quota is used, every request returns 429 with monthly_quota_exceeded until the UTC calendar month rolls over. Enterprise has no monthly quota.

{
  "error": {
    "type": "rate_limited",
    "code": "monthly_quota_exceeded",
    "message": "The Free plan includes 1,000 requests per calendar month and this account has used 1,000. ...",
    "plan": "free",
    "quota": 1000,
    "used": 1000,
    "resets_at": "2026-10-01T00:00:00+00:00"
  }
}

Pagination & errors

List endpoints are cursor-paginated. Pass limit (max 100) and follow next_cursor from each response until it is null.

{
  "data": [ /* ... */ ],
  "next_cursor": "eyJvZmZzZXQiOjEwMH0",
  "has_more": true
}

Errors use standard status codes and a consistent body:

{
  "error": {
    "type": "forbidden",
    "code": "tier_required",
    "message": "This division requires a Pro plan or higher for soccer.",
    "required_tier": "pro"
  }
}

Usage-billed endpoints (portal, query, alerts, exports) add billing errors: 403 api_billing_not_enabled until billing is enabled in the console, 403 api_billing_past_due after a failed invoice, and 402 budget_exceeded once month-to-date spend reaches your budget cap (the response includes budget_usd and spent_usd). Requests made with a test key return 403 test_key_paid_endpoint on those endpoints.

Versioning & deprecation

The API is versioned by URL prefix; /v1 is the current and only version. Additive changes — new endpoints, new response fields, new optional parameters, new enum values — ship without notice, so write clients that tolerate unknown fields. Breaking changes get at least 90 days’ notice on the changelog and by email, and affected responses carry Deprecation and Sunset headers you can alert on.

Read the full policy and release history on the changelog.

API reference & spec

The complete endpoint-by-endpoint contract lives in the interactive API reference — every endpoint, parameter, response schema, and error shape, with runnable requests and generated code samples in cURL, Python, JavaScript, and more. It is rendered from the API’s published OpenAPI 3.1 document, so it can never drift from what the API actually does.

The spec itself is public — no authentication required:

curl https://api.magisterial.ai/v1/openapi.json

Import it into Postman, generate a typed client with your SDK generator of choice, or hand it to an LLM agent as the contract for this API. For agents there is also a single-file markdown rendering of the whole contract — auth, conventions, every endpoint and object shape — sized to fit in a model’s context:

curl https://api.magisterial.ai/v1/llms.txt

Official SDKs

Official client libraries for Python and TypeScript/JavaScript. Both are fully typed — every model is generated from the published OpenAPI spec, so the SDKs cannot drift from the live API — and both handle the plumbing for you: automatic cursor pagination, retries, and a one-call submit-and-poll helper for natural-language queries.

Python
pip install magisterial
GitHub · PyPI · Python 3.10+, sync & async
TypeScript / JavaScript
npm install magisterial
GitHub · npm · Node 18+, zero dependencies

Full SDK guide — quickstart, pagination, errors, and method reference →

Players

Players are the heart of the dataset: profiles across seven sports and every division — NCAA D1 through D3, NAIA, and NJCAA — each carrying identity, position, class year, hometown, season-by-season statistics, and accolades. Where an athlete has claimed their profile, it is marked verified.

Search is built for evaluation workflows: filter by scope, team, conference, position, or class year, and rank by any stat the sport carries. It is the foundation for scouting tools, roster intelligence, and player-facing products alike.

Endpoints and schemas in the API reference →

Persons & careers

A player record belongs to one program; athletes don’t. Persons are the durable identity behind the player records — when an athlete transfers, redshirts, or resurfaces two divisions away, their stints are tied together into a single career you can follow from first season to last, across schools and across divisions.

Transfer history is part of that record: every school change, where it came from and where it landed. That is what makes longitudinal work possible — recruiting diligence on where a prospect has been, how production traveled with them, and what a roster’s movement says about the program.

Endpoints and schemas in the API reference →

Teams & coaches

Teams are the program-level view: every program in scope, with its conference, division, current roster, and season-by-season records. Where players answer “who should I look at,” teams answer “what am I up against” — opponent scouting, conference dashboards, and program tracking all start here.

Coaching staffs complete the picture: who runs a program, what their role is, and — through career records that span schools — where they coached before. Programs are ultimately run by people, and coaching changes ripple into recruiting, transfers, and style of play.

Endpoints and schemas in the API reference →

Schools

A school is the institution behind its teams — one row per college, cross-division and cross-sport, carrying the federal IPEDS UNITID as a stable external join key. Look a school up by ipeds_unitid to match it against your own institution records, then list every program it fields, or filter GET /v1/teams straight to that school’s programs with the same parameter.

Every team also carries its own school object inline, so most integrations never need a separate call — the dedicated Schools endpoints exist for identity lookups and for listing a school’s full athletics department at once.

Endpoints and schemas in the API reference →

Movements

Rosters and coaching staffs change constantly, and most of that change is signal: a transfer, a coaching hire, a title change. Movements turns those diffs into a feed — newest first, cross-division, with a display snapshot of who moved and where.

The default feed is curated: an event appears once our pipeline confirms it (a portal-corroborated transfer, a verified head-coach change) or an editor publishes it. Enterprise keys can read earlier in the same pipeline with status=resolved or status=observed, trading editorial confirmation for same-day signal.

Endpoints and schemas in the API reference →

Games

Games are the ground truth beneath the season aggregates: schedules and results, box scores, and — where our coverage includes it — full play-by-play. Season stat lines tell you what happened on average; games tell you what happened on Tuesday night against the conference leader.

That granularity is what makes deeper work possible: verifying a stat line against the games behind it, computing your own metrics from raw events, or building anything that lives at the level of a single match — recaps, win probability, momentum, matchup analysis.

Endpoints and schemas in the API reference →

Transfer portal

The transfer portal is where college rosters are made and unmade, and speed matters: by the time a name circulates, the conversation has often already started. The API serves the live NCAA feed — who entered, from which program, with what status — continuously refreshed and filterable by your sport and division scope.

It is designed for incremental workflows: poll for what is new since your last check rather than re-reading the world. Open on the Developer plan to verified coaches; no coaching plan is needed. Keys with the portal.contact.read scope also receive contact information. Usage-billed per request.

Endpoints and schemas in the API reference →

Webhook alerts

Alerts turn the portal from something you check into something that reaches out to you. Declare what you are watching for once — a sport, a division, a position, any filter the catalog supports — and the alert is evaluated against the live feed as it refreshes, collecting the entries that match.

Matches can be polled from the API or delivered to your webhook endpoint, so the moment a left-footed center-back enters the portal, your Slack channel, CRM, or recruiting board already knows. Standing alerts are the automation layer of the portal — billed per active alert per month, not per check. See webhook delivery below for event shapes, signatures, and retries.

Endpoints and schemas in the API reference →

Webhook delivery

Webhooks push events to your server the moment they happen — no polling. Register endpoint URLs in the console, subscribe each one to the event types you care about, and verify every delivery with its signing secret. Delivery logs, test pings, and manual redelivery live in the console too.

EventFires when
alert.triggeredA watch alert found new portal matches (one event per alert per evaluation cycle; matches are batched).
query.completedAn async /v1/query run reached done, error, or cancelled.
pingYou pressed “Send test” in the console.

Want a raw portal firehose? Create an alert with broad filters (a whole division, no position filter) and subscribe an endpoint to alert.triggered — every new portal entry in that scope arrives as a match event.

Delivery payload

Every delivery is an HTTP POST with a JSON body. Respond with any 2xx within 10 seconds; anything else is retried with backoff (30s, 2m, 10m, 30m, 2h, 6h, 12h, 24h — 8 attempts over roughly two days). The Magisterial-Webhook-Id header carries the event id — treat it as your idempotency key, since retries and manual redeliveries reuse it. An endpoint that exhausts retries 10 times in a row is auto-disabled until you re-enable it in the console.

{
  "id": "0d4f0c9e-1b2a-4c3d-8e5f-6a7b8c9d0e1f",
  "type": "alert.triggered",
  "created_at": "2026-07-21T18:00:12+00:00",
  "data": {
    "alert_id": "a3e8c2d1-77b4-4f0e-8d21-0b9c8e7f6a5d",
    "alert_name": "D1 women's soccer goalkeepers",
    "match_count": 1,
    "matches": [
      {
        "match_id": 99012,
        "matched_at": "2026-07-21T18:00:11+00:00",
        "player": {
          "name": "Nakamura, Yui",
          "school": "Santa Clara University",
          "division": "D1",
          "sport_path": "womens-soccer"
        }
      }
    ]
  }
}

Verifying signatures

Each delivery carries a Magisterial-Signature header of the form t=<unix-ts>,v1=<hex>, where v1 is the HMAC-SHA256 of {t}.{raw_body} keyed with the endpoint’s whsec_ secret. Verify against the raw bytes (before any JSON parsing), compare with a constant-time function, and reject timestamps older than a few minutes to block replays.

import hashlib, hmac, time

def verify(secret: str, signature_header: str, body: bytes) -> bool:
    parts = dict(p.split("=", 1) for p in signature_header.split(","))
    timestamp, received = parts["t"], parts["v1"]
    if abs(time.time() - int(timestamp)) > 300:  # 5-minute tolerance
        return False
    expected = hmac.new(
        secret.encode(), f"{timestamp}.".encode() + body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, received)

# In your handler (FastAPI shown; use the RAW request body):
#   sig = request.headers["Magisterial-Signature"]
#   assert verify(WEBHOOK_SECRET, sig, await request.body())
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(secret, signatureHeader, rawBody) {
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((p) => p.split("=", 2)),
  );
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
  const expected = createHmac("sha256", secret)
    .update(`${parts.t}.`)
    .update(rawBody) // Buffer of the raw request body
    .digest();
  const received = Buffer.from(parts.v1, "hex");
  return expected.length === received.length && timingSafeEqual(expected, received);
}

Natural-language query

Sometimes the question doesn’t map cleanly onto filters: “Who led the NESCAC in assists this season?” “Which D2 programs lost their starting goalkeeper to the portal?” The query endpoint takes the question in plain English and puts an agent to work on it — researching across players, teams, games, and the portal within your scope, and coming back with an answer.

Runs are asynchronous: submit, get a run id, poll until it completes (typically under a minute). It is the fastest way to answer exploratory questions without writing search logic, and the building block for conversational features in your own product. Usage-billed by what the run consumes.

Endpoints and schemas in the API reference →

Bulk exports

When you need the dataset rather than a page of it — loading a warehouse, training a model, running offline analysis — walking a paginated endpoint cursor by cursor is the wrong tool. Exports are the bulk lane: one asynchronous job writes an entire dataset in your scope — players, teams, games, or coaches — to a compressed CSV or JSONL file.

Start the job, poll it, and collect a signed download URL when it finishes. Billed by result size on success only — a failed export never costs anything.

Endpoints and schemas in the API reference →

Managed athletes

Coach contact fields are a per-athlete outreach permission, not a dataset — GET /v1/teams/{team_id}/coaches only returns email and phone when the request is backed by a verified player claim. Managed athletes is how a platform that represents many athletes gets that backing at scale, on the Enterprise plan.

1Invite by player id

POST /v1/athletes with the athlete's player_id. The invitation email goes to an address we can vouch for — their on-file contact, their school .edu address, or the account that already claimed the profile.

2The athlete accepts

They accept on magisterial.ai, which also completes their verified claim if it wasn't already. The grant moves from invited to active.

3Call the coaches endpoint on their behalf

Any key on your account with outreach.coach.read passes on_behalf_of=<player_id> to GET /v1/teams/{team_id}/coaches. Coach email and phone come back evaluated against that athlete's verified claim and recorded in the athlete's access log. Not metered.

4Athletes see and can withdraw access

The athlete sees every delegated read in their own settings (the same log GET /v1/athletes/{grant_id}/access returns) and can revoke the authorization at any time; a revoked or unclaimed grant denies on the next call.

Seats default to 250 managed athletes per account (Enterprise agreements can raise the ceiling); each grant, resend, and revoke goes through the managed_athlete.manage key scope. Every delegated read is logged to the athlete’s access log whether or not it returned contacts, so both sides have the same record.

Coach directory on custom plans. A custom Enterprise plan can include the coach directory outright. Your plan then carries coach_contact_directory: true, and a key with the exact coach.contact.directory_read scope reads coach email and phone on GET /v1/teams/{team_id}/coaches without on_behalf_of, like any other read and not metered. A coach's own contact opt-out is still honored, and contact fields never appear in bulk exports. What a custom plan includes, and its price, is set per customer.

Endpoints and schemas in the API reference →

This is a preview of the v1 surface. Endpoints and shapes may change before general availability — breaking changes follow the deprecation policy. See plans & pricing for limits, quotas, and usage prices.