# Unternehmen anreichern (https://webmetic.de/docs/api/anleitungen/unternehmen-anreichern)

Mit dieser Anleitung ergänzen Sie Unternehmen aus Ihrem Customer-Relationship-Management (CRM) über die API um Daten. Am Ende haben Sie:

- eine Enrichment-Liste mit einer Zeile je Domain, jede fertig als gefunden oder nicht gefunden
- Branche, Mitarbeiterzahl, Umsatz, Adresse, Telefonnummer und weitere Daten als JSON oder CSV
- ein Skript, das Sie für die nächsten Domains wieder starten
- dieselbe Liste für Ihr Team im Tab **Enrichment**

## Bevor Sie beginnen [#bevor-sie-beginnen]

- **Dauer**: Das Skript ist in wenigen Minuten eingerichtet. Wie lange das Anreichern dauert, steht unter [Auf das Ergebnis warten](#auf-das-ergebnis-warten).
- **Konto**: Sie brauchen ein Webmetic-Konto, das Enrichment nutzen kann. Wenn Ihr Konto Enrichment derzeit nicht nutzen kann, zum Beispiel nach einer Kündigung, antwortet die API mit `403` und `ACCOUNT_NOT_ELIGIBLE`.
- **Zugang**: Sie brauchen den Schlüssel **Enrichment**, nicht den Schlüssel für Visitor Intelligence. Mit dem falschen Schlüssel antwortet die API mit `403` und `ENRICHMENT_KEY_NOT_FOUND`. Außerdem brauchen Sie cURL oder eine Umgebung für JavaScript oder Python mit `requests`.
- **Rechte**: Ein Admin erzeugt den Schlüssel unter [**Menü** > **Integrationen** > **API**](https://app.webmetic.de/?menu=api_details) in der Karte **Enrichment** mit **Schlüssel generieren**. Für die Aufrufe selbst brauchen Sie nur den Schlüssel.
- **Plan**: Sie können nur in der Testphase, in Growth und in Enterprise den Schlüssel erzeugen, Listen anlegen und Domains hinzufügen. Enrichment kostet derzeit keine Credits.

Wenn schon ein Schlüssel besteht, klicken Sie nicht auf **Neu generieren**, denn danach gilt der alte Schlüssel nicht mehr.

## Liste anlegen [#liste-anlegen]

Legen Sie die Liste mit [`POST /enrichment/lists`](https://webmetic.de/docs/api/referenz/enrichment/lists-post) an. Der Body braucht einen `name` mit 1 bis 200 Zeichen, und eine Beschreibung in `desc` ist optional.

Die API antwortet mit `201` und dem Objekt `list`. Merken Sie sich `list.id`, denn alle weiteren Aufrufe brauchen diese ID.

Weil Namen nicht eindeutig sein müssen, legt jeder Aufruf eine neue Liste an, auch mit demselben Namen.

## Domains hinzufügen [#domains-hinzufügen]

Senden Sie die Domains mit [`POST /enrichment/lists/{list_id}/domains`](https://webmetic.de/docs/api/referenz/enrichment/lists-list-id-domains) im Feld `domains`. Eine Anfrage nimmt 1 bis 5.000 Einträge auf. Wenn Sie mehr Einträge haben, senden Sie weitere Anfragen an dieselbe Liste.

Webmetic kürzt jeden Eintrag auf die Domain. Aus `https://www.ihre-domain.de/kontakt` und `info@ihre-domain.de` wird so `ihre-domain.de`. Sie müssen die Einträge also nicht vorher bereinigen.

Die API antwortet mit `202` und den Zählern `added`, `skipped_existing` und `invalid`, aber ohne Zeilen-IDs. Was die Zähler bedeuten, steht unter [Doppelte und ungültige Einträge](#doppelte-und-ungültige-einträge).

Webmetic reichert die neuen Zeilen danach im Hintergrund an, ohne dass Sie etwas starten.

## Auf das Ergebnis warten [#auf-das-ergebnis-warten]

Rufen Sie [`GET /enrichment/lists/{list_id}`](https://webmetic.de/docs/api/referenz/enrichment/lists-list-id) ab, bis `pending_rows` 0 ist. Die Liste hat keinen eigenen Status, und `pending_rows` zählt alle Zeilen, die noch nicht `done` oder `miss` sind.

Unternehmen, die Webmetic schon kennt, sind meist nach etwa einer Minute fertig, aber große Listen brauchen länger. Wenn Webmetic eine Website erst lesen muss, dauert es Stunden bis einige Tage.

Weil jede Zeile als `done` oder `miss` endet, erreicht `pending_rows` immer 0. Geben Sie Ihrem Skript trotzdem eine eigene Höchstdauer, statt tagelang zu warten.

Lesen Sie die Zeilen erst, wenn `pending_rows` 0 ist, denn vorher können Zeilen halb gefüllt sein.

Die Abfragen kosten keine Credits, und [Webhooks](https://webmetic.de/docs/api/webhooks) melden keine fertigen Listen. Mehr dazu unter [Asynchrone Ergebnisse](https://webmetic.de/docs/api/asynchrone-ergebnisse).

## Zeilen abrufen [#zeilen-abrufen]

Rufen Sie [`GET /enrichment/lists/{list_id}/rows`](https://webmetic.de/docs/api/referenz/enrichment/lists-list-id-rows) mit `status=done` ab. So erhalten Sie nur die Zeilen, zu denen Webmetic Daten gefunden hat.

Beginnen Sie mit `page=1` und erhöhen Sie `page`, bis `isLast` im Block `pagination` `true` ist. Eine Seite umfasst standardmäßig und höchstens 10.000 Zeilen. Mehr dazu unter [Paginierung](https://webmetic.de/docs/api/paginierung).

Jede Zeile enthält alle Felder, und unbekannte Werte stehen als `null` darin. Spannen wie `201–500`, Branchen und Beschreibungen sind deutsch. [Enrichment-Liste](https://webmetic.de/docs/api/datenmodell/enrichment-liste) beschreibt alle Felder.

Alternativ liefert [`GET /enrichment/lists/{list_id}/export`](https://webmetic.de/docs/api/referenz/enrichment/lists-list-id-export) alle Zeilen auf einmal als CSV-Datei mit deutschen Spaltennamen. Die Datei hat keine Zeilen-ID, und die Spalte `Domain` enthält `company_url`.

## Daten ins CRM zurückschreiben [#daten-ins-crm-zurückschreiben]

Webmetic schreibt Enrichment-Daten nicht in Ihr CRM, denn die CRM-Integrationen übertragen nur Unternehmen, die Webmetic auf Ihrer Website erkennt. Ihr eigener Code übernimmt deshalb das Zurückschreiben.

Ordnen Sie die Zeilen über die Domain Ihren Datensätzen zu:

1. Kürzen Sie vor dem Senden die Website jedes Datensatzes auf die Domain und merken Sie sich die Zuordnung.
2. Suchen Sie für jede Zeile mit `done` die Datensätze mit derselben `domain`.
3. Schreiben Sie nur Felder, die nicht `null` sind, damit vorhandene Werte in Ihrem CRM erhalten bleiben.
4. Prüfen Sie Zeilen ohne passenden Datensatz von Hand.

Wenn eine Website auf eine Domain weiterleitet, die Webmetic schon kennt, steht danach diese Domain in `domain`, und die Zuordnung findet die Zeile nicht. Beim Prüfen hilft `company_url`, die Adresse, unter der die Website antwortet.

Schreiben Sie Zeilen mit `miss` nicht zurück, denn sie tragen als Namen nur einen Platzhalter aus der Domain.

Enrichment-Zeilen haben keine `company_id`. Sie verbinden die Zeilen deshalb auch mit Daten aus [Besuche täglich exportieren](https://webmetic.de/docs/api/anleitungen/besuche-taeglich-exportieren) über die Domain. Kürzen Sie dafür `company_url` aus den Besuchsdaten auf die Domain ohne `www.`.

## Vollständiges Skript [#vollständiges-skript]

Das Skript verbindet alle Schritte. Ersetzen Sie `crm` durch die Datensätze aus Ihrem CRM und `write_to_crm` (Python) oder `writeToCrm` (JavaScript) durch den Aufruf Ihres CRM. Der Tab cURL zeigt die einzelnen Aufrufe.

Die Funktion `to_domain` kürzt gröber als Webmetic und behält Subdomains. Wenn im CRM eine Website wie `shop.ihre-domain.de` steht, landet die Zeile deshalb in der Prüfung von Hand, obwohl Webmetic Daten gefunden hat.

Das JavaScript nutzt `await` außerhalb von Funktionen. Führen Sie es deshalb als ES-Modul aus, zum Beispiel als Datei mit der Endung `.mjs`.

Sie wählen Abfrageintervall und Höchstdauer selbst. Wenn die Liste danach nicht fertig ist, bricht das Skript mit der Listen-ID ab. Wenn Sie die ID in `LIST_ID` eintragen, wartet das Skript beim nächsten Start weiter.

Bei einem Fehler nennt das Skript Status und Code, zum Beispiel `PLAN_REQUIRED`. Alle Codes finden Sie unter [Fehler](https://webmetic.de/docs/api/fehler).

```bash tab="cURL" tab-group="lang"
# 1. Create the list
curl -X POST "https://hub.webmetic.de/enrichment/lists" \
  -H "Authorization: wmtc_IHR_API_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{"name": "CRM 2026-10"}'

# 2. Add domains, up to 5,000 per request
curl -X POST "https://hub.webmetic.de/enrichment/lists/IHRE_LISTEN_ID/domains" \
  -H "Authorization: wmtc_IHR_API_SCHLUESSEL" \
  -H "Content-Type: application/json" \
  -d '{"domains": ["https://www.bergquell.example/kontakt", "info@nordlicht-logistik.example"]}'

# 3. Repeat until pending_rows is 0
curl "https://hub.webmetic.de/enrichment/lists/IHRE_LISTEN_ID" \
  -H "Authorization: wmtc_IHR_API_SCHLUESSEL"

# 4. Read the found rows, raise page until isLast is true
curl "https://hub.webmetic.de/enrichment/lists/IHRE_LISTEN_ID/rows?status=done&page=1&page_size=1000" \
  -H "Authorization: wmtc_IHR_API_SCHLUESSEL"

# Or: every row of the list as one CSV file
curl "https://hub.webmetic.de/enrichment/lists/IHRE_LISTEN_ID/export" \
  -H "Authorization: wmtc_IHR_API_SCHLUESSEL" \
  -o enrichment.csv
```

```js tab="JavaScript" tab-group="lang"
const BASE = 'https://hub.webmetic.de/enrichment';
const HEADERS = {
  Authorization: 'wmtc_IHR_API_SCHLUESSEL', // Enrichment key
  'Content-Type': 'application/json',
};
const POLL_MS = 30 * 1000; // your own choice
const MAX_WAIT_MS = 2 * 60 * 60 * 1000; // your own choice
let LIST_ID = null; // set an existing list ID to keep waiting

// CRM records: record ID -> website or email as stored in your CRM
const crm = {
  'crm-1001': 'https://www.bergquell.example/kontakt',
  'crm-1002': 'info@nordlicht-logistik.example',
};

async function call(method, path, body) {
  const res = await fetch(BASE + path, {
    method,
    headers: HEADERS,
    body: body ? JSON.stringify(body) : undefined,
  });
  const text = await res.text();
  if (res.ok) return JSON.parse(text);
  let message = text.slice(0, 200); // not JSON, for example a proxy error page
  try {
    const { detail } = JSON.parse(text);
    message = `${detail.error}: ${detail.message}`;
  } catch {}
  throw new Error(`${res.status} ${message}`);
}

function toDomain(value) {
  // rough version of Webmetic's shortening, subdomains stay
  const host = value.trim().toLowerCase().split('@').pop().split('://').pop();
  return host.split(/[/?:]/)[0].replace(/^www\./, '');
}

async function writeToCrm(recordId, fields) {
  console.log(recordId, fields); // replace with your CRM's update call
}

const byDomain = {};
for (const [recordId, website] of Object.entries(crm)) {
  (byDomain[toDomain(website)] ??= []).push(recordId);
}

if (!LIST_ID) {
  // 1. Create the list
  LIST_ID = (await call('POST', '/lists', { name: 'CRM 2026-10' })).list.id;
  // 2. Add domains, up to 5,000 per request
  const domains = Object.keys(byDomain);
  for (let i = 0; i < domains.length; i += 5000) {
    console.log(await call('POST', `/lists/${LIST_ID}/domains`, { domains: domains.slice(i, i + 5000) }));
  }
}

// 3. Wait until no row is pending
const deadline = Date.now() + MAX_WAIT_MS;
while ((await call('GET', `/lists/${LIST_ID}`)).list.pending_rows > 0) {
  if (Date.now() > deadline) throw new Error(`Still running: set LIST_ID = '${LIST_ID}' and run again later`);
  await new Promise((resolve) => setTimeout(resolve, POLL_MS));
}

// 4. Read the found rows page by page
const rows = [];
for (let page = 1; ; page += 1) {
  const body = await call('GET', `/lists/${LIST_ID}/rows?status=done&page=${page}&page_size=1000`);
  rows.push(...body.rows);
  if (body.pagination[0].isLast) break;
}

// 5. Write back known values only
const ROW_FIELDS = ['id', 'domain', 'status', 'src', 'created_at', 'enriched_at'];
for (const row of rows) {
  const recordIds = byDomain[row.domain];
  if (!recordIds) {
    console.log('Check by hand:', row.domain, row.company_url);
    continue;
  }
  const fields = Object.fromEntries(
    Object.entries(row).filter(([key, value]) => value !== null && !ROW_FIELDS.includes(key)),
  );
  for (const recordId of recordIds) await writeToCrm(recordId, fields);
}
```

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

BASE = "https://hub.webmetic.de/enrichment"
HEADERS = {"Authorization": "wmtc_IHR_API_SCHLUESSEL"}  # Enrichment key
POLL_SECONDS = 30  # your own choice
MAX_WAIT_SECONDS = 2 * 60 * 60  # your own choice
LIST_ID = None  # set an existing list ID to keep waiting

# CRM records: record ID -> website or email as stored in your CRM
crm = {
    "crm-1001": "https://www.bergquell.example/kontakt",
    "crm-1002": "info@nordlicht-logistik.example",
}


def call(method, path, **kwargs):
    res = requests.request(method, BASE + path, headers=HEADERS, timeout=60, **kwargs)
    if res.status_code >= 400:
        try:
            detail = res.json()["detail"]
            message = f"{detail['error']}: {detail['message']}"
        except (ValueError, KeyError, TypeError):
            message = res.text[:200]  # not JSON, for example a proxy error page
        raise SystemExit(f"{res.status_code} {message}")
    return res.json()


def to_domain(value):
    # rough version of Webmetic's shortening, subdomains stay
    host = value.strip().lower().split("@")[-1].split("://")[-1]
    host = re.split(r"[/?:]", host)[0]
    return host[4:] if host.startswith("www.") else host


def write_to_crm(record_id, fields):
    print(record_id, fields)  # replace with your CRM's update call


by_domain = {}
for record_id, website in crm.items():
    by_domain.setdefault(to_domain(website), []).append(record_id)

if LIST_ID is None:
    # 1. Create the list
    LIST_ID = call("POST", "/lists", json={"name": "CRM 2026-10"})["list"]["id"]
    # 2. Add domains, up to 5,000 per request
    domains = list(by_domain)
    for i in range(0, len(domains), 5000):
        print(call("POST", f"/lists/{LIST_ID}/domains", json={"domains": domains[i:i + 5000]}))

# 3. Wait until no row is pending
deadline = time.monotonic() + MAX_WAIT_SECONDS
while call("GET", f"/lists/{LIST_ID}")["list"]["pending_rows"] > 0:
    if time.monotonic() > deadline:
        raise SystemExit(f"Still running: set LIST_ID = '{LIST_ID}' and run again later")
    time.sleep(POLL_SECONDS)

# 4. Read the found rows page by page
rows, page = [], 1
while True:
    params = {"status": "done", "page": page, "page_size": 1000}
    body = call("GET", f"/lists/{LIST_ID}/rows", params=params)
    rows += body["rows"]
    if body["pagination"][0]["isLast"]:
        break
    page += 1

# 5. Write back known values only
ROW_FIELDS = {"id", "domain", "status", "src", "created_at", "enriched_at"}
for row in rows:
    record_ids = by_domain.get(row["domain"])
    if not record_ids:
        print("Check by hand:", row["domain"], row["company_url"])
        continue
    fields = {k: v for k, v in row.items() if v is not None and k not in ROW_FIELDS}
    for record_id in record_ids:
        write_to_crm(record_id, fields)
```

Die Domains im Beispiel sind erfunden. Die API zählt Einträge mit der Endung `.example` als `invalid`.

## Doppelte und ungültige Einträge [#doppelte-und-ungültige-einträge]

Die Zähler der Antwort und die Status der Zeilen zeigen, was mit Ihren Einträgen passiert ist:

- **Doppelt in einer Anfrage**: Webmetic legt die Domain einmal an und zählt die Wiederholungen nicht. Die drei Zähler ergeben deshalb nicht immer die Zahl der gesendeten Einträge.
- **Schon in der Liste**: `skipped_existing` zählt Domains, die in dieser Liste schon stehen. Webmetic legt keine zweite Zeile an. In einer anderen Liste erhält dieselbe Domain eine eigene Zeile.
- **Ungültig**: `invalid` zählt Einträge ohne lesbare Domain, zum Beispiel einen Unternehmensnamen oder eine IP-Adresse. Die API nennt nicht, welche Einträge das waren. Wenn Sie das wissen müssen, kürzen Sie Ihre Einträge vor dem Senden selbst wie `to_domain` im Skript und prüfen dabei, welche keine Domain enthalten.
- **Mehrere Domains in einem Eintrag**: Webmetic liest nur die letzte. Senden Sie jede Domain als eigenen Eintrag.
- **Subdomain einer Plattform**: Webmetic kürzt sie auf die Domain der Plattform. Geben Sie deshalb die eigene Domain des Unternehmens an.
- **Nicht gefunden**: Bei `miss` hat Webmetic keine verwertbaren Daten gefunden.
- **Zwei Zeilen mit derselben Domain**: Wenn zwei Domains per Weiterleitung zum selben Unternehmen führen, kann die Liste zwei solche Zeilen enthalten.
- **Erneut anreichern**: `done` und `miss` sind endgültig, und Webmetic aktualisiert fertige Zeilen nicht. Für frische Daten legen Sie eine neue Liste an.

Sie können Listen löschen und Zeilen entfernen, aber nur im Dashboard. Mehr dazu unter [Ergebnisse und Export](https://webmetic.de/docs/enrichment/ergebnisse-und-export).

## Nächste Schritte [#nächste-schritte]

- [Enrichment-Liste](https://webmetic.de/docs/api/datenmodell/enrichment-liste): Alle Felder einer Zeile mit Typ, Beispiel und Bedeutung.
- [Ergebnisse und Export](https://webmetic.de/docs/enrichment/ergebnisse-und-export): Dieselbe Liste im Dashboard prüfen, korrigieren und exportieren.
- [Fehler](https://webmetic.de/docs/api/fehler): Fehlercodes wie `PLAN_REQUIRED` verstehen und beheben.
