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.

Filtering

Every list endpoint filters the same way. The reference lists the fields each endpoint filters on; the forms are always these, and filters combine with AND:

FormMeaning
field=valueExact match. Text compares case-insensitively.
field=a,b,cAny of the values.
field_not=a,bNone of the values. A row with no value for the field is left out too.
field_min, field_maxInclusive range, for numbers and dates.
q=textName contains, case-insensitive.
sort_by, sort_orderWhere a list has more than one useful order.
# One team's finished conference games in 2025, biggest crowds first
curl "https://api.magisterial.ai/v1/games?sport=soccer&division=D3&gender=men\
&team_id=344&season=2025&conference_game=true&status=final&sort_by=attendance" \
  -H "Authorization: Bearer mag_live_xxxxxxxxxxxxxxxxxxxx"

# Every movement at one school, across all of its sports
curl "https://api.magisterial.ai/v1/movements?school_id=14" \
  -H "Authorization: Bearer mag_live_xxxxxxxxxxxxxxxxxxxx"

POST /v1/players/search takes the same names in its JSON body, with arrays in place of comma lists, and every season stat as a range under stats, keyed by the stat fields GET /v1/filters/catalog lists for the sport:

{
  "sport": "soccer", "division": "D3", "gender": "men",
  "conference": ["UAA", "NESCAC"],
  "position": "Forward",
  "class_year_not": "Senior / Graduate",
  "height_inches_min": 70,
  "stats": { "g": { "min": 5 }, "gp": { "min": 10 } },
  "sort_by": "g"
}
Nothing is ignored. A parameter the endpoint does not declare returns 400 unknown_parameter with the list it accepts, so a mistyped filter never comes back as an unfiltered page. Schools publish position, class year and home state in their own words (“F”, “Fwd”, “Forward”); a filter on one of them matches every spelling of the canonical value you give. A conference is named by its full name or its abbreviation.

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, within_miles is the field as an object with latitude, longitude and miles (near, the hometown radius). alert_ops is the op values an alert predicate on the field accepts. Until 2027-01-12 the catalog also returns the keys it had before, such as request_key; they name parameters of the Magisterial app’s own search, not of /v1. The changelog entry lists them.

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. Plus, Premium, Developer Pro and Developer Ultra are bought online and scale from a side project to a product in production; each also opens the Magisterial app, drawing on the same limits. 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
Plus
$19.99 / month
Billed monthly, cancel anytime

Side projects and first prototypes

  • •Everything in Free at 20 requests per minute, 25,000 requests per month
  • •Chat in the Magisterial app on this plan's limits
  • •$5 of natural-language query and app chat usage included each month, usage-billed past it
  • •Transfer portal and alerts are usage-billed ($0.05 per portal request, $5 per alert per month); both need a verified coach
  • •Up to 3 transfer-portal alerts and bulk exports up to 50,000 rows per job
Premium
$49.99 / month
Billed monthly, cancel anytime

Apps with their first real users

  • •Everything in Plus at 100 requests per minute, 100,000 requests per month
  • •Chat in the Magisterial app on this plan's limits
  • •$15 of natural-language query and app chat usage included each month, usage-billed past it
  • •Transfer portal and alerts are usage-billed ($0.05 per portal request, $5 per alert per month); both need a verified coach
  • •Up to 10 transfer-portal alerts and bulk exports up to 250,000 rows per job
Developer Pro
$99.99 / month
Billed monthly, cancel anytime

Ambitious personal projects and indie products

  • •Everything in Premium at 300 requests per minute, 250,000 requests per month
  • •Chat in the Magisterial app on this plan's limits
  • •$40 of natural-language query and app chat usage included each month, usage-billed past it
  • •Transfer portal requests and alerts included, no usage fee; both need a verified coach
  • •Up to 25 transfer-portal alerts and bulk exports up to 500,000 rows per job
  • •Coach directory: coach email and phone on every coaching staff
  • •Coach and player movement webhooks: every published movement pushed to your endpoint, no coach verification needed
  • •A team of up to 5 people on one account, with admin and member roles
