Skip to content

Errors

Every error response (4xx and 5xx) is an RFC 9457 problem body with the media type application/problem+json:

{
"type": "https://api.octaneai.com/errors/insufficient_scope",
"title": "insufficient_scope",
"detail": "The API key does not carry the scope this route needs",
"status": 403,
"request_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"required": ["quizzes:read"]
}
  • type is a stable identifier on the API’s host.
  • title is the machine-readable code. Branch on it, not on detail.
  • detail is for people and may change.
  • status repeats the HTTP status.
  • request_id is the same value as the Request-Id response header.

Extra fields appear on some errors only: required (the missing scopes, on insufficient_scope), retry_after (seconds, on 429 and 503) and errors (one entry per invalid input, on most validation_error answers; an analytics range or filter refusal may carry max_days, filter or value instead). Read them with a default.

Codes

StatusCodeWhen
401invalid_api_keyThe key is missing, malformed, revoked or expired. The response carries WWW-Authenticate: Bearer.
403plan_requiredThe store’s plan has no API access. Two webhook reads stay open on a paused plan; see Webhooks.
403insufficient_scopeThe key lacks the scope named in required.
404not_foundNo such resource for this store. An id that belongs to another store, an id of the wrong kind, a deleted quiz, a removed person and an expired export all answer the same; detail is always Not found.
405method_not_allowedThe route exists, the method does not.
409conflictThe change cannot be made in the object’s current state: publishing a version with no pages, or requesting an export of a dataset that is still being built.
409ai_run_in_progressThe AI assistant is editing this quiz right now; wait for it to finish, then retry the archive, unarchive or version save.
409endpoint_limitThe store has reached its webhook endpoint quota (5 on Plus, 25 on Enterprise).
409webhook_conflictAn endpoint with this URL exists with a different configuration, a delivery skipped while the plan was paused was asked to be redelivered, or a test was sent to a disabled endpoint.
422validation_errorThe request did not validate. errors lists each field (a dotted path such as body.label or query.from) and message. A query parameter the route does not declare, a malformed cursor, an unknown timezone, an unknown answer-filter term and a search statement that ran past 10 seconds all answer this.
422webhook_url_unsafeThe webhook URL is not a public https address.
429rate_limitedThe key’s or the store’s allowance is spent, or the store’s 15 search slots are all taken; see Retry-After (also retry_after in the body). A store-level refusal has already spent the key’s own allowance for that request.
500internal_errorSomething on our side; send us the request_id.
502upstream_errorA service this request depends on failed; retry shortly.
503limiter_unavailableLimits could not be checked for a write, search or analytics call; retry after Retry-After. Reads are admitted.
503dependency_unavailableA backing service is temporarily unavailable; retry after Retry-After.
504timeoutThe request ran past 30 seconds.
otherhttp_error, request_failedA status the API did not produce itself (a proxy or an upstream answered); treat it like internal_error and send us the request_id.

Analytics routes also answer 304 Not Modified with no body when your If-None-Match matches; that is not an error. See Analytics.

Retry policy

AnswerWhat to do
429Wait Retry-After seconds, then repeat the same request.
503 (limiter_unavailable, dependency_unavailable)Wait Retry-After (2 seconds), then repeat.
502Repeat after a short pause.
504Do not repeat a write blindly: the request may have completed before the answer was cut off. Re-read the resource first (the quiz summary, the export list, the endpoint), then repeat only what is missing.
500Quote the request_id to support; repeating rarely helps.

Which writes are safe to repeat: archive, unarchive, publish, endpoint PATCH and DELETE, reveal-secret, redeliver and redeliver-failed (each redelivery is a new attempt of the same event, deduplicated by webhook-id on your side). Repeating POST .../versions saves a second version, POST /v1/exports answers 409 conflict while the first file builds (poll that one), POST /v1/webhooks with the same URL and configuration answers 200 with the existing endpoint, and rotate-secret rotates again.

A validation error

{
"type": "https://api.octaneai.com/errors/validation_error",
"title": "validation_error",
"detail": "Request validation failed",
"status": 422,
"request_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
"errors": [
{ "field": "query.sort", "message": "Unknown parameter" }
]
}

A misspelled filter is refused rather than ignored, so a typo never silently returns the whole list.

The Problem type

One type covers every error body; the pages that list their own errors link here.

export type Problem = {
type: string; // "https://api.octaneai.com/errors/<title>"
title: string; // the machine code: "rate_limited", "validation_error", ...
detail: string; // for people; may change
status: number; // repeats the HTTP status
request_id: string; // the Request-Id header
required?: string[]; // insufficient_scope: the missing scopes
retry_after?: number; // 429 and 503: seconds to wait
errors?: { field: string; message: string }[]; // validation_error: one entry per bad input
max_days?: number; // an analytics range refusal
filter?: string; // an analytics filter refusal
value?: string; // an analytics filter refusal
};

Handling errors

The request below asks for a quiz with a filter the route does not know, so it answers 422:

import os
import httpx
r = httpx.get(
"https://api.octaneai.com/v1/quizzes",
params={"state": "live"},
headers={"Authorization": f"Bearer {os.environ['OCTANE_API_KEY']}"}, timeout=30)
if r.status_code >= 400:
problem = r.json()
code = problem["title"]
if code == "rate_limited":
print("wait", problem["retry_after"], "seconds")
elif code == "insufficient_scope":
print("missing", problem["required"])
elif code == "validation_error":
for e in problem.get("errors") or []:
print(e["field"], e["message"]) # query.state Unknown parameter
else:
print(code, problem["request_id"])

The Request-Id header

Every response, success or error, carries a Request-Id header. Every authenticated request is listed in your store’s API logs under Developer > API logs, where the id finds it. Quote it when you write to support.