# Errors (https://webmetic.de/docs/en/api/fehler)

The API reports every error as JSON with a status code, a fixed error code, and a readable text.

## Error response format [#error-response-format]

All public endpoints answer an error in the same format. One example is a request without an API key:

```bash
curl "https://hub.webmetic.de/company-sessions?domain=your-website.com"
```

The API answers with status `403`:

```json
{
  "detail": {
    "error": "API_KEY_MISSING",
    "message": "Send your API key in the Authorization header."
  }
}
```

- **`detail.error`**: the error code in capitals. Check this field in your code.
- **`detail.message`**: an English text for people, for example for your log. Don't check this text in code, because it can change.

This format doesn't apply to the [Data Layer](https://webmetic.de/docs/en/data-layer), which answers with a shorter body.

Some errors contain more fields in `detail`:

| Field | Comes with | Meaning |
| --- | --- | --- |
| `errors` | `VALIDATION_ERROR` | List of the missing or invalid parameters and fields. Each entry gives the place in `loc`, the problem in `msg`, and the kind of error in `type` |
| `feature` | `PLAN_REQUIRED`, `FEATURE_DISABLED` | The feature your plan doesn't include or that is turned off |
| `required_tier` | `PLAN_REQUIRED` | The plan the feature needs |
| `current_tier` | `PLAN_REQUIRED` | Your current plan |
| `required` | `INSUFFICIENT_CREDITS` | How many credits the action needs |
| `available` | `INSUFFICIENT_CREDITS` | How many credits your account has |
| `website` | `WEBSITE_NOT_AUTHORIZED`, `SEGMENT_WEBSITE` | The website in question |
| `limit` | `WEBHOOK_LIMIT_REACHED` | The maximum number of webhooks for your account |

The API always reports problems with the key as `403`, never as `401`. For `RATE_LIMITED`, `CONCURRENT_UPDATE`, `PROVIDER_UNAVAILABLE`, `PROVIDER_ERROR`, and `INTERNAL_ERROR`, a new attempt can work because the error is temporary. For all other errors, change the request, the account, or the plan first.

This format applies to the API endpoints. For a mistyped path outside them, the API answers `{"detail": "Not Found"}`. So check in your code whether `detail` is an object.

## Common errors [#common-errors]

Any endpoint can return these errors. The key codes apply to the **Visitor Intelligence** key, which you also use for `/contacts/*` and `/webhooks`. Enrichment has its own codes.

