Information
- OpenAPI version:
3.1.0
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.
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 and changeable on the dashboard at any time (quizzes:read, quizzes:write, quizzes:publish, analytics:read, experiments:read, experiments:write, 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 or changing its scopes takes effect on the next request (within two minutes at the outside). To rotate, create the new key first, switch, then revoke the old one.
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.
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 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.
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.{"data": [...], "has_more": true, "next_cursor": "..."}. When has_more is false, next_cursor is null.422 validation_error.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.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.
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, or does not include the view; for a view, feature names it and plan the lowest plan that includes it |
| 403 | insufficient_scope |
the key lacks the scope named in required |
| 403 | experiments_disabled |
the store’s plan does not include experiments; creating, changing, starting or republishing one is refused, reading and ending still answer |
| 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 | endpoint_limit |
the store already has as many webhook endpoints as its plan allows |
| 409 | webhook_conflict |
the webhook request conflicts with the endpoint’s state: its URL already has an endpoint configured differently, a test was sent to a disabled endpoint, or the event being redelivered was skipped while the plan was paused |
| 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 |
| 422 | invalid_experiment |
the experiment cannot be set up or changed that way; detail says why |
| 422 | webhook_url_unsafe |
the webhook URL is not a public https address |
| 422 | webhook_pii_required |
an endpoint that subscribes to identity.captured must include contact details (include_pii: true) |
| 4xx | http_error, request_failed |
a refusal without a code of its own; detail says why |
| 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, feature and plan on a plan_required view, retry_after on 429 and 503, errors on validation_error.
The version is in the path: /v1. Within v1 the API only grows:
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.
An oct_live_... key minted on the dashboard
Security scheme type: http