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

# Handle API errors

> Read the status code and body of a failed DarknetSearch API call, tell a request you can fix from a limit or an outage, and decide whether to retry.

A failed call returns an HTTP status code and, almost always, a JSON body. The status code tells you what kind of problem it is. The body tells you which one. This page lists the bodies the API returns, word for word, and what to do about each.

Errors that only one endpoint returns are listed in that endpoint's guide.

### Read an error

Branch on the status code first, then read the body. The keys in the body depend on which part of the platform refused the call:

| Key | Returned by |
| - | - |
| `detail` | The API's own refusals: authentication, license, plan, allowances, capacity, timeouts and unavailable services. Also many of the services behind the endpoints. |
| Field names and `non_field_errors` | Request validation. Each key lists the problems with that field. |
| `message` | The two rate-limit `429`s, the `500` for unexpected errors, the `502` for data the API could not validate, and some report and account-setup responses |
| `details` | Attack Surface and Domain monitoring (SpoofGuard in the API reference) |
| `error` | Some parameter checks, Attack Surface reports and file exports |
| `error_message` | Filtered credentials export downloads |

To log a readable message, take the first of `detail`, `error_message`, `details`, `error` and `message` that the body contains. If it has none of them, the body is a set of field errors.

Some bodies add a machine-readable key: `resource` on allowance and capacity refusals, and `code` on some Attack Surface refusals. Branch on those keys when they are present. When you match a message, match its start, not the whole string.

> [!NOTE]
>
> A `4xx` from the service behind an endpoint is forwarded with its own keys. Do not rely on the value types inside it: numbers, `true`, `false` and `null` can arrive as strings such as `"25"`, `"True"`, `"False"` and `"None"`.

Not every error body is JSON. A path that does not exist returns an HTML page with `404`.

### The order of checks

The API checks each request in this order and stops at the first refusal:

1. **Path.** An unknown path returns `404` with an HTML page. A path missing its trailing `/` returns a `301` redirect.
2. **API key.** A missing, unknown, revoked or expired key returns `401`.
3. **Organization.** A request that names another organization returns `403`.
4. **License and plan feature.** `403`.
5. **Per-second rate limit and API Calls allowance.** `429`.
6. **Method.** A method the endpoint does not accept returns `405`.
7. **The endpoint itself.** A spent feature allowance (`429`), a full capacity slot (`409`), request validation (`400`), then the service behind the endpoint: its own `4xx`, or `502`, `503` and `504`. Endpoints differ in whether they validate the body before or after the allowance and capacity checks.

When `Content-Type` is exactly `application/json`, a body that does not parse is refused with `400` at step 3, before the license check and the rate limit.