| Status | Code | Meaning | What you do |
| --- | --- | --- | --- |
| 403 | `API_KEY_MISSING` | The request has no `Authorization` header | Send the key in the `Authorization` header |
| 403 | `API_KEY_INVALID` | The key has the wrong format, for example with `Bearer` in front | Send the key unchanged, without `Bearer`, spaces, or quotes |
| 403 | `API_KEY_NOT_FOUND` | No account has this key, for example after generating a new one or with the Enrichment key | Copy the current key under [**Menu** > **Integrations** > **API**](https://app.webmetic.de/?menu=api_details) |
| 404 | `NOT_FOUND` | The path doesn't exist | Check the address in the [reference](https://webmetic.de/docs/en/api/referenz/visitor-intelligence/company-sessions) |
| 405 | `METHOD_NOT_ALLOWED` | The path doesn't accept this method, for example `POST` instead of `GET` | Use the method from the reference |
| 422 | `VALIDATION_ERROR` | A parameter or field is missing or invalid, for example `page` below 1 or `page_size` above 10,000 | Fix the places listed in `errors`. For details, see [Pagination](https://webmetic.de/docs/en/api/paginierung) |
| 500 | `INTERNAL_ERROR` | An error at Webmetic, with no details in the answer | Try again later. If the error stays, write to [support](https://webmetic.de/docs/en/konto/support) |

## Visitor Intelligence [#visitor-intelligence]

The eight visit endpoints, for example `/company-sessions` and `/company`, also return these errors.

| Status | Code | Meaning | What you do |
| --- | --- | --- | --- |
| 400 | `INVALID_DATE` | The API can't read `from_date` or `to_date`, for example `-3 weeks` or `31.02.2026` | Use, for example, `2026-10-01` or `-7 days`. For details, see [Date ranges and time zones](https://webmetic.de/docs/en/api/zeitraeume-und-zeitzonen) |
| 403 | `API_KEY_NOT_AUTHORIZED` | The `domain` doesn't belong to your account or is spelled differently | Enter the domain exactly as it appears under [**Menu** > **Websites**](https://app.webmetic.de/?menu=websites) |
| 429 | `RATE_LIMITED` | More than one request per second to the same endpoint | Wait the number of seconds in `Retry-After`, then send the request again. For details, see [Rate limits](https://webmetic.de/docs/en/api/anfragelimits) |

On the Free plan, the API reports no error for hidden companies but returns the row with `blurred: true` and only a few fields. For details, see [Plans](https://webmetic.de/docs/en/konto/plaene).

## Enrichment [#enrichment]

The endpoints under `/enrichment/*` use the **Enrichment** key.

| Status | Code | Meaning | What you do |
| --- | --- | --- | --- |
| 400 | `INVALID_DOMAIN` | No domain can be read from the input to `GET /enrichment/company` | Enter a domain such as `your-website.com` |
| 403 | `ENRICHMENT_KEY_MISSING` | The request has no `Authorization` header | Send the Enrichment key in the `Authorization` header |
| 403 | `ENRICHMENT_KEY_INVALID` | The key has the wrong format | Send the key unchanged, without `Bearer` |
| 403 | `ENRICHMENT_KEY_NOT_FOUND` | No account has this Enrichment key, for example with the Visitor Intelligence key | Copy the current **Enrichment** key under [**Menu** > **Integrations** > **API**](https://app.webmetic.de/?menu=api_details) |
| 403 | `PLAN_REQUIRED` | New lists, new domains, and single lookups need the Growth plan. Reading and exporting still work, except with `ACCOUNT_NOT_ELIGIBLE` | Switch to the plan in `required_tier`, see [Plans](https://webmetic.de/docs/en/konto/plaene) |
| 403 | `ACCOUNT_NOT_ELIGIBLE` | Your account can't use Enrichment right now, for example after a cancellation | Write to [support](https://webmetic.de/docs/en/konto/support). A new attempt changes nothing |
| 404 | `LIST_NOT_FOUND` | The list doesn't exist or belongs to another account | Use the ID of a list from `GET /enrichment/lists` |
| 422 | `VALIDATION_ERROR` | For example `domain` under 3 characters, more than 5,000 domains per call, or an unknown value in `status` | Fix the parameter. Spread many domains over several calls |

`GET /enrichment/company` also answers with `202` and `404`, which are not errors. `202` means the result isn't ready yet, and `404` with `"status": "not_found"` means there is no company data for this domain. For details, see [Asynchronous results](https://webmetic.de/docs/en/api/asynchrone-ergebnisse).

## Contacts [#contacts]

The endpoints under `/contacts/*` use the **Visitor Intelligence** key. If a call ends with an error, Webmetic deducts no credits. For details, see [Credits in the API](https://webmetic.de/docs/en/api/credits).

| Status | Code | Meaning | What you do |
| --- | --- | --- | --- |
| 402 | `INSUFFICIENT_CREDITS` | Your credits aren't enough. Webmetic deducts nothing and unlocks nothing | Buy more [credits](https://webmetic.de/docs/en/credits) in the dashboard and repeat the call |
| 403 | `CONTACTS_SETUP_REQUIRED` | Your account hasn't set up contacts yet. This affects `search`, `reveal`, and `find` | Set up contacts once, see [Set up contacts](https://webmetic.de/docs/en/ansprechpartner/einrichten) |
| 403 | `COMPANY_NOT_IDENTIFIED` | The `company_id` doesn't belong to a company that visited your websites | Use a `company_id` from `/company-sessions` or `/company` |
| 403 | `PLAN_REQUIRED` | The company is hidden on the Free plan | Pick a company you can see, or switch plans |
| 403 | `FEATURE_DISABLED` | Your partner turned off the **Data profiles** feature for your account | Ask your partner to enable the feature, see [Choose features for customers](https://webmetic.de/docs/en/partner/funktionen-fuer-kunden) |
| 404 | `COMPANY_WITHOUT_WEBSITE` | This company has no website on file | The API returns no contacts for this company |
| 404 | `CONTACT_NOT_FOUND` | The contact isn't from your search, can no longer be found, or may not be delivered | Search again and use the new result |
| 409 | `CONCURRENT_UPDATE` | The same unlock is already running, or another deduction ran at the same moment | Repeat the call after a short wait. Webmetic never deducts twice |
| 502 | `PROVIDER_UNAVAILABLE` | The data source is temporarily unreachable | Try again in a few minutes |
| 502 | `PROVIDER_ERROR` | The request to the data source failed | Try again later. If the error stays, write to [support](https://webmetic.de/docs/en/konto/support) |

## Webhooks [#webhooks]

The endpoints under `/webhooks` use the **Visitor Intelligence** key. For details, see [Webhooks](https://webmetic.de/docs/en/api/webhooks).

| Status | Code | Meaning | What you do |
| --- | --- | --- | --- |
| 400 | `INVALID_URL` | The URL is empty, longer than 2,048 characters, not https, contains credentials, or has no valid host or port | Use a public https URL without credentials |
| 400 | `PRIVATE_ADDRESS` | The host points to a private or reserved address | Use a server that is reachable from the internet |
| 400 | `UNRESOLVABLE_HOST` | The host can't be resolved | Check the host name and its DNS record |
| 400 | `SEGMENT_REQUIRED` | The `segment.entered` event needs a `segment_id` | Send the `segment_id` |
| 400 | `SEGMENT_WEBSITE` | The segment belongs to a website the webhook doesn't cover | Add the website to `websites` |
| 400 | `NO_WEBSITE` | Your account has no website yet | Add a [website](https://webmetic.de/docs/en/konto/websites) first |
| 400 | `UNKNOWN_EVENT` | `GET /webhooks/sample` doesn't know the event | Use an event name from the [reference](https://webmetic.de/docs/en/api/referenz/webhooks/sample) |
| 403 | `WEBSITE_NOT_AUTHORIZED` | A website in `websites` or `website` doesn't belong to your account | Enter the website as it appears in your account |
| 404 | `WEBHOOK_NOT_FOUND` | The webhook doesn't exist or belongs to another account | Use the ID of a webhook from `GET /webhooks` |
| 404 | `SEGMENT_NOT_FOUND` | The segment doesn't exist | Check the `segment_id` |
| 409 | `WEBHOOK_LIMIT_REACHED` | Your account already has the maximum number of webhooks. `limit` gives the number | Delete webhooks you no longer need |

## Error handling in code [#error-handling-in-code]

The example reads the code from `detail.error` when an error comes back, and it also handles a text in `detail`. For `RATE_LIMITED` and `CONCURRENT_UPDATE`, it waits the seconds in `Retry-After` and tries up to three times. If the header is missing, it waits only briefly. It reports all other errors with status, code, and text.

```js tab="JavaScript" tab-group="lang"
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const RETRY = ['RATE_LIMITED', 'CONCURRENT_UPDATE'];

async function callWebmetic(url, attempts = 3) {
  for (let i = 1; i <= attempts; i++) {
    const res = await fetch(url, {
      headers: { Authorization: 'wmtc_YOUR_API_KEY' },
    });
    if (res.ok) return res.json();

    const { detail } = await res.json();
    // Paths outside the API answer {"detail": "Not Found"}
    const known = detail !== null && typeof detail === 'object';
    const error = known ? detail.error : `HTTP_${res.status}`;
    const message = known ? detail.message : String(detail);
    // Temporary errors: wait and try again
    if (RETRY.includes(error) && i < attempts) {
      const retryAfter = Number(res.headers.get('Retry-After'));
      await sleep(retryAfter > 0 ? retryAfter * 1000 : 1000 * i); // Retry-After first, else the backoff
      continue;
    }
    throw new Error(`${res.status} ${error}: ${message}`);
  }
}

const data = await callWebmetic(
  'https://hub.webmetic.de/company-sessions?domain=your-website.com',
);
```

```python tab="Python" tab-group="lang"
import time
import requests

RETRY = ("RATE_LIMITED", "CONCURRENT_UPDATE")

def call_webmetic(url, params, attempts=3):
    for i in range(1, attempts + 1):
        res = requests.get(
            url,
            params=params,
            headers={"Authorization": "wmtc_YOUR_API_KEY"},
        )
        if res.ok:
            return res.json()

        detail = res.json().get("detail")
        # Paths outside the API answer {"detail": "Not Found"}
        if not isinstance(detail, dict):
            detail = {"error": f"HTTP_{res.status_code}", "message": str(detail)}
        # Temporary errors: wait and try again
        if detail["error"] in RETRY and i < attempts:
            retry_after = res.headers.get("Retry-After", "")
            time.sleep(int(retry_after) if retry_after.isdigit() else i)  # Retry-After first, else the backoff
            continue
        raise RuntimeError(f"{res.status_code} {detail['error']}: {detail['message']}")

data = call_webmetic(
    "https://hub.webmetic.de/company-sessions",
    {"domain": "your-website.com"},
)
```

If your code doesn't know an error code, treat it as an error that a new attempt won't fix.

## Next steps [#next-steps]

- [Rate limits](https://webmetic.de/docs/en/api/anfragelimits): Plan requests so that no status `429` comes back.
- [Authentication](https://webmetic.de/docs/en/api/authentifizierung): Find the right key and send it correctly.
- [Credits in the API](https://webmetic.de/docs/en/api/credits): Read which calls cost credits and what happens on `402`.
