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.
Open the console, create a key, and copy it — the full token is shown only once.
curl https://api.magisterial.ai/v1/sports \ -H "Authorization: Bearer mag_live_xxxxxxxxxxxxxxxxxxxx"
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.
"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:
| Data | Inside its churn window | Otherwise |
|---|---|---|
| Rosters | every 24 hours | every 7 days |
| Coaching staffs | every 7 days (Mar 1 to Jun 30) | every 14 days |
| Season statistics | every 7 days in season | every 14 days |
| Schedules and box scores | every 24 hours, sport lanes every 4 to 6 hours in season | daily |
| Movements | observed at scrape time, identity resolution every 6 hours | editorial 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.
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
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
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
| Limit | Free | Developer | Enterprise |
|---|---|---|---|
| Read requests per minute | 10 | 300 | 1,000 (custom) |
| Requests per calendar month | 1,000 | 250,000 | Unlimited |
| Usage-billed endpoints | Not available | Available | Available |
| Query runs per minute / in flight | — | 10 / 3 | 30 / 10 |
| Alert and export writes per minute | — | 30 | 60 |
| Transfer-portal alerts | — | 25 | 250 |
| Export rows per job / jobs in flight | — | 500,000 / 2 | 5,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.
| Endpoint | Price | Notes |
|---|---|---|
| GET /v1/portal | $0.05 per request | Also requires a verified Max coach on the account and the portal scope on the key. |
| POST /v1/query | Model cost × 3 | Billed by token usage once the run completes; the model is fixed server-side. |
| POST /v1/alerts | $5 per alert per month | Per active transfer-portal alert; matches push to your webhook. |
| POST /v1/exports | $0.10 per 10,000 rows | Per 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
| Event | Fires when | Data |
|---|---|---|
alert.triggered | A watch alert found new portal matches (one event per alert per evaluation cycle; matches are batched). | alert_id, alert_name, match_count, matches[] with per-player details |
query.completed | An async /v1/query run reached done, error, or cancelled. | run_id, status, error, created_at, finished_at — fetch the answer via GET /v1/query/{run_id} |
ping | You pressed “Send test” in the console. | message |
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.
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.
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.
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.
They accept on magisterial.ai, which also completes their verified claim if it wasn't already. The grant moves from invited to active.
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.
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.