Refusals at steps 1 to 4 never reach the rate limit. They carry no `X-RateLimit-*` headers and count toward none of your limits. From step 5 on, a response that is not a `2xx` uses none of your API Calls allowance and none of any feature allowance: a unit taken for the call is refunded. A response that passed the rate limit still counts toward the per-second limit. [Rate limits](https://wiki.darknetsearch.com/api/guides/rate-limits.md) and [Plans and quotas](https://wiki.darknetsearch.com/api/guides/plans-and-quotas) explain the limits.

### Branch on the status code

| Status | What it means | Retry? |
| - | - | - |
| `400` | The request is malformed, or the service behind the endpoint refused it | No. Fix the request. A few business rules clear with time. |
| `401` | The API key is missing, unknown, revoked or expired | No |
| `403` | The license, the plan or the organization does not allow the call | No |
| `404` | The path or the object does not exist | No |
| `405` | The endpoint does not accept this method | No |
| `409` | A capacity limit is full, or the object is not in a state that allows the call | Not until the state changes |
| `410` | A Filtered credentials export has expired or failed | No. Create a new export. |
| `415` | The body's `Content-Type` is not accepted | No |
| `422` | The service behind the endpoint refused a value that passed validation | No |
| `429` | A rate limit, an allowance or a capacity ceiling refused the call | Depends on the body. See [429](#429-too-many-requests). |
| `500` | An unexpected error | Yes, with backoff. For a `POST`, see the warning below. |
| `502` | The service behind the endpoint failed | Yes, with backoff. For a `POST`, see the warning below. |
| `503` | The service behind the endpoint is temporarily unavailable | Yes, with backoff. For a `POST`, see the warning below. |
| `504` | The service behind the endpoint timed out | Yes, with backoff. For a `POST`, see the warning below. |

The helper below retries only what can succeed on a retry: a per-second `429` on any method, and a `500`, `502`, `503` or `504` on a `GET`. It raises everything else with the status and the body attached, so your code can branch on them.

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

import requests

BASE_URL = "https://client-api.leak.center"
MESSAGE_KEYS = ("detail", "error_message", "details", "error", "message")


class DarknetSearchError(Exception):
    def __init__(self, status, body):
        self.status = status
        self.body = body
        message = next((body[key] for key in MESSAGE_KEYS if key in body), body)
        super().__init__(f"HTTP {status}: {message}")


def is_retryable(method, status, body):
    if status == 429:
        return body.get("retry_after_seconds") == 1  # per-second rate limit only
    return method == "GET" and status in (500, 502, 503, 504)


def call(method, path, attempts=5, **kwargs):
    headers = {"X-API-Key": os.environ["DARKNETSEARCH_API_KEY"]}
    for attempt in range(1, attempts + 1):
        response = requests.request(method, BASE_URL + path, headers=headers, timeout=120, **kwargs)
        if response.ok:
            return response
        try:
            body = response.json()
        except ValueError:
            body = response.text
        if not isinstance(body, dict):
            body = {"detail": body}
        if attempt == attempts or not is_retryable(method, response.status_code, body):
            raise DarknetSearchError(response.status_code, body)
        delay = float(response.headers.get("Retry-After", 2 ** attempt))
        time.sleep(delay + random.random())


try:
    call("POST", "/api/service/create_infra_search_request/", json={"search_term": "acme.com"})
except DarknetSearchError as error:
    if error.status == 429 and "resource" in error.body:
        print(f"The {error.body['resource']} allowance is spent for this {error.body['period']}.")
    else:
        raise
```

```javascript Node.js theme={"dark"}
const BASE_URL = "https://client-api.leak.center";
const MESSAGE_KEYS = ["detail", "error_message", "details", "error", "message"];

class DarknetSearchError extends Error {
  constructor(status, body) {
    const key = MESSAGE_KEYS.find((name) => name in body);
    super(`HTTP ${status}: ${JSON.stringify(key ? body[key] : body)}`);
    this.status = status;
    this.body = body;
  }
}

function isRetryable(method, status, body) {
  if (status === 429) return body.retry_after_seconds === 1; // per-second rate limit only
  return method === "GET" && [500, 502, 503, 504].includes(status);
}

async function call(method, path, { attempts = 5, ...init } = {}) {
  const headers = { "X-API-Key": process.env.DARKNETSEARCH_API_KEY, ...init.headers };
  for (let attempt = 1; ; attempt++) {
    const response = await fetch(BASE_URL + path, { ...init, method, headers });
    if (response.ok) return response;
    const text = await response.text();
    let body;
    try {
      body = JSON.parse(text);
    } catch {
      body = text;
    }
    if (body === null || typeof body !== "object" || Array.isArray(body)) body = { detail: body };
    if (attempt === attempts || !isRetryable(method, response.status, body)) {
      throw new DarknetSearchError(response.status, body);
    }
    const delay = Number(response.headers.get("Retry-After") ?? 2 ** attempt);
    await new Promise((resolve) => setTimeout(resolve, (delay + Math.random()) * 1000));
  }
}
```

> [!WARNING]
>
> **Check before you retry a call that starts a job.** After a `500`, `502`, `503` or `504` on a `POST` that starts a scan, a search or a report, list that feature's jobs before you send it again. The service behind the endpoint may have started the job before the error. A `504` means only that the API stopped waiting.

> [!NOTE]
>
> A `2xx` can still carry refusals. Quick search answers `201`, and `200` when you read the record again. Each source in it reports its own outcome, such as `not_entitled` or `quota_exceeded`. See [Quick search](https://wiki.darknetsearch.com/api/guides/wide-search.md).

### 400 Bad Request

Request validation answers with one key per field. Each key holds a list of problems:

```json theme={"dark"}
{
  "search_term": ["This field is required."]
}
```

A rule that spans several fields uses `non_field_errors`:

```json theme={"dark"}
{
  "non_field_errors": ["from_year_month cannot be after to_year_month."]
}
```

A body that is not valid JSON returns `{"detail": "JSON parse error - …"}`, followed by the parser's message.

> [!TIP]
>
> Send JSON bodies with `Content-Type: application/json`. Without that header, `curl -d` sends a form body. The API reads it as a form and reports every field as missing.

Other `400` bodies come from parameter checks in the API, such as `{"error": "filter_task_id parameter is required"}`, or from the service behind the endpoint. Some of those enforce a business rule that clears with time. Attack Surface, for example, refuses a new assessment while your organization already has the maximum number running or queued, and refuses an assessment equivalent to one started for the same domain in the last 30 minutes. Both answer `400` with a `details` message. See [Attack Surface assessments](https://wiki.darknetsearch.com/api/guides/attack-surface-assessments.md).

### 401 Unauthorized

| Body | Cause | What to do |
| - | - | - |
| `{"detail": "Authentication credentials were not provided."}` | The request has no `X-API-Key` header, or the header is empty. | Send the key in `X-API-Key` on every request. |
| `{"detail": "Invalid or expired API key."}` | The key is unknown, revoked or past its expiry. | Check that the header carries the key alone, with no prefix such as `Bearer` and no quotes. If it does, the key no longer works: create a new one. Retrying with the same key fails again. |
| `{"detail": "Given token not valid for any token type", "code": "token_not_valid", "messages": [...]}` or `{"detail": "Authorization header must contain two space-delimited values", "code": "bad_authorization_header"}` | The request carries an `Authorization` header, for example the API key sent with a `Bearer` prefix. The API reads that header before `X-API-Key`. | Send the key in `X-API-Key` and remove the `Authorization` header. |

A `401` with any other body comes from the service behind the endpoint, not from your key. Retry a `GET` later with backoff, and contact support if it repeats.

[Authentication](https://wiki.darknetsearch.com/api/guides/authentication.md) explains how to create, send and replace keys.

### 403 Forbidden

| Body | Cause | What to do |
| - | - | - |
| `{"detail": "Your license is not active."}` | Your organization has no license in force. A client workspace also needs the license of the organization that manages it in force. | Do not retry. Calls succeed again once the license is back in force. |
| `{"detail": "This feature is not included in your plan."}` | The endpoint needs a plan feature your plan does not include. On these endpoints, a license that is not in force returns this message too. | See [Plan features](https://wiki.darknetsearch.com/api/guides/plans-and-quotas#plan-features). |
| `{"detail": "You don't have access to this organization"}` | The request names an organization other than the key's own, for example in an `org_id` parameter kept from an older integration. | Remove it. A key always acts as its own organization. See [Client organizations](https://wiki.darknetsearch.com/api/guides/authentication.md#client-organizations). |

To tell a plan refusal from a lapsed license, call [`get_api_call_usage`](https://client-api.leak.center/scalar-docs/#tag/general-endpoints/GET/api/service/get_api_call_usage/) for the current month. It has no plan feature check, so it returns `Your license is not active.` when the license is the cause. [`get_direct_org_info`](https://client-api.leak.center/scalar-docs/#tag/organization-settings/GET/api/service/get_direct_org_info/) answers even without a license in force, which confirms the key itself works.

On monitor create and update, a `service` that is missing or not recognized also returns `This feature is not included in your plan.` See [Monitors](https://wiki.darknetsearch.com/api/guides/monitoring-rules.md).

A `403` with any other body comes from the service behind the endpoint. The endpoint's guide describes it.

### 404 Not Found

| Body | Cause |
| - | - |
| `{"detail": "Not found."}` or `{"detail": "No … matches the given query."}` | The ID in the path does not exist, or belongs to another organization. |
| `{"detail": "Invalid page."}` | `page` is past the last page of a list. Stop paging. |
| An HTML page | The path does not exist. |
| Any other JSON body | The service behind the endpoint could not find the object. For example, file exports answer `{"error": "Export does not exist"}`. |

Copy each path exactly as the [API reference](https://client-api.leak.center/scalar-docs/) shows it. Most paths end in `/`, and a few do not. A request that drops the trailing `/` gets a `301` redirect to the path with it. Most HTTP clients follow the redirect and resend a `POST` as a `GET`, so the call you meant never runs.

### 409 Conflict

A `409` with `resource` and `limit` means a capacity slot is full. Nothing is created:

```json theme={"dark"}
{
  "detail": "You have reached your Monitored Assets limit of 25.",
  "resource": "monitored_assets",
  "limit": 25
}
```

| `resource` | Slots are held by | Refused call |
| - | - | - |
| `monitored_assets` | [Monitors](https://wiki.darknetsearch.com/api/guides/monitoring-rules.md), and domains under [Domain monitoring](https://wiki.darknetsearch.com/api/guides/domain-monitoring-scans.md) | Creating a monitor, or starting a domain scan |
| `automated_reports` | Active [credential comparison report](https://wiki.darknetsearch.com/api/guides/reports-credential-comparison.md) schedules | Creating an active schedule, or activating one |

`limit` is your organization's slot count, packs included. A `limit` of `0` means your plan includes none. Retrying fails until you free a slot or add a pack. [Capacity slots](https://wiki.darknetsearch.com/api/guides/plans-and-quotas#capacity-slots) explains how each slot is freed.

Other `409`s mean the object is not in a state that allows the call. Most come from the service behind the endpoint:

| Call | Body | What to do |
| - | - | - |
| Create an [Attack Surface report](https://wiki.darknetsearch.com/api/guides/attack-surface-reports.md) | `error`. Activity and comparison reports add a `code` such as `scan_not_completed`. Snapshot reports send `error` only. | The assessment cannot produce that report yet, or cannot cover the requested period. Branch on `code` when it is present, and retry after the assessment completes. While the assessment is still running, the full report and most targeted reports answer `400` with `error` instead. |
| Read or download an Attack Surface report | `{"code": "report_regeneration_required", "error": "This report predates the current privacy contract and cannot be read or downloaded. Regenerate the report.", "regeneration_required": true}` | Create the report again. |
| Retry a [takedown](https://wiki.darknetsearch.com/api/guides/domain-takedowns.md)'s preparation | `{"details": "The takedown draft preparation cannot be retried yet."}` | Read the takedown's status before you retry. |
| Delete a [credential comparison report](https://wiki.darknetsearch.com/api/guides/reports-credential-comparison.md) | `{"message": "Report is processing and cannot be deleted"}` | Delete it after it finishes. |
| Download a [Filtered credentials](https://wiki.darknetsearch.com/api/guides/credentials-filtered.md) export | `{"error_message": "Export is still processing (status: pending)", "status": "pending"}` | Poll the export until it is ready. |

A Filtered credentials export that has expired or failed returns `410` from the same download call, with an `error_message`. Exports expire 24 hours after creation. Create a new one.

### 422 Unprocessable Entity

A `422` comes from the service behind the endpoint. It refused a value that passed the API's own validation, such as an impossible date. The API reference lists it for Credential Check and Ransomwatch endpoints.

Credential Check answers `{"detail": ...}`, where `detail` is a message or a list of field errors. See [Credential Check errors](https://wiki.darknetsearch.com/api/guides/credentials-check.md#errors). Do not retry the same value.

### 429 Too Many Requests

The API returns four kinds of `429`. Tell them apart by the body:

| The body has | Cause | What to do |
| - | - | - |
| `"message": "Rate limit exceeded"` and `"retry_after_seconds": 1` | The per-second rate limit | Wait for `Retry-After` (1 second), then retry. |
| `"message": "Rate limit exceeded"` with `limit`, `remaining` and `reset_at` | The month's API Calls allowance is used up | Queue API-key requests until `reset_at`. |
| `detail`, `resource`, `period` and `limit` | A feature allowance is used up | Do not retry before the `period` resets or a pack is added. |
| `details` and `"code": "heavy_scan_capacity_full"` | Attack Surface capacity for heavy assessments is full across the platform | Retry later, or start a lighter assessment. |

The two rate-limit `429`s carry a `Retry-After` header. The other two do not. [Rate limits](https://wiki.darknetsearch.com/api/guides/rate-limits.md#when-you-exceed-a-limit) shows both rate-limit bodies in full. A `429` with any other body comes from the service behind the endpoint, and the endpoint's guide describes it.

A feature allowance refusal names the allowance in `resource`:

```json theme={"dark"}
{
  "detail": "You have reached your Infra Search Scan limit for this month.",
  "resource": "infra_search_scan",
  "period": "month",
  "limit": 10
}
```

`period` is `month`, or `year` for takedowns. `limit` is your allowance for that window, packs included. [Metered allowances](https://wiki.darknetsearch.com/api/guides/plans-and-quotas#metered-allowances) lists every allowance.

The Attack Surface refusal comes from the assessment service:

```json theme={"dark"}
{
  "details": "Deep scan capacity is full (2 heavy scans running). Please try again shortly or use a lighter scan profile (quick/standard).",
  "code": "heavy_scan_capacity_full"
}
```

An assessment is heavy when it enables `perform_port_scan`, `active_vuln_scan` or `dns_typosquatting_enabled`, or sets `domain_mode` to `dual_domain` or `all`. A full assessment enables port scanning and typosquatting by default. A targeted assessment is heavy too when its report type is `domain_spoofing_risk` or `application_security`, or `infrastructure_risk` with port scanning left on, which is the default. The assessment unit is refunded. See [Attack Surface assessments](https://wiki.darknetsearch.com/api/guides/attack-surface-assessments.md).

### 500, 502, 503 and 504

| Status | Body | Cause | What to do |
| - | - | - | - |
| `500` | `{"message": "Internal server error, please try again!"}` | An unexpected error in the API | Retry with backoff. If it repeats, contact support with the time, the endpoint and the request. |
| `502` | `{"detail": "The requested service is temporarily unavailable."}` | The service behind the endpoint failed, could not be reached, or sent a reply that is not JSON | Retry with backoff. |
| `502` | `{"message": "An unexpected error occurred. Please try again later or contact support."}` | The service behind the endpoint answered with data the API could not validate | Retry later. If it repeats for the same request, contact support. |
| `503` | `{"detail": "The requested service is temporarily unavailable."}` | The service behind the endpoint reported that it is temporarily unavailable | Retry with backoff. |
| `504` | `{"detail": "The requested service timed out."}` | The service behind the endpoint did not answer in time, or reported a timeout of its own | Retry with backoff. |

A few endpoints word their `502` differently, or pass the service's own `5xx` through. Credential comparison reports, for example, answer `{"message": "Please try again later..."}`, and Credential Check passes the lookup service's error through. Their guides list them. Handle every `5xx` the same way.

Back off between retries: wait 2, 4, 8 and 16 seconds, add up to a second of random delay, and stop after 5 attempts. See the [warning above](#branch-on-the-status-code) before you retry a call that starts a job.

### Statuses the reference does not list

Each operation in the [API reference](https://client-api.leak.center/scalar-docs/) lists the statuses it documents. The rules on this page apply to every operation, including those whose entry leaves a status out:

* **`401`** applies to every endpoint that needs a key. Several entries list none, including Quick search, Paste search and report schedules.
* **`403`** for a license that is not in force applies to almost every endpoint. About half of the entries list no `403`.
* **`409`** for a full capacity slot applies to [`alert_system_alert_create_alert_rule`](https://client-api.leak.center/scalar-docs/#tag/alert-system/POST/api/service/alert_system_alert_create_alert_rule/), [`create_report_schedule`](https://client-api.leak.center/scalar-docs/#tag/report/POST/api/service/create_report_schedule/) and [`update_report_schedule`](https://client-api.leak.center/scalar-docs/#tag/report/PATCH/api/service/update_report_schedule/\{schedule_id}/). Their entries list no `409`.
* **`429`** for the per-second rate limit applies to every endpoint that needs a key. Several entries list none, including Quick search, Telegram search, Paste search, report schedules and the usage and organization endpoints.
* **`503`** and **`504`** apply to every endpoint that calls a service behind it. Most entries list only `502`.
* **`405`** and **`415`** are never listed. `405` returns `{"detail": "Method \"GET\" not allowed."}`, naming the method you sent. `415` returns `{"detail": "Unsupported media type \"text/plain\" in request."}`, naming the `Content-Type` you sent. The API accepts JSON, form and multipart bodies.

One status in the reference never means what its label suggests:

* **`402 Payment Required`** appears on four Discord search entries. There are no credits. A plan refusal is a `403`, and a spent allowance is a `429`.

The reference gives every `429` a generic description. On a metered endpoint, it can be any of the four refusals in [429 Too Many Requests](#429-too-many-requests), so read the body before you retry.

### Endpoints

| Action | Endpoint |
| - | - |
| Confirm that a key works and see which organization it acts as | [`GET /api/service/get_direct_org_info/`](https://client-api.leak.center/scalar-docs/#tag/organization-settings/GET/api/service/get_direct_org_info/) |
| Count your organization's requests by day and status code | [`GET /api/service/get_api_call_usage/`](https://client-api.leak.center/scalar-docs/#tag/general-endpoints/GET/api/service/get_api_call_usage/) |


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