Developer Ultra
$499.00 / month
Billed monthly, cancel anytime

Products in production

  • •Ten times Developer Pro at 3,000 requests per minute, 2,500,000 requests per month
  • •Chat in the Magisterial app on this plan's limits
  • •$250 of natural-language query and app chat usage included each month, usage-billed past it
  • •Transfer portal requests and alerts included, no usage fee; both need a verified coach
  • •Up to 250 transfer-portal alerts and bulk exports up to 5,000,000 rows per job
  • •Coach directory: coach email and phone on every coaching staff
  • •Coach and player movement webhooks: every published movement pushed to your endpoint, no coach verification needed
  • •A team of up to 50 people on one account, with admin and member roles
Enterprise
Contact sales
Per-customer agreement

Companies integrating the data into their own product

  • •6,000 requests per minute by default, custom rate limits, and no monthly request cap
  • •Custom export ceilings and alert counts
  • •Coach and player movement webhooks: every published movement pushed to your endpoint, no coach verification needed
  • •A team of up to 100 people by default, with admin and member roles
  • •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: coach email and phone on every coaching staff
  • •Direct support channel with a named contact
  • •Invoiced usage billing on request
LimitFreePlusPremiumDeveloper ProDeveloper UltraEnterprise
Read requests per minute10201003003,0006,000 (custom)
Requests per calendar month1,00025,000100,000250,0002,500,000Unlimited
Usage-billed endpointsNot availableAvailableAvailableAvailableAvailableAvailable
Portal requests and alerts—Usage-billedUsage-billedIncludedIncludedIncluded
Query and app chat usage included per month—$5$15$40$250$500 (custom)
Query runs per minute / in flight—2 / 15 / 210 / 3100 / 30200 / 60
Alert and export writes per minute—101530300600
Transfer-portal alerts—31025250500
Export rows per job / jobs in flight—50,000 / 1250,000 / 1500,000 / 25,000,000 / 2010,000,000 / 40

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 request on Plus and PremiumIncluded from Developer Pro up. Needs a verified coach on the account and the portal scope on the key.
POST /v1/queryModel cost × 3Drawn first from the plan's included monthly usage, which chat in the app shares; billed past it once the run completes.
POST /v1/alerts$5 per alert per month on Plus and PremiumIncluded from Developer Pro up, to the plan's alert count; matches push to your webhook. Needs a verified coach on the account and the portal alert scope on the key.
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-Monthly-Quota-Reset: 1754006400

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, and the 429 carries the same X-Monthly-Quota-* headers. X-Monthly-Quota-Reset is the Unix time (seconds) of the next reset, 00:00 UTC on the 1st of the month, not your billing date. 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.

A request that fails validation returns 422 validation_error in the same body, with error.fields naming each field and what was wrong with it.

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 the API returns.

{
  "id": 184223,
  "name": "Jordan Rivera",
  "team_id": 1873,
  "sport": "soccer",
  "gender": "men",
  "home_state": "OR",
  "jersey_number": "8",
  "season": "2026",
  "stats": {
    "gp": 18, "gs": 17, "g": 7, "a": 5, "pts": 19,
    "sh": 41, "sog": 22, "sh_pct": 0.171, "sog_pct": 0.537,
    "yc": 3, "rc": 0, "pk_made": 1, "pk_att": 2,
    "sv": null, "ga": null, "gaa": null
    /* every stat field of the sport, null when the source did not publish it */
  }
}

Every stat field of the sport is present on every row, so a missing stat is null, never a missing key. A player row carries its profile at the top level under the names the filters use. GET /v1/players/{player_id} returns stats_by_season, one entry per season with the same stats object, and stats_totals. Track and field has no season stats: a search row reports the athlete’s best mark in track, and a player’s track_bests lists the best per event.

Until 2027-01-12 the player endpoints also return the keys they returned before 2026-10-09, beside the new ones, and send Deprecation and Sunset headers. The changelog entry lists them.

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

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

