> ## Documentation Index
> Fetch the complete documentation index at: https://cortex-e852fafe-docs-pro-2457-api-response-cleanup.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Error Responses

> Response envelope, HTTP status codes, error codes, and retry patterns.

## Response envelope

HydraDB core endpoints (`/databases`, `/context/*`, `/query`, `/feedback`, and `/webhooks/indexing*`) use the same top-level envelope for successful and failed requests. Connector endpoints (`/connectors*`) and `PATCH /databases/{database}/metadata-schema` return their success body without this envelope, but their errors use it too.

```json theme={"dark"}
{
  "success": false,
  "data": null,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Request validation failed"
  },
  "meta": {
    "request_id": "9d13aef4-02f4-4e73-8c62-4c2601d04f9d",
    "latency_ms": 4.8
  }
}
```

| Field | Description |
| - | - |
| `success` | `false` for errors. |
| `data` | `null` for error responses. One exception: a failed `DELETE /context` keeps its per-ID results under `data`. |
| `error.code` | Machine-readable code for programmatic handling. |
| `error.message` | Human-readable explanation of what failed. |
| `meta.request_id` | Request identifier. Include it when contacting support. |
| `meta.latency_ms` | Server-side processing time in milliseconds. |
| `detail` | Deprecated copy of the error (`detail.error_code`, `detail.message`) kept for older clients. Read `error` instead. |

<Info>
  Use `error.code` for branching and log `meta.request_id` for every failed request. The HTTP status tells you the class of failure; the error code tells you what to do.
</Info>

## HTTP status codes

| Code | Meaning | Retry? |
| - | - | - |
| `400` | Invalid parameters or malformed request | No |
| `401` | Missing, expired, or invalid API key | No |
| `402` | Your plan does not allow the request, for example one connector too many | No. Upgrade |
| `403` | Authenticated, but not permitted for the resource, or the plan's database limit is reached | No |
| `404` | Database, source, memory, or related resource was not found | No |
| `409` | Conflict: the database already exists, a source is still indexing when you delete it, or a schema field conflicts | Only `SOURCE_PROCESSING`, after `Retry-After` |
| `413` | Request body or upload too large | No |
| `415` | Unsupported `Content-Type`, such as a `POST /context/ingest` body that is neither a multipart form nor JSON | No |
| `422` | Well-formed request that failed validation, or a query sent before the database finished provisioning | Only `TENANT_INFRA_NOT_READY`, once the database is ready |
| `429` | Rate limit exceeded | Yes, with backoff |
| `500` | Internal server error | Yes, with backoff |
| `503` | Temporary service unavailability | Yes, with backoff |

## Common error codes

| Code | Typical status | Meaning |
| - | - | - |
| `INVALID_INPUT` | `400` | The default for a bad request: a required parameter is missing, malformed, or mutually incompatible with another parameter. Also used for `413` and `415`. |
| `UNAUTHORIZED` | `401` | The `Authorization: Bearer <your_api_key>` header is missing or invalid. |
| `PAYMENT_REQUIRED` | `402` | A plan limit refused the write, for example the connector count. |
| `FREE_PLAN_DEPRECATED` | `402` | The workspace is on the deprecated Free plan. The message and `detail.upgrade_url` carry the upgrade link. |
| `FORBIDDEN` | `403` | The API key is valid but does not have access to the requested route or resource, or the plan's database limit prevents the operation. |
| `DATABASE_ALREADY_EXISTS` | `409` | `POST /databases` received a `database` (formerly `tenant_id`) that is already in use. |
| `DATABASE_NOT_FOUND` | `404` | The requested database does not exist or is not visible to the current API key. |
| `NOT_FOUND` | `404` | The requested resource does not exist, for example a source ID in `DELETE /context`. |
| `SOURCE_PROCESSING` | `409` | `DELETE /context` hit a source that is still indexing. Retry after `Retry-After`. |
| `VALIDATION_ERROR` | `400` or `422` | One or more fields failed semantic validation: `400` on `POST /query` for a metadata filter that does not fit the field's type, `422` on connectors for credentials that fail validation. |
| `TENANT_INFRA_NOT_READY` | `422` | `POST /query` reached a database that is still provisioning. Poll [Database Status](/api-reference/v2/endpoint/tenant-status) until `ready_for_ingestion` is `true`, then retry. |
| `CORPUS_TYPE_UNSUPPORTED` | `400` | The `type` value does not fit this endpoint or database, for example `type: "all"` on ingest. |
| `RATE_LIMITED` | `429` | The API key exceeded its current rate limit. |
| `INTERNAL_ERROR` | `500` | HydraDB hit an unexpected server-side error. A rare auth-layer failure reports `INTERNAL_SERVER_ERROR` instead. |
| `SERVICE_UNAVAILABLE` | `503` | A dependency is temporarily unavailable or the service is under load. |

