Errors

Every error has the same shape, a stable type and a plain-English message. Here's what each one means and how to fix it.

When something goes wrong, you get a non-2xx HTTP status and this body:

{
  "error": {
    "type": "insufficient_credits",
    "message": "This call needs 3 credits and you have 1. Buy credits at /app/billing.",
    "docs": "https://veqen-app.com/docs/errors#insufficient_credits"
  }
}
  • type is stable. Branch on it in your code.
  • message is written for humans. Log it, or show it to a developer.
  • Every response also has an X-Request-Id header. Include it if you contact support.

Failed requests are never charged.

Error types

missing_api_key

HTTP 401. The request had no API key. Add the header Authorization: Bearer vq_live_....

invalid_api_key

HTTP 401. The key is malformed, doesn't exist, or was revoked. Copy it again carefully, or create a new key in the dashboard.

account_suspended

HTTP 403. The account is suspended, usually after a payment dispute. Contact us.

insufficient_credits

HTTP 402. Your balance doesn't cover this call. Buy credits, turn on auto top-up, or wait for your free credits to reset on the 1st. Unconfirmed accounts get no free credits: confirm your email first.

invalid_request

HTTP 422. The JSON body is missing a field or has the wrong type. The message names the field, for example email: Field required.

idempotency_key_reused

HTTP 422 or 409. You reused an Idempotency-Key with a different request body (422), or sent the same key twice at the same moment (409). Use a fresh key for each distinct request. See idempotency.

rate_limited

HTTP 429. Too many requests per second. Wait for the number of seconds in the Retry-After header, then retry. See rate limits.

server_error

HTTP 500. Something broke on our side. Retry with exponential backoff. If you send an Idempotency-Key, retries are always safe.

A good retry policy

Retry on 429 and 5xx only. Never retry 4xx errors other than 429: the same request will fail the same way.

import time, requests


def check(payload, key, attempts=4):
    for attempt in range(attempts):
        r = requests.post(
            "https://veqen-app.com/v1/signup/check",
            headers={"Authorization": f"Bearer {key}", "Idempotency-Key": payload["request_id"]},
            json={k: v for k, v in payload.items() if k != "request_id"},
            timeout=10,
        )
        if r.status_code == 429 or r.status_code >= 500:
            time.sleep(float(r.headers.get("Retry-After", 2**attempt)))
            continue
        return r
    return r
async function check(body, key, requestId, attempts = 4) {
  let res;
  for (let attempt = 0; attempt < attempts; attempt++) {
    res = await fetch("https://veqen-app.com/v1/signup/check", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${key}`,
        "Content-Type": "application/json",
        "Idempotency-Key": requestId,
      },
      body: JSON.stringify(body),
    });
    if (res.status !== 429 && res.status < 500) return res;
    const wait = Number(res.headers.get("Retry-After")) || 2 ** attempt;
    await new Promise((r) => setTimeout(r, wait * 1000));
  }
  return res;
}

Fail open, not closed

If Veqen is unreachable, don't block real people. Treat a timeout or 5xx like a review verdict: let them in, and require email confirmation.