> ## Documentation Index
> Fetch the complete documentation index at: https://vaquill.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors

> HTTP status codes and error handling

## Error Format

Errors return JSON with a `detail` field:

```json theme={"theme":"github-dark"}
{
  "detail": "Invalid or missing API key."
}
```

Validation errors (422) include field-level details:

```json theme={"theme":"github-dark"}
{
  "detail": [
    {
      "loc": ["body", "query"],
      "msg": "Field required",
      "type": "missing"
    }
  ]
}
```

## Status Codes

| Code | Meaning | Action |
| - | - | - |
| `200` | Success | |
| `400` | Bad Request | The request parsed but the API refused it. See [400 versus 422](#400-versus-422) |
| `401` | Unauthorized | Check your API key and `Authorization` header |
| `402` | Payment Required | Check your account dashboard. |
| `403` | Forbidden | Your key lacks the required scope, or your plan does not include the feature. Webhook delivery is Business-only |
| `404` | Not Found | Resource does not exist |
| `409` | Conflict | You already hold this watch. See [409 on watches](#409-on-watches) |
| `422` | Validation Error | Check request body for missing or invalid fields |
| `429` | Rate Limited | Back off and retry with exponential backoff. Also returned when you exceed your plan's watch cap |
| `500` | Internal Error | Retry after a brief delay. [Contact support](mailto:contact@vaquill.ai) if it persists |

On `403`, webhook delivery on a watch requires the Business plan: use `channel: "email"` or upgrade.
On `429`, the watch cap is 3 outside Business and 100 on Business.

### 400 versus 422

A `422` is schema validation: a field is missing, or its type or range is wrong.
FastAPI rejects it before your request reaches any business logic, and the response carries the `loc` array shown above.

A `400` is a request that is well-formed but not allowed.
`POST /watches` and `PATCH /watches/{id}` return it for:

* a `scope` the board cannot be narrowed by, or an `actId` scope that does not exist on that board,
* a source that is not scopable at all, meaning a board whose `scopable` is `false` such as the Federal Register,
* a missing `webhookUrl` or `emailAddress` for the `channel` you asked for, or one set on a channel that does not use it,
* a `webhookUrl` the API refuses as a delivery target,
* a `webhookAuth.headerName` that is not a legal header name, or outbound auth set on an email-only watch.

The `detail` string names the specific problem, so read it rather than retrying.

### 409 on watches

`POST /watches` returns `409` when you already hold a watch on the same board, the same channel, and the same scope.
Watches are unique on that triple, so this means the subscription you were about to create already exists rather than that something went wrong.
`PATCH /watches/{id}` returns it for the same reason, when narrowing a watch's `scope` would collide with another watch you already hold.
List your watches with `GET /watches` and reuse the existing one.

## Rate Limiting

Limits are enforced per API key and scale with your plan. When you exceed one the API returns `429` with a `Retry-After` header telling you how many seconds to wait. Respect it, and add exponential backoff so retries spread out under sustained pressure.

<CodeGroup>
  ```python Python theme={"theme":"github-dark"}
  import time
  import requests

  def search_with_retry(payload, max_retries=5):
      url = "https://api.vaquill.ai/api/v1/us/statutes/search"
      headers = {
          "Authorization": "Bearer vq_key_...",
          "Content-Type": "application/json",
      }
      for attempt in range(max_retries):
          resp = requests.post(url, headers=headers, json=payload)
          if resp.status_code == 429:
              # Prefer the server hint, fall back to exponential backoff.
              retry_after = resp.headers.get("Retry-After")
              delay = int(retry_after) if retry_after else 2 ** attempt
              time.sleep(delay)
              continue
          resp.raise_for_status()
          return resp.json()
      raise RuntimeError("Rate limit: max retries exceeded")

  data = search_with_retry({"query": "data breach notification"})
  ```

  ```javascript JavaScript theme={"theme":"github-dark"}
  async function searchWithRetry(payload, maxRetries = 5) {
    const url = "https://api.vaquill.ai/api/v1/us/statutes/search";
    const headers = {
      Authorization: "Bearer vq_key_...",
      "Content-Type": "application/json",
    };

    for (let attempt = 0; attempt < maxRetries; attempt++) {
      const resp = await fetch(url, {
        method: "POST",
        headers,
        body: JSON.stringify(payload),
      });

      if (resp.status === 429) {
        const retryAfter = resp.headers.get("Retry-After");
        const delay = retryAfter ? Number(retryAfter) * 1000 : 2 ** attempt * 1000;
        await new Promise((r) => setTimeout(r, delay));
        continue;
      }
      if (!resp.ok) throw new Error(`Request failed: ${resp.status}`);
      return resp.json();
    }
    throw new Error("Rate limit: max retries exceeded");
  }

  const data = await searchWithRetry({ query: "data breach notification" });
  ```
</CodeGroup>

<Tip>
  Smooth your request rate before you hit the ceiling. A small client-side delay between calls in a paging loop keeps you comfortably under the limit and avoids retries entirely.
</Tip>

## Troubleshooting

<AccordionGroup>
  <Accordion title="401 but my key is correct">
    Include the `Bearer` prefix: `Authorization: Bearer vq_key_...`. The key alone is rejected.
  </Accordion>

  <Accordion title="422 validation error">
    Set `Content-Type: application/json`. Check the `loc` field in the response to find the problematic field.
  </Accordion>
</AccordionGroup>

## Related

<CardGroup cols={2}>
  <Card title="Authentication" icon="key" href="/docs/api-guide/authentication">
    Create and manage your API key.
  </Card>

  <Card title="Best practices" icon="circle-check" href="/docs/api-guide/best-practices">
    Scope queries, cache coverage, and retry well.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.