Skip to content

Overview

The public REST API for a store’s quizzes, analytics, shopper profiles, exports and webhooks.

Create a key on the dashboard (Developer > API keys) and send it as a Bearer token. See Plans for API availability.

Authentication and limits

Keys

A store admin creates keys on the dashboard (Developer > API keys). A key is shown once and never again, and may carry an expiry chosen when it is created. Send it as a Bearer token:

Authorization: Bearer oct_live_...

A key carries scopes chosen when it is created (quizzes:read, quizzes:write, quizzes:publish, analytics:read, profiles:read, webhooks:manage); the default is read-only (quizzes:read, analytics:read). A route that needs a scope the key lacks answers 403 insufficient_scope and names the scope it needs. A missing, malformed, revoked or expired key answers 401 invalid_api_key with WWW-Authenticate: Bearer.

Revoking a key takes effect within two minutes. To rotate, create the new key first, switch, then revoke the old one.

Plans

The API is available on the Plus and Enterprise plans. A key of a store on another plan answers 403 plan_required; a plan change gates within five minutes.

Limits

Every key has a leaky bucket and every store has one larger bucket shared by all its keys: three times a key’s on Plus, six times on Enterprise (bot_multiplier). A request costs 1 (reads), 2 (writes) or 3 (analytics) units; the bucket drains at a steady rate. An analytics call is charged 1 when the key is accepted and 2 more once an answer body is served, so a 304, a 404 or a 422 costs 1. The key’s bucket is charged first; a request the key’s bucket rejects never touches the store’s. GET /v1/store returns the three numbers in force (capacity, leak_per_second, bot_multiplier).

Every limited response carries:

RateLimit: limit=72, remaining=42, reset=2
RateLimit-Policy: 72;w=6

The headers are absent on a 401 and on a 403 answered before the limit is checked (plan_required, insufficient_scope). remaining is what the bucket can still take now and reset is the number of seconds until it is empty again; on an admitted request the headers describe the key’s bucket, on a 429 they describe the bucket that rejected (the store’s bucket reports its own, larger limit). Past the limit the answer is 429 rate_limited with a Retry-After header (seconds, also in the body as retry_after). When limits cannot be checked at all, reads are admitted and writes and analytics bodies answer 503 limiter_unavailable with Retry-After: 2.

Analytics caching

Analytics are cached for a short time. If the cache is unavailable or an answer is not ready after two seconds, the answer is computed directly and may be slower. Rate-limit checks still apply: if they cannot run, the request returns 503 limiter_unavailable.

Pagination

List endpoints page by cursor:

GET /v1/quizzes?limit=50
GET /v1/quizzes?limit=50&cursor=<next_cursor from the previous page>
  • limit is 20 by default, at least 1 and at most 100.
  • The response is {"data": [...], "has_more": true, "next_cursor": "..."}. When has_more is false, next_cursor is null.
  • A cursor is opaque and positional: it names a place in the list, never a filter. Send it back unchanged with the same filters. A cursor that was not issued by the API answers 422 validation_error.
  • Lists are newest first and stable under inserts: a row created after the first page was read appears on a later request of the first page, never in the middle of a walk.
  • Quiz and version lists order by creation time, with quiz UUID and public version UUID respectively breaking ties. Profile lists order by last-seen time, then profile UUID. These lists accept limits from 1 through 100.
  • POST /v1/profiles/search pages the same way: send the same body with every page and the cursor and limit as query parameters; total is exact up to 5,000 and total_capped says when counting stopped there. Totals on sparse filters can take about a second on very large stores.
  • /v1/analytics/quizzes pages by offset (offset, limit, sort, dir, and q), with no cursor; its default limit is 25 and its maximum is 1000. It answers data, has_more and total, and echoes limit, offset, sort and dir.
  • A query parameter the route does not declare answers 422 validation_error naming it.

Ids are prefixed strings (quiz_..., ver_..., key_...); an id of the wrong kind, of another store, or that does not exist answers 404 not_found.

Errors

Every non-2xx response 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 code, detail is for people, and request_id is the same value as the Request-Id response header. Quote the request_id when you write to support: it finds the request in the store’s API logs.

status title 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
403 insufficient_scope the key lacks the scope named in required
404 not_found no such resource for this store (an id of another store answers the same); detail is always Not found and never echoes the id
405 method_not_allowed the route exists, the method does not
409 conflict the request cannot apply in the resource’s current state: publishing a version with no pages, or starting an export of a dataset that is still being built
409 ai_run_in_progress the AI assistant is editing the quiz right now; archive, unarchive or write again when it has finished
422 validation_error the request did not validate; errors lists field and message, a query parameter the route does not declare included
429 rate_limited the key’s or the store’s bucket is full; see Retry-After (also retry_after in the body). A store-level reject has already spent the key’s own bucket for that request; there is no refund.
500 internal_error something on our side; send 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 or analytics call; retry after Retry-After
503 dependency_unavailable a backing service is temporarily unavailable; retry after Retry-After
504 timeout the request ran past 30 seconds

The body is the Problem schema of the OpenAPI document: the five fields above on every error, required on insufficient_scope, retry_after on 429 and 503, errors on validation_error.

Versioning

The version is in the path: /v1. Within v1 the API only grows:

  • New endpoints, new optional parameters and new response fields may appear at any time. Clients must ignore fields they do not know.
  • Nothing is removed, renamed or retyped, and no input becomes required, without a deprecation window. A deprecated endpoint or field keeps working for at least 90 days and answers with Deprecation and Sunset headers naming the date; the replacement is available for the whole window.

Within /v1 the API only grows; nothing is removed or renamed.

Information

  • OpenAPI version: 3.1.0

An oct_live_... key minted on the dashboard

Security scheme type: http