# Errors

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

```json
{
  "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](https://veqen-app.com/contact).

### 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](/docs/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](/docs/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.

```python [Python]
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
```

```javascript [JavaScript]
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.
