# Magisterial API documentation

Programmatic access to Magisterial's college sports data — players, season
stats, teams, schools, rosters, coaching staffs, games, cross-program careers
and transfers, roster/coaching-staff movements, the live NCAA transfer
portal, natural-language query, and bulk exports. Eight sports (soccer,
basketball, baseball, softball, volleyball, football, lacrosse, track and field)
across NCAA D1/D2/D3 (incl. FBS/FCS), NAIA, NJCAA.

## The canonical references

- Complete API reference as one markdown document (recommended for agents —
  auth, scoping, pagination, errors, every endpoint and object shape):
  https://api.magisterial.ai/v1/llms.txt
- OpenAPI 3.1 spec: https://api.magisterial.ai/v1/openapi.json
- Postman collection (every endpoint, generated from the spec; set the
  api_key collection variable once): https://api.magisterial.ai/v1/postman.json
- Interactive human reference: https://api.magisterial.ai/v1/docs
  (mirrored at https://magisterial.ai/docs with sign-in-aware, runnable examples)

## Quickstart

No key needed for the coverage matrix:

    curl https://api.magisterial.ai/v1/coverage

Everything else takes a bearer key (self-serve at https://magisterial.ai/console):

    curl https://api.magisterial.ai/v1/teams?sport=soccer&division=D3&gender=men \
      -H "Authorization: Bearer mag_live_..."

- Six plans, inherited by every key from its owner's account:
  Free ($0, every account): reads at 10 requests/minute, 1,000 requests per
  month, no usage-billed endpoints. Plus ($19.99/month): 20 requests/minute,
  25,000 per month. Premium ($49.99/month): 100/minute, 100,000 per month.
  Developer Pro ($99.99/month): 300/minute, 250,000 per month. Developer
  Ultra ($499/month): 3,000/minute, 2,500,000 per month. Every paid plan
  opens the usage-billed endpoints and chat in the Magisterial app.
  Enterprise (contact sales): custom limits, no monthly cap. Plans are
  bought on their own, separate from the coaching subscriptions.
  Details: https://magisterial.ai/pricing/developers.md
- The free read endpoints (players, teams, games, coaches, careers,
  reference data, coverage) are on every plan at the plan's limits.
- Test keys (mag_test_) hit the free read endpoints for real and are blocked
  from usage-billed endpoints, so development can never create charges.
- Usage-billed (every paid plan): /v1/query (model cost x 3, after the
  plan's included monthly usage) and /v1/exports ($0.10 per started
  10,000-row block). On Plus and Premium, /v1/portal is $0.05/request and
  /v1/alerts is $5/alert/month; from Developer Pro up both are included.
  Both need an active verified coach on the account and the matching scope
  on the key; no coaching plan is needed. Portal questions asked through
  /v1/query follow the same rule.
  All under a monthly budget cap you control. A Free key receives 403 plan_required on these.
- Every response carries X-Magisterial-Plan; capped plans add
  X-Monthly-Quota-Limit / X-Monthly-Quota-Remaining / X-Monthly-Quota-Reset
  (Unix seconds of the next reset: 00:00 UTC on the 1st of the month, not
  your billing date) and return 429 monthly_quota_exceeded, with the same
  headers, once the calendar month's requests are used.

## Schools and freshness

- Every team carries a school object (id, slug, athletics_domain,
  ipeds_unitid — the federal IPEDS UNITID, null for about 13% of schools,
  mostly NAIA, junior colleges, and non-US institutions). GET /v1/schools and
  GET /v1/schools/{school_id} look institutions up directly and list every
  program a school fields, so you can join on ipeds_unitid first and fall
  back to name/state/conference only for the remainder.
- Team, roster, and coaching-staff responses carry a freshness object
  (observed_at, checked_at, changed_at, next_check_at, cadence_hours) and
  GET /v1/coverage adds freshness.lanes (last run per ingest lane) and
  freshness.cadence_policy (the seasonal refresh policy). No licensed feed
  backs this data — it is compiled from each program's public athletics
  site, stats.ncaa.org, TFRRS, NCAA transfer-portal listings, federal IPEDS
  / College Scorecard data, and the coaches and athletes who claim and
  correct their own profiles.

## Short seasons

- The record and stats on a team's season (documents on GET /v1/teams/{team_id})
  are counted from the games we hold, and a past season can hold fewer games
  than the team played. Every season carries a schedule object: games_played
  (completed, non-exhibition games on the team's own published schedule, as
  last read) beside games_held (final, non-exhibition games we hold).
- schedule.status is complete (games_held equals games_played, and
  official_record is the season's record), inconsistent (games_held is above
  games_played: a held game is not one of the season's or the schedule as
  read is wrong; official_record is null), partial (games_held is below
  games_played by missing_games; the record and stats cover only the games
  held, so the season is short) or unverified (we have not read the team's
  schedule for that season; games_played, missing_games and official_record
  are null). record_from_held_games is the record over the games held
  whatever the status.

## Filtering

- Every list endpoint filters the same way: field=value (exact; text is
  case-insensitive), field=a,b,c (any of), field_not=a,b (none of; a row
  with no value for the field is left out too), field_min / field_max
  (inclusive range for numbers and dates), q=text (name contains), and
  sort_by / sort_order where a list has more than one useful order. Filters
  combine with AND. Each endpoint's parameters are listed in llms.txt.
- A parameter the endpoint does not declare returns 400 unknown_parameter
  with the list it accepts; nothing is ignored silently.
- POST /v1/players/search takes the same names in its JSON body (arrays in
  place of comma lists) and every season stat as a range under stats, keyed
  by the sport's stat fields from GET /v1/filters/catalog, e.g.
  {"stats": {"g": {"min": 10}}, "sort_by": "g"}.
- GET /v1/filters/catalog says how each field can be used. search_ops is
  what POST /v1/players/search takes: eq and in are the field as one value or
  an array, neq and not_in are field_not, gte and lte are field_min and
  field_max (for a stat, min and max under stats; filterable says whether a
  stat takes them), within_miles is the field as {latitude, longitude, miles}
  (near, the hometown radius). alert_ops is the op values an alert predicate on the
  field accepts. Deprecated, removed on 2027-01-12: request_key,
  manual_search_operators and the other keys the catalog marks deprecated;
  they name parameters of the Magisterial app's own search, not of /v1.
- Position, class year and home state match every spelling a roster uses
  for the canonical value; a conference is named by its full name or its
  abbreviation ("UAA").
- GET /v1/schools and GET /v1/movements need no scope: school_id alone on
  /v1/movements is one school's movements in every sport.

## Movements

- GET /v1/movements lists roster and coaching-staff movements, newest first,
  across every sport and division unless filtered (sport and gender or
  sport_path, division, team_id, kind). Default status=published (every
  plan) carries what is confirmed: a transfer resolved to the transfer edge
  it made (the arrival at the new school and the departure from the old
  one), a coach's move resolved to the post at another school they held the
  season before, and confirmed head-coach changes. A departure with no known
  destination and a new hire with no known previous post are not published;
  Enterprise keys
  can read them with status=resolved (identity resolved) and status=observed
  (every non-dismissed event, including pending). Each entry carries
  resolution_status, resolved_at, school_id, and ipeds_unitid; since
  compares against published_at on the published tier and observed_at on the
  resolved and observed tiers. To poll for new movements, pass the newest
  published_at you hold as since and drop rows whose kind and id you already
  have. A movement's key is (kind, id): ids are unique within a kind only. A
  confirmed coach title change (e.g. a promotion to head coach) carries
  resolution_status title_change_confirmed; dismissed events are never
  returned. When resolution found the other side of a move, counterpart
  names that school (the origin of an arrival, the destination of a
  departure).
- GET /v1/movements/changes is the change log of the published feed, oldest
  change first, on every plan: each movement that is or was published, in
  its current state (state published or retracted, a per-movement revision,
  changed_at, and for a retraction the reason). A correction does not move
  published_at and a retracted movement leaves GET /v1/movements, so this is
  the endpoint to poll to keep a copy correct: read until has_more is false,
  then pass the last changed_at as since. Apply each entry by
  (movement.kind, movement.id).
- GET /v1/teams/{team_id}/rankings lists a team's national rankings by
  season (RPI, NET, NPI, coaches polls where held): rank on the season's
  latest release, with is_final true once that release is the final one.
- GET /v1/teams/{team_id}/postseason lists what a team won and how far it
  went, by season. kind national_tournament: each national championship
  tournament it played in (organizer NCAA, NAIA, or CFP for the College
  Football Playoff), with result (champion, runner_up, semifinals,
  quarterfinals, round_of_<n>; regional, super_regional, championship_finals
  in baseball and softball, where NAIA softball's opening round is regional
  and its ten-team World Series championship_finals), teams_remaining, seed,
  wins and losses. kind
  bowl_game: an FBS bowl (organizer and event the bowl, result won or lost,
  never champion); a playoff game played as a bowl is both a CFP entry and a
  bowl_game entry, and in coverage the bowls are one line with organizer
  'FBS bowls'. kind
  conference_regular_season and conference_tournament: the conference titles
  it won (result champion; runner_up where the conference names the loser of
  its tournament final), read from the conference's own list of champions,
  with shared true for co-champions and conference_division for a division
  title. coverage lists the competitions and seasons held; inside it a
  season with no entry is one the team did not play in or did not win. A
  conference with no coverage entry is not held.
- From Developer Pro up the published feed can be pushed as webhooks:
  coach_movement.published / .updated / .retracted and the same three for
  player_movement. Each event carries one movement and a per-movement
  revision; apply them in revision order. No coach verification is needed.
- A query parameter an endpoint does not accept is refused with 400
  unknown_parameter (the error lists what the endpoint accepts); a filter is
  never silently ignored.
- A player's name is natural order ("Jordan Rivera") on every record;
  first_name and last_name are set when the roster source published the
  name in split form. Roster and coaching-staff responses carry coverage
  (held, season_not_held, none_listed, not_collected, not_sponsored,
  discontinued), so an empty list says what it means, and every team
  carries status (active or discontinued) and discontinued_at.
- GET /v1/sports lists each sport's genders and the divisions each gender is
  offered in; GET /v1/divisions?sport=... returns one sport's list. NAIA
  football is in scope.
- division is optional on GET /v1/teams/{team_id}, /roster and /coaches:
  omitted, the team is read in its own current division. A team outside a
  division that is given is a 404 team_not_found whose error.division names
  the division it is in now (divisions change when a program moves).

## Coach contacts and managed athletes

- Coach email/phone on GET /v1/teams/{team_id}/coaches are returned when the
  plan includes the coach directory or the request is backed by a verified
  player claim; none of these reads is metered. When they are withheld,
  contacts_unavailable_reason says why (plan_required or key_scope_required).
- Coach directory: Developer Pro, Developer Ultra and Enterprise include it.
  A key with the exact coach.contact.directory_read scope reads coach
  contacts on the coaches endpoint like any other read. A coach who asked
  not to be contacted has contact_opt_out true and no contact fields.
  Contact fields never appear in bulk exports.
- Managed athletes: POST /v1/athletes invites a player by id, the athlete
  accepts on magisterial.ai (completing their verified claim), and any key
  with outreach.coach.read can then pass on_behalf_of=<player_id> to the
  coaches endpoint. Every delegated read is recorded in the athlete's access
  log (GET /v1/athletes/{grant_id}/access) and athletes can withdraw at any
  time. Requires the Enterprise plan and the managed_athlete.manage key
  scope; seats default to 250 per account.

## Player stats

- A stat has one name everywhere. GET /v1/filters/catalog lists each sport's
  stat fields under stats_by_sport, each with a label and a type. Those are
  the names POST /v1/players/search filters and sorts by, and the names in
  every stats object: a search row's stats, each entry of a player's
  stats_by_season, stats_totals, and the stat columns of a players export.
- Every stat field of the sport is present on every row. A stat the source
  did not publish is null, not a missing key.
- A player row carries its profile at the top level under the names the
  filters use (person_id, team_id, sport, gender, height_inches,
  jersey_number, home_city, home_state, home_country, high_school, major,
  minor, bats, throws).
- Track and field has no season stats: a search row reports the athlete's
  best mark in track (best_mark_seconds, best_mark_meters or best_points),
  and a player's track_bests lists the best per event.
- Deprecated, removed on 2027-01-12: the keys the player endpoints returned
  before 2026-10-09 (profile copies and the soccer keys sh%, sog%, sv%,
  yc-rc, pg-pa inside a search row's stats; season_stats on a player and on
  season history; the detail list on a 422). They are still returned beside
  the new fields until then, with Deprecation and Sunset headers.
- A request that fails validation (422) uses the standard error envelope:
  code validation_error, with error.fields naming each field.

## Other surfaces

- SDKs (Python + TypeScript): https://magisterial.ai/sdk.md
- MCP connector for AI assistants: https://magisterial.ai/connectors.md
- Agent guide: https://magisterial.ai/agents.md
- API changelog + deprecation policy (90 days' notice on /v1 breaking
  changes, Deprecation/Sunset headers): https://magisterial.ai/changelog.md
