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"]}typeis a stable identifier on the API’s host.titleis the machine-readable code. Branch on it, not ondetail.detailis for people and may change.statusrepeats the HTTP status.request_idis the same value as theRequest-Idresponse 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
| Status | Code | When |
|---|---|---|
| 401 | invalid_api_key | The key is missing, malformed, revoked or expired. The response carries WWW-Authenticate: Bearer. |
| 403 | plan_required | The store’s plan has no API access. Two webhook reads stay open on a paused plan; see Webhooks. |
| 403 | insufficient_scope | The key lacks the scope named in required. |
| 404 | not_found | No 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. |
| 405 | method_not_allowed | The route exists, the method does not. |
| 409 | conflict | The 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. |
| 409 | ai_run_in_progress | The AI assistant is editing this quiz right now; wait for it to finish, then retry the archive, unarchive or version save. |
| 409 | endpoint_limit | The store has reached its webhook endpoint quota (5 on Plus, 25 on Enterprise). |
| 409 | webhook_conflict | An 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. |
| 422 | validation_error | The 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. |
| 422 | webhook_url_unsafe | The webhook URL is not a public https address. |
| 429 | rate_limited | The 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. |
| 500 | internal_error | Something on our side; send us the request_id. |
| 502 | upstream_error | A service this request depends on failed; retry shortly. |
| 503 | limiter_unavailable | Limits could not be checked for a write, search or analytics call; retry after Retry-After. Reads are admitted. |
| 503 | dependency_unavailable | A backing service is temporarily unavailable; retry after Retry-After. |
| 504 | timeout | The request ran past 30 seconds. |
| other | http_error, request_failed | A 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
| Answer | What to do |
|---|---|
429 | Wait Retry-After seconds, then repeat the same request. |
503 (limiter_unavailable, dependency_unavailable) | Wait Retry-After (2 seconds), then repeat. |
502 | Repeat after a short pause. |
504 | Do 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. |
500 | Quote 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 osimport 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"])type Problem = { type: string; title: string; detail: string; status: number; request_id: string; required?: string[]; retry_after?: number; errors?: { field: string; message: string }[];}; // the full type is in "The Problem type" above
const res = await fetch("https://api.octaneai.com/v1/quizzes?state=live", { headers: { Authorization: `Bearer ${process.env.OCTANE_API_KEY}` },});if (!res.ok) { const problem = (await res.json()) as Problem; switch (problem.title) { case "rate_limited": console.log("wait", problem.retry_after, "seconds"); break; case "insufficient_scope": console.log("missing", problem.required); break; case "validation_error": (problem.errors ?? []).forEach((e) => console.log(e.field, e.message)); break; // query.state Unknown parameter default: console.log(problem.title, problem.request_id); }}package main
import ( "encoding/json" "fmt" "log" "net/http" "os")
type Problem struct { Type string `json:"type"` Title string `json:"title"` Detail string `json:"detail"` Status int `json:"status"` RequestID string `json:"request_id"` Required []string `json:"required,omitempty"` RetryAfter int `json:"retry_after,omitempty"` Errors []struct { Field string `json:"field"` Message string `json:"message"` } `json:"errors,omitempty"`}
func main() { req, _ := http.NewRequest("GET", "https://api.octaneai.com/v1/quizzes?state=live", nil) req.Header.Set("Authorization", "Bearer "+os.Getenv("OCTANE_API_KEY")) res, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } defer res.Body.Close() if res.StatusCode >= 400 { var p Problem json.NewDecoder(res.Body).Decode(&p) switch p.Title { case "rate_limited": fmt.Println("wait", p.RetryAfter, "seconds") case "insufficient_scope": fmt.Println("missing", p.Required) case "validation_error": for _, e := range p.Errors { fmt.Println(e.Field, e.Message) // query.state Unknown parameter } default: fmt.Println(p.Title, p.RequestID) } }}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.
