Error Reference
All Canviq API errors follow RFC 7807 (Problem Details for HTTP APIs).
{
"error": "plan_limit_reached",
"message": "You've reached the 10,000 submissions/month limit on your current plan.",
"limit": 10000,
"current": 10001,
"plan": "professional",
"upgrade_url": "https://{your-slug}.canviq.app/en/admin/settings/billing"
}
| Field | Always present | Description |
error | Yes | Machine-readable error code |
message | Yes | Human-readable explanation |
| other fields | No | Context-specific (see individual errors) |
HTTP Status → Error Code Reference
400 Bad Request
| Error code | When |
validation_error | Request body is malformed or missing required fields |
invalid_slug | Slug contains reserved words, invalid characters, or is too short/long |
invalid_redirect | Redirect URL is not a registered trusted origin |
{
"error": "validation_error",
"message": "title is required and must be between 5 and 200 characters",
"field": "title"
}
401 Unauthorized
| Error code | When |
unauthorized | No Authorization header, or key format is invalid |
bootstrap_token_used | Bootstrap token was already consumed |
bootstrap_token_expired | Bootstrap token passed its 7-day TTL |
session_expired | Session cookie has expired (browser flows) |
403 Forbidden
| Error code | When |
forbidden | API key is valid but lacks the required scope |
trial_expired | Trial has ended and action requires an active plan |
org_suspended | Organization account is suspended |
{
"error": "forbidden",
"message": "This key does not have the 'members:write' scope. Regenerate with the required scope.",
"required_scope": "members:write"
}
402 Payment Required
| Error code | When |
plan_limit_reached | Monthly quota exceeded (submissions, members, etc.) |
trial_expired | Trial ended; action requires upgrade |
{
"error": "plan_limit_reached",
"message": "You've reached the 500 submissions/month limit on your Trial plan.",
"limit": 500,
"current": 501,
"plan": "trial",
"upgrade_url": "https://{your-slug}.canviq.app/en/admin/settings/billing"
}
404 Not Found
| Error code | When |
not_found | Resource (org, submission, category, etc.) does not exist |
409 Conflict
| Error code | When |
slug_taken | Organization slug is already in use |
already_member | User is already a member of the org |
duplicate_vote | User already voted on this submission |
429 Too Many Requests
| Error code | When |
rate_limited | Request rate exceeds the limit for this key tier |
{
"error": "rate_limited",
"message": "Rate limit exceeded. Retry after 23 seconds.",
"retry_after": 23,
"limit": 100,
"window": "1 minute"
}
Always check the Retry-After header and back off accordingly.
500 Internal Server Error
| Error code | When |
internal_error | Unexpected server-side error |
{
"error": "internal_error",
"message": "An unexpected error occurred. Please retry. If the issue persists, contact support.",
"request_id": "req_01HRXYZ..."
}
Include the request_id when contacting support — it allows us to trace the request in our logs.
Error Handling Patterns
Retry with Backoff (Node.js)
async function callCanviq<T>(
fn: () => Promise<Response>,
maxRetries = 3
): Promise<T> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const response = await fn()
if (response.ok) return response.json() as Promise<T>
const error = await response.json()
// Don't retry client errors (4xx, except 429)
if (
response.status >= 400 &&
response.status < 500 &&
response.status !== 429
) {
throw new Error(`Canviq error: ${error.error} — ${error.message}`)
}
// Rate limited — wait and retry
if (response.status === 429) {
const retryAfter = ((error.retry_after as number) ?? 60) * 1000
await new Promise((resolve) => setTimeout(resolve, retryAfter))
continue
}
// Server error — exponential backoff
if (attempt < maxRetries) {
await new Promise((resolve) =>
setTimeout(resolve, Math.pow(2, attempt) * 1000)
)
}
}
throw new Error('Max retries exceeded')
}