Skip to content

Error Reference

All Canviq API errors follow RFC 7807 (Problem Details for HTTP APIs).


Error Format

{
  "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')
}