For Postman there is a ready-made collection of every endpoint, generated from the same spec. In Postman choose Import and paste this URL, then set the collection variable api_key once; a pre-request script sends it as the bearer token on every request. Each request lists its optional filters switched off, and list requests remember the ids they return so the detail requests beside them run without copying anything.

https://api.magisterial.ai/v1/postman.json

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 eight 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 carries what is confirmed: a transfer once it is resolved to the transfer it made (the arrival at the new school and the departure from the old one), a coach's move once it is 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 earlier in the same pipeline with status=resolved or status=observed, trading confirmation for same-day signal.

An entry names the school where the change was seen. When resolution found the other side of the move, it also carries a counterpart: the school a new arrival came from, or the school a departure went to.

From Developer Pro up you do not have to poll. Subscribe a webhook endpoint to the coach or player movement events and every published movement, correction and retraction is pushed as it happens. See movement events.

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 every paid plan to verified coaches; no coaching plan is needed. Keys with the portal.contact.read scope also receive contact information. Usage-billed per request on Plus and Premium, included from Developer Pro up.

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.

Alerts are portal data, so they follow the portal’s access rule: open on every paid plan to verified coaches, with no coaching plan needed. Movement events are a separate feed and need no coach verification.

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). Needs a verified coach, like the portal feed.
query.completedAn async /v1/query run reached done, error, or cancelled.
coach_movement.publishedplayer_movement.publishedA movement entered the published feed of GET /v1/movements. Developer Pro and up.
coach_movement.updatedplayer_movement.updatedA published movement changed: a linked person, a resolved origin school, a corrected title. Developer Pro and up.
coach_movement.retractedplayer_movement.retractedA published movement was withdrawn. Developer Pro and up.
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.

Movement events

From Developer Pro up, the published movement feed can be pushed to you. Subscribe an endpoint to the coach events, the player events, or both, and optionally narrow it to the sports and divisions you follow. Movement events need no coach verification.

Each event carries one movement, as the object GET /v1/movements returns, and a revision that rises by one with every event for that movement. A retried delivery can arrive after a newer one, so keep the highest revision you have applied for each kind and id and ignore anything at or below it. Applied that way, the events leave you holding exactly what the published feed holds.

{
  "id": "7b0c0f4e-52b1-5d0a-9a41-0f6f1d2c3b4a",
  "type": "coach_movement.published",
  "created_at": "2026-10-08T14:02:11+00:00",
  "data": {
    "revision": 1,
    "movement": {
      "id": 48211,
      "kind": "coach",
      "event_type": "coach_added",
      "sport_path": "womens-soccer",
      "division": "D1",
      "season": "2027",
      "team_id": 1234,
      "school_id": 567,
      "school_name": "Example State University",
      "subject": { "name": "Jordan Example", "title": "Head Coach", "is_head": true },
      "person_id": 9911,
      "counterpart": { "team_id": 987, "school_id": 654, "school_name": "Sample College", "division": "D2" },
      "resolution_kind": "head_coach_change_confirmed",
      "status": "published",
      "published_at": "2026-10-08T12:00:31+00:00"
    }
  }
}
  • updated lists the fields that changed in data.changed. Replace your copy with the movement it carries.
  • retracted carries the movement as you last received it and a reason: unpublished, dismissed or deleted. Remove it from your copy.
  • Events begin when you subscribe and do not replay history. To start, page through GET /v1/movements once, then follow the webhook. To poll instead, GET /v1/movements/changes serves the same corrections and retractions on every plan.
  • A coach who leads both the men’s and women’s program at one school produces one event per program. Group them by person_id and school_id.

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.

Portal questions follow the portal’s access rule: the answer draws on the live feed when the key carries the portal scope and the account has a verified coach, with no coaching plan needed. Without them the run has no portal data to draw on.

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. Developer Pro, Developer Ultra and Enterprise include the coach directory. 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 who asked not to be contacted is listed with contact_opt_out true and no contact fields, and contact fields never appear in bulk exports.

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.