Conventions: envelope, categories, paging and errors
The rules every MotoGP API endpoint follows — response envelope, Moto2/Moto3 categories, pagination, identifiers, error bodies and caching.
6 min read
Learn these rules once and every endpoint behaves the way you expect.
The response envelope
Every successful response is a JSON object with two keys:
| Key | Contents |
|---|---|
data | An object (single resource) or an array (list) |
meta | total, page, size, total_pages — paging fields are null when the endpoint is not paged |
{
"data": { "year": 2026, "status": "ACTIVE" },
"meta": { "total": 1, "page": null, "size": null, "total_pages": null }
}
Field names are snake_case throughout (rider_number, total_points, season_year).
Categories: MotoGP, Moto2 and Moto3
Season-scoped endpoints accept a category query parameter with one of MOTOGP, MOTO2 or
MOTO3. class is an alias with the same values — send one or the other, not both.
GET /standings/seasons/2026/riders?category=MOTO2
GET /races/seasons/2026?category=MOTO3
Pass category explicitly so the result never depends on a default.
Paging
The “get all” endpoints — /riders, /teams, /races and /standings — are paged with page
(from 1) and size (1–200, default 20):
GET /riders?page=2&size=50
"meta": { "total": 118, "page": 2, "size": 50, "total_pages": 3 }
Loop until page reaches total_pages:
async function getAll(path) {
const rows = [];
for (let page = 1; ; page++) {
const { data, meta } = await getEnvelope(`${path}?page=${page}&size=200`);
rows.push(...data);
if (!meta.total_pages || page >= meta.total_pages) return rows;
}
}
Season-scoped lists such as /riders/seasons/{year} return the whole season in one response.
Ordering
Standings come back ordered by position. Calendars and race results are not guaranteed to be
ordered — sort them yourself:
races.sort((a, b) => a.round_number - b.round_number);
results.sort((a, b) => a.position - b.position);
Identifiers
| Resource | Look it up by |
|---|---|
| Rider | Race number: /riders/{rider_number} (e.g. 93) |
| Team | Opaque ID from any team list: /teams/{id}, or by name: /teams/name/{name} |
| Race | Season + round: /races/seasons/{year}/round/{round}, or opaque ID: /races/{race_id}/results |
| Season | Year: /seasons/{year} |
Take opaque IDs ("82MQRdM5q7ub4OSm6Jhx") from a list response instead of hard-coding them, and
match teams by ID rather than by name — sponsor names change from season to season.
Standing types
Besides the dedicated routes (/riders, /teams, /manufacturers, /bmw-m), you can request a
standing table by type:
GET /standings/seasons/2026/type/MANUFACTURER?category=MOTOGP
Types are RIDER, TEAM, MANUFACTURER and BMW_M.
Errors
Errors use standard HTTP status codes and a JSON body:
{
"timestamp": "2026-01-27T19:18:29",
"status": 404,
"error": "Not Found",
"message": "Rider not found with number: 99",
"path": "/riders/99"
}
| Status | Meaning | What to do |
|---|---|---|
| 400 | Invalid parameter (bad year, unknown category) | Fix the request; validation_errors lists the problems when present |
| 401 / 403 | Missing or invalid RapidAPI key, or no subscription | Check both headers and your plan |
| 404 | The rider, team, race or season doesn’t exist | Show a “not found” state |
| 429 | Rate limit or quota reached on your plan | Back off and retry later, or upgrade the plan |
| 5xx | Temporary server problem | Retry with exponential backoff; check the status page |
Caching
Results and standings change after each race weekend, and profiles change between seasons. Cache aggressively to save quota and keep pages fast:
| Data | Suggested cache time |
|---|---|
| Seasons, riders, teams, career stats | 24 hours |
| Calendar | 12 hours |
| Results and standings | 1 hour on race weekends, 12 hours otherwise |
Keep your key on the server
Your RapidAPI key is a billing credential. Call the API from your backend, a serverless function or a build step — never from browser or mobile code that ships the key to users. The standings table guide shows a small proxy with caching.