<Note>
  Endpoint pages list the most common codes for that operation. New codes may be added over time, so clients should handle unknown `error.code` values gracefully.
</Note>

<Note>
  `PATCH /databases/{database}/metadata-schema` currently returns `INTERNAL_ERROR` for its `400` and `409` errors too, so branch on the HTTP status there.
</Note>

<Note>
  **Deprecated `/tenants` routes keep their pre-rename error codes.** For backward compatibility, a request for a database that does not exist returns `NOT_FOUND` on the deprecated `/tenants` routes (not `DATABASE_NOT_FOUND`), and a duplicate on `POST /tenants` returns `INVALID_INPUT` (not `DATABASE_ALREADY_EXISTS`), whereas the canonical `/databases` routes return `DATABASE_NOT_FOUND` and `DATABASE_ALREADY_EXISTS` as shown above. The HTTP status is identical on both. The route decides the code, so the old `tenant_id` field sent to a `/databases` route still gets the new codes. See [Migrating from `tenant_id` and `sub_tenant_id`](/essentials/v2/multi-tenant#7-migrating-from-the-legacy-tenant-and-sub-tenant-fields).
</Note>

## Ingestion error codes

Asynchronous ingestion failures surface a numeric `E####` code in the `error_code` field of [`GET /context/status`](/api-reference/v2/endpoint/source-status) responses and `indexing.status_changed` [webhook](/essentials/v2/webhooks) payloads. Unlike the HTTP `error.code` values above (which describe why a *request* was rejected), these describe why a specific *item* failed to index.

Many storage- and capacity-related ingestion errors are **transient**: the pipeline retries them automatically with backoff, and they typically self-resolve within minutes. A code appearing in `error_code` does not by itself mean the item has failed permanently; only treat an item as a real failure once it reaches the terminal `errored` status.

| Code | Meaning | Severity |
| - | - | - |
| `E1002` | The file format is not supported. Returned in the [`POST /context/ingest`](/api-reference/v2/endpoint/ingest-context) response itself, per file, before anything is queued. User message: *"This file format isn't supported. Please upload a PDF, Office document (Word, Excel, PowerPoint), image, CSV or text file."* | **Terminal** (not retried) |
| `E6001` | Vector-store storage/indexing error while persisting processed data. The pipeline retries automatically and it usually clears within minutes. User message: *"Failed to store the processed data. Please try again. If the issue persists, contact [support@hydradb.com](mailto:support@hydradb.com)."* | **Transient** (retryable) |

<Note>
  `E1002` is the one ingestion code that does **not** follow the polling advice above. The file is rejected at upload, so it never enters the pipeline and never gets a status record. Polling [`/context/status`](/api-reference/v2/endpoint/source-status) for it returns `FILE_NOT_FOUND`, not `E1002`. Read `error_code` on each item in the upload response instead. See [Supported file formats](/api-reference/v2/endpoint/ingest-context#supported-file-formats) for what is accepted, and note that one rejected file does not affect the other files in the same request.
</Note>

## Retry pattern

Retry only transient failures: `429`, `500`, and `503`. Use exponential backoff with jitter and keep retries bounded. Two other codes clear on their own, so wait instead of backing off blindly: `409 SOURCE_PROCESSING` (wait for `Retry-After`) and `422 TENANT_INFRA_NOT_READY` (wait until the database is ready).

<CodeGroup>
  ```typescript TypeScript SDK theme={"dark"}
  import { HydraDBError } from "@hydradb/sdk";

  async function withRetry<T>(
    operation: () => Promise<T>,
    maxRetries = 3
  ): Promise<T> {
    for (let attempt = 0; attempt <= maxRetries; attempt++) {
      try {
        return await operation();
      } catch (error) {
        if (!(error instanceof HydraDBError)) throw error;

        const retryable = [429, 500, 503].includes(error.statusCode);
        if (!retryable || attempt === maxRetries) throw error;

        const baseDelayMs = 2 ** attempt * 1000;
        const jitterMs = Math.floor(Math.random() * 250);
        await new Promise((resolve) => setTimeout(resolve, baseDelayMs + jitterMs));
      }
    }

    throw new Error("unreachable");
  }

  const result = await withRetry(() =>
    client.query({
          database: "my_first_database",
          query: "What are the pricing tiers?",
          type: "knowledge",
    })
  );
  ```

  ```python Python SDK theme={"dark"}
  import random
  import time

  from hydra_db.core.api_error import ApiError

  def with_retry(operation, max_retries=3):
      for attempt in range(max_retries + 1):
          try:
              return operation()
          except ApiError as exc:
              retryable = exc.status_code in (429, 500, 503)
              if not retryable or attempt == max_retries:
                  raise

              delay = (2 ** attempt) + random.uniform(0, 0.25)
              time.sleep(delay)

  result = with_retry(lambda: client.query(
      database="my_first_database",
      query="What are the pricing tiers?",
      type="knowledge",
  ))
  ```

  ```typescript fetch theme={"dark"}
  async function hydraRequest(url: string, options: RequestInit, maxRetries = 3) {
    for (let attempt = 0; attempt <= maxRetries; attempt++) {
      const response = await fetch(url, options);
      if (response.ok) return response.json();

      const retryable = [429, 500, 503].includes(response.status);
      if (!retryable || attempt === maxRetries) {
        throw await response.json();
      }

      const baseDelayMs = 2 ** attempt * 1000;
      const jitterMs = Math.floor(Math.random() * 250);
      await new Promise((resolve) => setTimeout(resolve, baseDelayMs + jitterMs));
    }
  }
  ```
</CodeGroup>

## Handling errors

<CodeGroup>
  ```typescript TypeScript SDK theme={"dark"}
  import { HydraDBError } from "@hydradb/sdk";

  try {
    await client.context.ingest({
      type: "knowledge",
      database: "my_first_database",
      documents: [{ path: "policy.pdf", filename: "policy.pdf" }],
    });
  } catch (error) {
    if (error instanceof HydraDBError) {
      // error.body is typed `unknown`; it holds the raw snake_case error envelope.
      const body = error.body as
        | { error?: { code?: string }; meta?: { request_id?: string } }
        | undefined;
      const code = body?.error?.code;
      const requestId = body?.meta?.request_id;

      if (code === "DATABASE_NOT_FOUND") {
        // Create or select a valid database before ingesting.
      } else if (error.statusCode === 429) {
        // Retry with backoff.
      } else {
        console.error({ code, requestId });
        throw error;
      }
    } else {
      throw error;
    }
  }
  ```

  ```python Python SDK theme={"dark"}
  from hydra_db.core.api_error import ApiError

  try:
      client.context.ingest(
          type="knowledge",
          database="my_first_database",
          documents=[("policy.pdf", open("policy.pdf", "rb"), "application/pdf")],
      )
  except ApiError as exc:
      # The machine-readable code lives in the response envelope, not on the
      # exception: ApiError carries status_code, headers and the parsed body.
      error_code = (exc.body or {}).get("error", {}).get("code")

      if error_code == "DATABASE_NOT_FOUND":
          # Create or select a valid database before ingesting.
          pass
      elif exc.status_code == 429:
          # Retry with backoff.
          pass
      else:
          raise
  ```
</CodeGroup>

## Troubleshooting

### Authentication failures

Send exactly one `Authorization` header:

```bash theme={"dark"}
Authorization: Bearer <your_api_key>
```

Also send `API-Version: 2` on raw HTTP requests. The official SDKs set the version header automatically.

### Database not found after creation

Database creation is asynchronous. After `POST /databases`, poll [`GET /databases/status`](/api-reference/v2/endpoint/tenant-status) until `infra.scheduler_status`, `infra.graph_status`, `infra.vectorstore_status.knowledge`, and `infra.vectorstore_status.memories` are all `true` (`infra.ready_for_ingestion` combines them). A query sent earlier returns `422 TENANT_INFRA_NOT_READY`.

### Ingestion validation errors

Common causes:

* `document_metadata` length does not match the `documents` array length.
* `app_knowledge`, `memories`, or `document_metadata` was sent as an object instead of a JSON-stringified multipart field.
* A memory item has neither `text` nor `user_assistant_pairs`.
* A typed metadata value does not match the database metadata schema.

### Empty query results

Empty results are not always errors. Check these first:

* Context status may still be `queued` or `processing`; poll [`GET /context/status`](/api-reference/v2/endpoint/source-status).
* `metadata_filters` may be too restrictive or may target the wrong metadata namespace.
* The query may be scoped to the wrong `database` or `collection` (formerly `tenant_id` / `sub_tenant_id`).
* The `type` value may exclude the store you need. Use `type: "all"` when combining knowledge and memories in `POST /query`.

## Related sections

* [API Reference](/api-reference/v2): endpoint inventory and conventions
* [Ingestion Status](/api-reference/v2/endpoint/source-status): async ingestion state
* [Query](/api-reference/v2/endpoint/query): retrieval parameters and response shape


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