Authentication, limits and errors
How to authenticate Opttab API requests, stay inside the rate limit, page through lists and handle every error code.
HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json
{ "error": "ThrottlerException: Too Many Requests", "code": "RATE_LIMITED" }
{
"error": "API_KEY_SCOPE",
"code": "FORBIDDEN"
}
{
"error": "Validation failed",
"code": "VALIDATION_ERROR",
"details": {
"formErrors": [],
"fieldErrors": { "pageSize": ["Number must be less than or equal to 100"] }
}
}
Every Opttab API request carries one API key. This page covers how to send it, the rate limit, paging, and what each error means.
Authenticate
Send your key in the Authorization header as a bearer token:
Bearer opt_live_…: the word Bearer, one space, then the secret that starts with opt_live_.
curl https://api.opttab.com/v1/external/workspace \
-H "Authorization: Bearer opt_live_xxxxxxxx"
const response = await fetch('https://api.opttab.com/v1/external/workspace', {
headers: { Authorization: `Bearer ${process.env.OPTTAB_API_KEY}` },
});
const workspace = await response.json();
import os
import requests
response = requests.get(
"https://api.opttab.com/v1/external/workspace",
headers={"Authorization": f"Bearer {os.environ['OPTTAB_API_KEY']}"},
)
workspace = response.json()
These do not authenticate:
- A signed-in Opttab browser session (cookie).
- An
X-API-Keyheader. - An
?api_key=query parameter.
An API key also does not open any other Opttab route. It works only on /v1/external/*.
The workspace comes from the key
You choose a workspace when you create the key. Every call reads that workspace, and no route takes a workspace id. To read another workspace, create another key.
An id from another workspace behaves as if it does not exist: you get 404.
How a request is checked
Rate limit
Each key can make 60 requests per 60 seconds. The count is per key, not per IP address, so two servers sharing one key share the limit.
Request 61 inside the window gets 429 Too Many Requests with a Retry-After header:
429, wait the number of seconds in Retry-After, then retry. If you sync a lot of data, space requests about one second apart, and add exponential backoff with jitter for repeated 429s.Paging
List routes return one page at a time:
Page number, starting at 1. Default 1.
Items per page, from 1 to 100. Default 20.
Every list response carries items, page, pageSize and totalCount. Keep asking for the next page until you have totalCount items:
curl "https://api.opttab.com/v1/external/prompts?page=2&pageSize=100" \
-H "Authorization: Bearer opt_live_xxxxxxxx"
const headers = { Authorization: `Bearer ${process.env.OPTTAB_API_KEY}` };
const prompts = [];
for (let page = 1; ; page++) {
const res = await fetch(
`https://api.opttab.com/v1/external/prompts?page=${page}&pageSize=100`,
{ headers }
);
const body = await res.json();
prompts.push(...body.items);
if (body.items.length === 0 || prompts.length >= body.totalCount) break;
}
import os
import requests
headers = {"Authorization": f"Bearer {os.environ['OPTTAB_API_KEY']}"}
prompts, page = [], 1
while True:
body = requests.get(
"https://api.opttab.com/v1/external/prompts",
params={"page": page, "pageSize": 100},
headers=headers,
).json()
prompts.extend(body["items"])
if not body["items"] or len(prompts) >= body["totalCount"]:
break
page += 1
Paged routes: prompts, a prompt's responses, topics and competitors. GET /v1/external/responses/{responseId}/sources returns all sources in one list.
Query parameters are strict. An unknown parameter, such as ?limit=50, is refused with 400.
Error envelope
Every error has the same JSON shape:
| Field | What it holds |
|---|---|
error | A reason code such as API_KEY_SCOPE, or a short message |
code | The HTTP status as a name, such as FORBIDDEN |
details | Optional. Extra detail, for example which field failed validation |
requestId | Optional. Quote it when you contact support |
Branch on error in your code, not on the message text.
Error reference
| Status | error | Meaning | Fix |
|---|---|---|---|
| 400 | Validation failed | page, pageSize or period is out of range, or the query has an unknown parameter. details.fieldErrors names the field. | Fix the query. pageSize maximum is 100. period is one of 1d, 7d, 30d, 3m, 6m, 1y. |
| 401 | API_KEY_INVALID | The key is missing, not in the Bearer opt_live_… form, revoked, expired or otherwise unusable. All cases share this one answer on purpose. | Check the header. If it is right, create a new key in Settings › API. |
| 403 | API_KEY_IP_DENIED | The caller's IP address is not on the key's Allowed addresses. | Add the address or range to the key, or call from an allowed address. |
| 403 | API_KEY_SCOPE | The key does not have the scope this route needs. | Edit the key and tick the scope. See the scope table in the API overview. |
| 404 | PROMPT_NOT_FOUND | The prompt id is malformed or not in the key's workspace. | Use an id from GET /v1/external/prompts with the same key. |
| 404 | PROMPT_RESPONSE_NOT_FOUND | The answer id is malformed or not in the key's workspace. | Use an id from GET /v1/external/prompts/{promptId}/responses. |
| 429 | ThrottlerException: Too Many Requests | More than 60 requests in 60 seconds with this key. | Wait Retry-After seconds, then retry. |
Example 400: