Mr.Anime API
A free REST API over the same data the site uses — search, trending, seasonal and per-title details. Authenticate with a personal bearer token.
Base URL: https://mr-anime.vercel.app/api/v1Quickstart
- Create a token on your account page (API tokens card). It is shown once — copy it.
- Send it as a bearer token on every request.
curl -H "Authorization: Bearer ma_live_YOUR_TOKEN" \
"https://mr-anime.vercel.app/api/v1/anime/search?q=naruto&perPage=5"// JavaScript
const res = await fetch("https://mr-anime.vercel.app/api/v1/anime/trending?perPage=10", {
headers: { Authorization: `Bearer ${process.env.MRANIME_API_TOKEN}` },
});
const { data, pageInfo } = await res.json();Authentication
Every request needs a token in the Authorization header as Bearer ma_live_… (an X-API-Key header works too). Tokens start with ma_live_, are stored hashed server-side, and only the SHA-256 hash is kept — losing the token means revoking it and creating a new one. Revocation takes effect immediately.
Rate limit: 60 requests per minute per token. Over-limit responses are 429 with a Retry-After header; every success carries X-RateLimit-Remaining. Responses are CDN-cached for 10 minutes to 1 hour, so repeat reads are cheap.
CORS is enabled (Access-Control-Allow-Origin: *), so the API can be called directly from browser apps.
OAuth 2.0 (third-party apps)
Mr.Anime is also an OAuth 2.0 provider — register an app on your account page (OAuth applications card) to get a client_id and a one-time client_secret. Two grant types are supported:
- authorization_code — “Sign in with Mr.Anime”: users approve your app, you receive a single-use 5-minute code, then exchange it for a 30-day access token.
- client_credentials — server-to-server API access with no user (read scope only).
Scopes
read(default) — all/api/v1anime endpoints.profile— additionally/api/v1/me(the user’s id, username, avatar, badges). Always requested alongsidereadby sign-in buttons.
1. Redirect the user
https://mr-anime.vercel.app/oauth/authorize
?client_id=CLIENT_ID
&redirect_uri=YOUR_CALLBACK
&response_type=code
&scope=read%20profile
&state=RANDOM_UNGUESSABLE_STRINGThe user signs in and clicks Approve; we redirect back to redirect_uri with code and state (or error=access_denied if they decline). Verify state matches what you sent.
2. Exchange the code
curl -X POST https://mr-anime.vercel.app/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d grant_type=authorization_code \
-d code=CODE \
-d redirect_uri=YOUR_CALLBACK \
-d client_id=CLIENT_ID \
-d client_secret=CLIENT_SECRET
# → { "access_token": "ma_at_…", "token_type": "Bearer",
# "expires_in": 2592000, "scope": "read profile" }Server-to-server (no user)
curl -X POST https://mr-anime.vercel.app/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d grant_type=client_credentials \
-d client_id=CLIENT_ID \
-d client_secret=CLIENT_SECRET
# → { "access_token": "ma_at_…", "expires_in": 2592000, "scope": "read" }3. Use the access token
curl -H "Authorization: Bearer ma_at_YOUR_TOKEN" \
"https://mr-anime.vercel.app/api/v1/me" # or any anime endpoint
# → { "data": { "id": "…", "username": "…", "avatarUrl": null,
# "role": "user", "isVerified": false, "badges": [] } }Tokens live 30 days and are revocable from your account page (revoking the app kills its tokens instantly). Errors from /oauth/token follow RFC 6749: invalid_client, invalid_grant, unsupported_grant_type, invalid_scope.
Endpoints
| Method | Path (under /api/v1) | Description |
|---|---|---|
| GET | /anime/trending | Currently trending anime, newest first. |
| GET | /anime/popular | Most popular anime of all time. |
| GET | /anime/top | Highest-rated anime. |
| GET | /anime/seasonal | Browse a season (defaults to the current one). |
| GET | /anime/search | Search with filters (q, genre, year, status…). |
| GET | /anime/{id} | Full details for a single anime by AniList id. |
| GET | /anime/{id}/episodes | Episode list with titles and thumbnails. |
| GET | /anime/{id}/episodes/{episode} | Player payload: metadata + episode + embed URLs + dub availability. |
| GET | /me | Identity of the token's user (requires the `profile` scope or a personal token). |
Common query parameters
page— 1…500, default 1.perPage— 1…50, default 24.search:q(max 200 chars),genre,year(1950–2100),season(WINTER|SPRING|SUMMER|FALL),format(TV|TV_SHORT|MOVIE|SPECIAL|OVA|ONA|MUSIC),status(RELEASING|FINISHED|NOT_YET_RELEASED|CANCELLED),sort(SEARCH_MATCH|TRENDING_DESC|POPULARITY_DESC|SCORE_DESC|START_DATE_DESC|FAVOURITES_DESC).seasonal:seasonandyeardefault to the current season and year.episodesendpoints:season(1…9999, default 1 — the site’s?s=) selects the TMDB season used for titles/thumbnails; the player endpoint also takeslang=sub|dub(defaultsub), mirroring the site’s?lang=.
Responses
List endpoints return data (an array of anime) plus pageInfo; the detail endpoint returns a single anime under data. The anime shape is identical everywhere:
| Field | Type | Notes |
|---|---|---|
| id | number | AniList id — use it on `/anime/{id}` and the site (`/anime/{id}`). |
| title | object | `romaji`, `english`, `native` strings (nullable). |
| format | string | `TV`, `MOVIE`, `OVA`, `ONA`, `SPECIAL`… (nullable). |
| status | string | `RELEASING`, `FINISHED`, `NOT_YET_RELEASED`, `CANCELLED`, `HIATUS` (nullable). |
| episodes | number | null | Total episode count. |
| duration | number | null | Minutes per episode. |
| season / seasonYear | string / number | `WINTER`, `SPRING`, `SUMMER`, `FALL` + year. |
| startDate / endDate | object | `{ year, month, day }`, nullable parts. |
| score / meanScore | number | null | Score as 0–100. |
| popularity / favourites | number | null | AniList popularity counts. |
| genres | string[] | Genre names. |
| description | string | null | Plain-text synopsis (HTML removed). |
| cover | object | null | `{ extraLarge, large, color }` image URLs. |
| banner | string | null | Banner image URL. |
| studios | string[] | Studio names. |
| nextAiringEpisode | object | null | `{ episode, airingAt }` — `airingAt` is a Unix timestamp. |
| isAdult | boolean | Adult flag. |
GET /api/v1/anime/16498
{
"data": {
"id": 16498,
"type": "ANIME",
"title": { "romaji": "Shingeki no Kyojin", "english": "Attack on Titan", "native": "進撃の巨人" },
"format": "TV",
"status": "FINISHED",
"episodes": 25,
"duration": 24,
"season": "SPRING",
"seasonYear": 2013,
"startDate": { "year": 2013, "month": 4, "day": 7 },
"endDate": { "year": 2013, "month": 9, "day": 29 },
"score": 84,
"meanScore": 84,
"popularity": 16800000,
"favourites": 320000,
"genres": ["Action", "Drama", "Fantasy"],
"description": "Centuries ago, mankind was slaughtered to near extinction…",
"cover": { "extraLarge": "https://s4.anilist.co/…", "large": "https://s4.anilist.co/…", "color": "#e4784c" },
"banner": "https://s4.anilist.co/…",
"studios": ["Wit Studio"],
"nextAiringEpisode": null,
"isAdult": false
}
}GET /api/v1/anime/trending?perPage=2
{
"data": [ { "id": 16498, "type": "ANIME", "title": { … }, … } ],
"pageInfo": { "currentPage": 1, "hasNextPage": true, "total": 500 }
}/anime/{id}/episodes mirrors the site’s episode grid — titles and thumbnails come from TMDB first, then AniList’s streaming list:
GET /api/v1/anime/16498/episodes
{
"data": [
{ "number": 1, "title": "To You, in 2000 Years", "thumbnail": "https://image.tmdb.org/t/p/w780/…" },
{ "number": 2, "title": "That Day", "thumbnail": "https://image.tmdb.org/t/p/w780/…" }
],
"meta": { "episodes": 25, "season": 1 }
}/anime/{id}/episodes/{episode} mirrors the watch page: full metadata, resolved episode info and one ready-to-embed url per provider (in switcher order), with per-provider dub availability probed the same way the site does:
GET /api/v1/anime/16498/episodes/1?lang=dub
{
"data": {
"anime": { … the same object as GET /anime/{id} … },
"episode": { "number": 1, "title": "To You, in 2000 Years", "thumbnail": "https://image.tmdb.org/t/p/w780/…" },
"season": 1,
"requestedLang": "dub",
"effectiveLang": "sub",
"dubAvailable": false,
"dubMissing": true,
"providers": [
{ "id": "provider-1", "name": "Megaplay", "url": "https://…/1/sub", "dubAvailable": false },
{ "id": "provider-2", "name": "Anixo", "url": "https://…/1", "dubAvailable": true }
]
}
}providers[].url can be loaded directly in an iframe (srcdoc-free plain embed). If you request lang=dub but no provider has dub for that episode, effectiveLang becomes sub and dubMissing is true — exactly what the site shows.
Errors
Errors always use this envelope:
{
"error": { "code": "invalid_token", "message": "The provided API token is not valid." }
}| Code | Status | Meaning |
|---|---|---|
| missing_token | 401 | No bearer token supplied. |
| invalid_token | 401 | Token does not exist. |
| token_revoked | 401 | Token (or its application) was revoked. |
| token_expired | 401 | OAuth access token expired (30 days). Get a new one. |
| insufficient_scope | 403 | OAuth token lacks the scope this endpoint needs. |
| rate_limited | 429 | More than 60 requests/minute. Retry after the `Retry-After` header. |
| invalid_params | 400 | Query parameter failed validation. |
| not_found | 404 | No anime exists with that id. |
| upstream_rate_limited | 429 | Upstream data provider is rate limiting us. Retry shortly. |
| upstream_error | 502 | Upstream data provider is unavailable. |
| auth_unavailable | 503 | Token verification temporarily unavailable. Retry. |
Need a token? Create one on your account page.