The Surventrics API uses conventional HTTP status codes and returns detailed error messages to help you debug issues.
All errors follow a consistent structure:
{
"error": {
"code": "error_code",
"message": "Human-readable error description",
"details": {
// Additional context (optional)
}
}
}| Status | Meaning |
|---|---|
200 OK | Request succeeded |
201 Created | Resource created successfully |
204 No Content | Request succeeded (no response body) |
400 Bad Request | Invalid request parameters or body |
401 Unauthorized | Missing or invalid API key |
402 Payment Required | The monthly AI actions are used up (code ai_limit_reached) |
403 Forbidden | API key lacks required scope |
404 Not Found | Resource doesn't exist |
422 Unprocessable Entity | The request is valid but the survey can't go live yet (code publish_blocked) |
429 Too Many Requests | Rate limit exceeded |
500 Internal Error | Server error (please report) |
503 Service Unavailable | Feature temporarily disabled via kill-switch (e.g. the survey health check, code unavailable) |
| Code | Description | Resolution |
|---|---|---|
unauthorized | Missing or invalid API key | Check your Authorization header |
forbidden | Access denied to the resource, or the credential's member lacks the capability the endpoint needs (error.details.reason is actor_capability; requiredCapability names it). A key with no current member behind it is refused with actor_not_member. | Check the survey belongs to your organisation; for actor_capability, ask an admin to change the member's role or reassign the key |
invalid_scope | API key missing required scope | Create a new key with the needed scope |
expired_key | API key has expired | Create a new API key |
inactive_key | API key has been deactivated | Re-activate or create a new key |
not_found | Resource not found | Check the resource ID |
invalid_request | Validation failed | Check the error details for specifics |
publish_blocked | The survey has a problem that stops it going live (HTTP 422, when status changes to live) | Fix each entry in details.problems, then try again |
rate_limited | Too many requests | Wait and retry with exponential backoff |
ai_limit_reached | The monthly AI actions are used up (HTTP 402) | Don't retry: wait for details.resetsAt, or upgrade |
quota_exceeded | Retired: no endpoint returns it. It was the AI allowance refusal's code until October 2026 | Handle ai_limit_reached instead |
internal_error | Server error | Retry later or contact support |
A handful of AI-powered endpoints enforce account-level limits that are distinct from the per-key and per-IP rate limits described above. A 402 from these endpoints means your organisation's monthly AI actions are used up (not that you are sending requests too quickly), and a 403 means your plan does not currently include the feature.
Metered AI endpoints (for example POST /api/v1/questions/check) draw down a monthly allowance of AI actions, and any extra AI action balance the organisation holds is used after it. When both are spent, the endpoint returns 402 with code ai_limit_reached and a details object: limit (the monthly allowance), remaining (what is left, extra balance included), packBalance (the extra balance), resetsAt (when the allowance next resets), offer (what the organisation can do about it: buy_pack when it can buy an AI action pack, upgrade when it cannot but a higher plan exists, and none when there is no plan above its own) and, when known, required (what this call needed). Unlike a rate limit, retrying will not succeed until the allowance resets on resetsAt or the organisation has more AI actions. On a paid plan an owner or admin can buy an AI action pack on the billing page when the offer is buy_pack.
HTTP/1.1 402 Payment Required
{
"error": {
"code": "ai_limit_reached",
"message": "Monthly AI action allowance reached",
"details": {
"feature": "ai_actions",
"limit": 1000,
"remaining": 0,
"packBalance": 0,
"resetsAt": "2026-11-14T00:00:00.000Z",
"offer": "upgrade",
"required": 1
}
}
}Changed in October 2026. This refusal used to carry code quota_exceeded. It is now ai_limit_reached on every endpoint, with the same 402 status. quota_exceeded stays in the published list of codes so generated clients keep compiling, but no endpoint returns it: a client that branches on it should branch on ai_limit_reached. Two fields also changed meaning: details.remaining now includes any extra AI action balance, and details.required is in AI actions (the call's weight times its model calls), not model calls.
When your plan does not include a requested AI feature (for example voice input), the endpoint returns 403 with code forbidden and the message "This feature is not currently available". This is an entitlement check, not a rate limit — upgrade your plan or enable the feature to gain access.
HTTP/1.1 403 Forbidden
{
"error": {
"code": "forbidden",
"message": "This feature is not currently available"
}
}HTTP/1.1 401 Unauthorized
{
"error": {
"code": "unauthorized",
"message": "Invalid or missing API key"
}
}HTTP/1.1 403 Forbidden
{
"error": {
"code": "invalid_scope",
"message": "API key missing required scope: questions:write"
}
}HTTP/1.1 400 Bad Request
{
"error": {
"code": "invalid_request",
"message": "Validation failed",
"details": {
"errors": {
"title": ["String must contain at least 1 character(s)"],
"type": ["Invalid enum value"]
}
}
}
}HTTP/1.1 400 Bad Request
{
"error": {
"code": "invalid_request",
"message": "Cannot modify questions on a live survey. Pause it first."
}
}code field for programmatic error handlingdetails field for debuggingdetails.errorsasync function apiRequest(url, options) {
const response = await fetch(url, options);
if (!response.ok) {
const error = await response.json();
switch (error.error.code) {
case 'unauthorized':
throw new Error('Invalid API key');
case 'rate_limited':
// Implement exponential backoff
await sleep(1000);
return apiRequest(url, options);
case 'invalid_request':
// Handle validation errors
console.log('Validation errors:', error.error.details?.errors);
throw new Error(error.error.message);
default:
throw new Error(error.error.message);
}
}
return response.json();
}