ankitects/anki · warning
Unexpected API access. Please report this message on the Ank
Error message
Unexpected API access. Please report this message on the Anki forums.
What it means
_check_dynamic_request_permissions() in qt/aqt/mediasrv.py guards the local Anki API exposed over the media server. POST requests must come from pages Anki itself trusts: they need a `Content-type: application/binary` header (to prove they aren't opaque cross-origin form posts) and must either come from a page granted full API access (_have_api_access()) or hit a small whitelist of reviewer/previewer endpoints. When either check fails, Anki shows the warning 'Unexpected API access. Please report this message on the Anki forums.' on the main thread and aborts the request with HTTP 403. This is a deliberate CSRF/origin defense, not a bug in your collection.
Source
Thrown at qt/aqt/mediasrv.py:1301
return wrapped
else:
return NotFound(message=f"{path} not found")
def _check_dynamic_request_permissions():
if request.method == "GET":
return
def warn() -> None:
show_warning(
"Unexpected API access. Please report this message on the Anki forums."
)
# check content type header to ensure this isn't an opaque request from another origin
if request.headers.get("Content-type") != "application/binary":
aqt.mw.taskman.run_on_main(warn)
abort(403)
# does page have access to entire API?
if _have_api_access():
return
# whitelisted API endpoints for reviewer/previewer
if request.path in (
"/_anki/getSchedulingStatesWithContext",
"/_anki/setSchedulingStates",
"/_anki/i18nResources",
"/_anki/congratsInfo",
):
pass
else:
# other legacy pages may contain third-party JS, so we do not
# allow them to access our API
aqt.mw.taskman.run_on_main(warn)
abort(403)View on GitHub (pinned to 2fae55543c)
Solutions
- If you are the author of the code making the request, set the header `Content-type: application/binary` on POST requests to /_anki endpoints and post protobuf bytes, not JSON.
- Only call whitelisted endpoints (getSchedulingStatesWithContext, setSchedulingStates, i18nResources, congratsInfo) from reviewer/previewer contexts without full API access.
- Use the official anki/tslib frontend code (which sends proper binary requests from trusted pages) instead of hand-rolled fetch calls against the local server.
- If the warning appears during normal use of a stock deck/add-on, report it on the Anki forums with the steps to reproduce, as the message requests — it indicates untrusted JS attempted an API call.
Example fix
// before
fetch("/_anki/addNote", {
method: "POST",
headers: { "Content-type": "application/json" },
body: JSON.stringify(payload),
});
// after
fetch("/_anki/addNote", {
method: "POST",
headers: { "Content-type": "application/binary" },
body: protobufBytes,
}); Defensive patterns
Strategy: validation
Validate before calling
const headers = { "Content-type": "application/binary" };
const allowed = ["/_anki/getSchedulingStatesWithContext", "/_anki/setSchedulingStates", "/_anki/i18nResources", "/_anki/congratsInfo"];
if (!allowed.includes(path)) {
throw new Error("endpoint not permitted from this page context");
}
await fetch(path, { method: "POST", headers, body: protobufBytes }); Type guard
function isBinaryPost(init: RequestInit): boolean {
return new Headers(init.headers).get("Content-type") === "application/binary";
} Try / catch
try {
const resp = await fetch("/_anki/" + endpoint, { method: "POST", headers: { "Content-type": "application/binary" }, body: data });
if (resp.status === 403) throw new Error("API access denied for this page/endpoint");
} catch (e) {
console.error("Anki API call blocked:", e);
} Prevention
- Always POST to /_anki endpoints with Content-type exactly 'application/binary'.
- From reviewer/previewer or third-party-embedding pages, restrict calls to the four whitelisted endpoints.
- Do not embed untrusted third-party JS in pages that call the Anki backend API.
- Treat a 403 plus the 'Unexpected API access' warning as a security guard firing, and fix the request origin/headers rather than bypassing it.
When it happens
Trigger: An HTTP POST to any /_anki endpoint whose `Content-type` header is not exactly `application/binary` (e.g. application/json, text/plain, missing, or the browser-normalized `application/x-www-form-urlencoded` from a cross-origin form), or a POST from a page without full API access (e.g. a legacy page or page embedding third-party JS) to an endpoint outside the whitelist (getSchedulingStatesWithContext, setSchedulingStates, i18nResources, congratsInfo).
Common situations: Third-party JavaScript embedded in shared decks/add-on webviews trying to call the Anki backend API; add-on or script authors posting JSON instead of binary protobuf with the correct Content-type header; a malicious webpage on another origin submitting a form POST to localhost (the exact attack this guard blocks); custom tooling hitting the local media server without mimicking the official client headers.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
AI-assisted analysis of ankitects/anki@2fae55543c (2026-09-12).
Data as JSON: /api/errors/314e5012b4e2fe3e.
Report an issue: GitHub.