Errors
All non-2xx responses use RFC 9457 problem
details JSON, with
Content-Type: application/problem+json.
HTTP/1.1 401 Unauthorized
Content-Type: application/problem+json
{
"type": "https://api.statmuse.com/errors/auth-key-missing",
"status": 401,
"title": "API key is required",
"instance": "/sports/nba/v1/ask",
"traceId": "550e8400-e29b-41d4-a716-446655440000"
}
Match on the slug
The type URI’s last segment is the slug, a stable identifier
clients branch on. The HTTP status code and human-readable title may be
refined over time; slugs will not be renamed without bumping the API
version.
Match on
type(or its slug). Do not match ontitle,statusordetail.
The traceId field
Every problem response includes a traceId on the response body,
not as a header. This is the request identifier used in StatMuse’s logs.
Include it when opening a support ticket so we can correlate the failure.
Slug taxonomy
Authentication & authorization
| Slug | Status | When |
|---|---|---|
auth-key-missing |
401 | No x-statmuse-api-key header. |
auth-key-invalid |
401 | Header present but doesn’t match a known key (or is longer than 256 characters). |
auth-key-disabled |
403 | Key was revoked from the dashboard. |
account-disabled |
403 | Account has been disabled. Contact support. |
auth-scope-insufficient |
403 | Rare. Fires only if your key was provisioned without access to this endpoint. Contact us. |
Quota & rate limiting
| Slug | Status | When |
|---|---|---|
rate-limited |
429 | Burst rate limit exceeded. Honor Retry-After and try again. |
quota-exceeded |
402 | Period quota exhausted. See Rate Limits & Quotas. |
Over MCP, quota exhaustion doesn’t arrive as this slug at all. A
tools/call made over quota returns a tool error instead; see
Errors over MCP.
Billing & subscription
These fire when your access is not in an active state. During the beta, access comes with StatMuse+. Manage or renew your membership on statmuse.com to restore it.
| Slug | Status | When |
|---|---|---|
subscription-past-due |
402 | Payment failed and is being retried. Update your payment method to restore access. |
subscription-canceled |
402 | Subscription was canceled. Re-subscribe to restore access. |
subscription-incomplete |
402 | Initial checkout payment hasn’t completed. |
subscription-unpaid |
402 | Subscription is unpaid after retries. Update your payment method. |
subscription-paused |
402 | Subscription is paused. Resume it to restore access. |
channel-not-entitled |
402 | Your membership is active but does not include this channel (REST vs MCP). |
Over MCP the subscription-* slugs and channel-not-entitled arrive as
403 instead of 402; see Errors over MCP.
Request validation & routing
| Slug | Status | When |
|---|---|---|
validation-failed |
400 | The request failed validation (e.g. a missing or invalid input parameter). Schema failures include per-field errors in the body. |
not-understood |
400 | We couldn’t parse your input into a sports question. |
not-supported |
400 | We parsed the question but it isn’t supported on this domain. |
unsupported-version |
400 | The version in the path (/v{n}) isn’t supported for this domain. |
route-not-found |
404 | The URL didn’t match any known route. |
payload-too-large |
413 | Request body exceeded the size limit. |
Server-side
| Slug | Status | When |
|---|---|---|
request-timeout |
504 | The request exceeded the end-to-end processing deadline. Retry with backoff. |
client-closed-request |
499 | You disconnected before we could respond. |
internal-error |
500/503/504 | Unexpected server error, including upstream faults. Include traceId if you report it. |
Errors over MCP
Errors over MCP are handled by your MCP client and your agent, not by code you write. They surface in two layers:
Refusals are problem details, at different statuses. Anything that
refuses a request before a tool runs (authentication, rate limiting,
subscription state) returns the same problem-details JSON with the
same slugs, and slug matching works exactly as above. One divergence:
MCP defines no 402, so the subscription-* family and
channel-not-entitled arrive as 403. Most clients render a refusal
as a failed connection or a generic tool failure rather than showing
the body.
A few slugs exist only over MCP:
| Slug | Status | When |
|---|---|---|
auth-token-missing |
401 | No bearer token. Normal during connect; this is what triggers the client’s sign-in. |
auth-token-invalid |
401 | Token expired or failed verification. The client should re-authenticate. |
account-not-provisioned |
403 | Signed in, but no API account exists. Get StatMuse+ first, then reconnect. |
Tool errors are not problem details. A tool call that fails on your
own input (an unanswerable stats query, an unknown ID) returns an MCP
tool error carrying a plain-text message; your agent sees it
directly and typically adjusts and retries on its own. An exhausted
quota travels the same channel: a tools/call made over quota returns
a tool error carrying the quota numbers, the reset date, and where to
upgrade, instead of running the tool. Connecting and listing tools
keep working regardless of quota state. How tool errors count against
your quota is covered in
Rate Limits & Quotas.
Handling errors in TypeScript
Branch on the slug derived from type. Special-case rate-limited and
quota-exceeded so you can honor Retry-After; log everything else
with traceId for correlation.
type ProblemDetails = {
type: string;
status: number;
title: string;
detail?: string;
traceId?: string;
};
const PROBLEM_BASE = "https://api.statmuse.com/errors/";
const slugFromType = (type: string): string =>
type.startsWith(PROBLEM_BASE) ? type.slice(PROBLEM_BASE.length) : type;
async function ask(input: string): Promise<unknown> {
const url = new URL("https://api.statmuse.com/sports/nba/v1/ask");
url.searchParams.set("input", input);
const res = await fetch(url, {
headers: { "x-statmuse-api-key": process.env.STATMUSE_API_KEY! },
});
if (res.ok) return res.json();
const problem = (await res.json()) as ProblemDetails;
const slug = slugFromType(problem.type);
switch (slug) {
case "rate-limited":
case "quota-exceeded": {
const retryAfter = Number(res.headers.get("Retry-After") ?? "0");
throw new RetryableError(slug, retryAfter);
}
case "not-understood":
case "not-supported":
throw new BadQuestionError(slug, problem.detail);
default:
console.error("StatMuse API error", { slug, traceId: problem.traceId });
throw new ApiError(slug, problem.title);
}
}
The Retry-After header is in seconds. For rate-limited it is
typically a small integer; for quota-exceeded it can be days. Decide
whether to wait, queue the work for later or surface the failure to the
end user.