Verankerungs-Messung
API für Partner · Entwurf 1.0
Stand 2026-09-23

Lesende REST-API für Partner und Kunden

Stand 2026-09-23, Entwurf aus der Discovery-Session Partner-API (App). Gilt für die Kunden-App (app/api). Änderungen als datierter Nachtrag in der Änderungshistorie am Ende, nie stillschweigend. Grundlage: docs/CONCEPT.md (Nachtrag 2026-09-23), docs/SCHNITTSTELLE-CLOUD.md (Teil 4, Läufe und Wizard-Leitplanke). Beispieldateien zu jedem Endpunkt liegen in docs/api-examples/ und stammen vom erfundenen Kunden „Hotel Seeblick"; Zahlen, Marken und Domains dort sind ausgedacht, die Summen gehen auf.

Verfügbarkeit in Stufen. Die Doku ist vor der Software fertig, damit Partner früh integrieren können. Jeder Endpunkt trägt seine Stufe:

StufeBedeutung
1Nur Doku und Beispieldateien.
2Endpunkt antwortet in Produktion mit den Beispieldaten für Testkeys (vm_test_…).
3Endpunkt liefert echte Daten für Produktivkeys (vm_live_…).

Stand 2026-09-23 stehen alle Endpunkte auf Stufe 1. Die Tabelle in Abschnitt 4 wird bei jedem Deploy nachgeführt.


1. Zweck und Abgrenzung #

Die API liefert die Ergebnisse der Verankerungs-Messung maschinenlesbar an zwei Arten von Abnehmern:

Die API ist lesend, mit einer Ausnahme: ein Partner kann einen Kunden anfragen (Abschnitt 4.4). Alles Weitere bleibt Arbeit des Operators: Wizard und Erstmessung in der Cloud, Paketwechsel, Pausieren, Beenden. Die Messung selbst läuft im getrennten Mess-System; die App speichert jede Messung als unveränderlichen Snapshot und liefert daraus.

Nicht Teil der API: Kosten und Preise, Nutzerverwaltung, Inbox, manuelle Nachmessung, Ändern oder Löschen von Kunden, Ändern von Prompts, Rohdaten des Mess-Systems über die Läufe hinaus (Abschnitt 4.7).

2. Grundlagen #

2.1 Basis-URL und Version #

https://app.verankerungsmessung.de/api/v1

Die Hauptversion steht im Pfad. Innerhalb von v1 sind Änderungen additiv: neue Felder, neue Endpunkte, neue Werte in Listen. Abnehmer müssen unbekannte Felder tolerieren. Ein Feld wird nie entfernt, umbenannt oder in seiner Bedeutung geändert, ohne dass eine v2 entsteht. Jede Antwort trägt den Header X-VM-API-Version: 1.0 (Minor zählt additive Änderungen).

2.2 Authentifizierung #

Jeder Aufruf trägt einen API-Key:

Authorization: Bearer vm_live_3f9c…

2.3 Format #

2.4 Fehler #

{ "error": "Messung nicht gefunden", "code": "not_found", "detail": null }
HTTPcodeBedeutung
401unauthorizedKey fehlt oder ist ungültig oder widerrufen
403forbiddenAktion für diesen Geltungsbereich nicht erlaubt
404not_foundRessource unbekannt oder nicht im Datenraum des Keys
409conflictAktion derzeit nicht möglich, Grund in detail
422validation_failedBody oder Parameter ungültig, Grund in detail
429rate_limitedLimit überschritten, Header Retry-After in Sekunden
500internal_errorFehler bei uns; bitte mit Zeitpunkt melden

error ist ein deutscher Text für Menschen und kann sich ändern; code ist stabil und für Programme.

2.5 Rate-Limit #

60 Anfragen je Minute und Key. Jede Antwort trägt X-RateLimit-Limit, X-RateLimit-Remaining und bei 429 zusätzlich Retry-After. Eine Messung ändert sich nach Freigabe nie; ein Abnehmer holt sie einmal und speichert sie. Für „gibt es Neues" reicht ein täglicher Aufruf der Messungsliste.

2.6 Listen und Seiten #

Listen liefern { "items": [ … ], "nextCursor": string | null }. Parameter limit (Standard 50, Höchstwert 200) und cursor (Wert aus nextCursor). Ist nextCursor null, gibt es keine weitere Seite. Sortierung je Endpunkt angegeben und stabil.

3. Begriffe #

3.1 Kunde und Paket #

Ein Kunde ist eine Marke mit einer Domain in einem Markt. Bei einem Partner ist das dessen Endkunde, etwa ein Hotel. Der Kunde hat ein Paket: die Zahl der gemessenen Prompts (10, 20, 30 oder 40) und das Intervall (monthly oder quarterly). Das Paket bestimmt den Preis; Abrechnung läuft außerhalb der API.

Kundenstatus:

statusBedeutung
requestedangefragt; der Operator bereitet den Wizard und die Erstmessung vor
activeErstmessung freigegeben, Messungen laufen im Intervall des Pakets
pausedvom Operator pausiert, keine neuen Messungen, alte bleiben abrufbar
endedbeendet, Daten bleiben lesbar

3.2 Messung, Prompt, Plattform, Lauf #

Eine Messung ist ein Durchlauf aller Prompts des Kunden auf allen Plattformen zu einem Zeitpunkt. Ein Prompt ist eine wörtliche Frage, wie ein Nutzer sie einer KI stellt. Eine Plattform ist ein KI-Assistent (chatgpt, gemini). Ein Lauf ist eine einzelne Antwort einer Plattform auf einen Prompt; je Prompt und Plattform gibt es n Läufe (Standard 50), damit Häufigkeiten belastbar sind.

3.3 Zielmarke und Kategorien #

Die Zielmarke ist die Marke des Kunden. Jede Antwort wird ausgewertet, welche Marken sie nennt und wie:

KategorieBedeutung
recommendeddie Marke wird als Empfehlung genannt, also als Antwort auf die Frage
mentioneddie Marke kommt in irgendeiner Form vor; enthält recommended und negative
negativedie Marke wird mit einem Einwand oder einer Abwertung genannt

Je Marke, Prompt und Plattform zählt die API, in wie vielen der n Läufe das zutrifft. Ein Lauf zählt je Marke und Kategorie höchstens einmal. Für einen einzelnen Lauf gilt die Einordnung der Zielmarke targetClassification mit Vorrang recommended vor negative vor mentioned, sonst none.

3.4 Quellen und Klassen #

Zitiert eine Antwort Webseiten, zählt die API je Prompt und Plattform, wie viele Läufe eine Domain zitieren (citingRuns), und darunter die einzelnen URLs. Jede Domain trägt eine Klasse:

kindBedeutung
ownDomain der Zielmarke
competitorDomain einer anderen erkannten Marke; brand nennt sie
thirdDritte: Portale, Presse, Verzeichnisse, Karten

3.5 Ampel und Schwellen #

Die Ampel ist unsere Bewertung des Anteils empfohlener Läufe der Zielmarke: gruen ab thresholds.ampel.gruen, gelb ab thresholds.ampel.gelb, sonst rot. Die Schwellen stehen in jeder Messung; ein Abnehmer kann sie übernehmen oder eigene Regeln auf die Zählwerte anwenden.

3.6 Stufen und Arten von Prompts #

Das Raster einer Vollmessung hat Stufen 0 bis 3, von der breiten Frage zur spitzen Nische. Jeder Prompt trägt stage.level und ein Label in der Reportsprache. kind unterscheidet:

kindBedeutung
questionKundenfrage entlang einer Achse (Anlass, Budget, Lage …)
vocabularybreite Frage mit einem Suchbegriff, Stufe 0
attributeFrage nach einer Eigenschaft der Zielmarke, attribute gesetzt

3.7 Sprachen #

market.languageCode ist die Messsprache der Prompts und Antworten. market.reportLanguage (de oder en) ist die Sprache der redaktionellen Texte und Labels im Block report. Marken- und Domain-Namen sind sprachneutral. Aufzählungswerte (ampel, kind, classification, verdict, horizon, status) sind Schlüssel und werden nie übersetzt.

4. Endpunkte #

EndpunktZweckStufe
GET /meKey prüfen, Geltungsbereich, Limits1
GET /customerseigene Kunden1
GET /customers/:idein Kunde1
POST /customersKunde anfragen (nur partner)1
GET /customers/:id/measurementsfreigegebene Messungen1
GET /measurements/:ideine Messung vollständig1
GET /measurements/:id/runsalle Läufe einer Messung1
GET /customers/:id/tech-checksTech-Check-Historie1
GET /tech-checks/:idein Tech-Check1
POST /customers/:id/tech-checksTech-Check auslösen1

4.1 GET /me #

Prüft den Key und zeigt, was er sieht. Beispiel api-examples/me.json.

FeldTypBedeutung
key.id, key.labelstringKennung und Bezeichnung des Keys, wie vom Operator vergeben
key.scopepartner | customerGeltungsbereich
key.createdAt, key.lastUsedAttimestamp | null
partnerobject | nullbei scope = partner: id, name, packages[] (wählbare Pakete)
customerobject | nullbei scope = customer: der Kunde wie in 4.3
rateLimit{ limit, window }
apiVersionstringwie Header X-VM-API-Version

4.2 GET /customers #

Alle Kunden im Datenraum des Keys, sortiert nach createdAt absteigend. Bei scope = customer genau ein Eintrag. Parameter status filtert auf einen Wert aus 3.1. Beispiel api-examples/customers.list.json.

4.3 GET /customers/:id #

Beispiel api-examples/customers.get.json.

FeldTypBedeutung
iduuid
partnerIduuid | nullnull bei Direktkunden
namestringFirmierung, wie vom Partner angegeben
brandstring | nullZielmarke; wird vom Operator im Wizard festgelegt, bis dahin null
domainstringHostname ohne Protokoll
statusenum3.1
package{ prompts, interval }prompts ∈ 10, 20, 30, 40; intervalmonthly, quarterly
market{ locationCode, languageCode, reportLanguage } | nullDataForSEO-Standort und -Sprache der Prompts, Reportsprache; null bis zur Erstmessung
notesstring | nullBriefing des Partners aus der Anfrage, unverändert
createdAttimestampAnfrage
activatedAttimestamp | nullFreigabe der Erstmessung
nextMeasurementAtdate | nullgeplanter Tag der nächsten Messung
latestMeasurement{ id, label, releasedAt } | nullneueste freigegebene Messung
latestTechCheck{ id, checkedAt } | null

4.4 POST /customers #

Nur scope = partner. Legt einen Kunden mit status = requested an. Der Operator bekommt eine Mail, führt den Wizard im Mess-System durch, startet die Erstmessung und gibt sie frei; ab dann ist der Kunde active. Der Partner sieht den Fortschritt über GET /customers/:id. Abrechnung des Pakets beginnt mit der Anfrage, außerhalb der API.

Body, Beispiel api-examples/customers.create.request.json:

FeldPflichtRegeln
nameja2 bis 120 Zeichen
domainjaHostname, ohne Protokoll und Pfad; wird auf Kleinschreibung und ohne www. normalisiert
package.promptsja10, 20, 30 oder 40, muss zu einem Paket des Partners passen (GET /me)
package.intervaljamonthly oder quarterly, muss zum Paket passen
notesneinbis 4.000 Zeichen; was für die Messung wichtig ist: Zielgruppen, Anlass, Wettbewerber, Sprache der Gäste

Antworten: 201 mit dem Kunden wie in 4.3 (api-examples/customers.create.response.json); 409 conflict, wenn für diese Domain bereits ein Kunde des Partners existiert, der nicht ended ist; 422 validation_failed.

Kein PATCH, kein DELETE. Paketwechsel, Pausieren und Beenden laufen über den Operator.

4.5 GET /customers/:id/measurements #

Freigegebene Messungen des Kunden, neueste zuerst. Entwürfe und laufende Messungen erscheinen nicht. Beispiel api-examples/measurements.list.json.

FeldTypBedeutung
id, customerIduuid
labelstringBezeichnung des Operators, etwa Messung 2026-Q4
sequenceintlaufende Nummer je Kunde, Erstmessung = 1
periodStart, periodEnddate | nullErhebungszeitraum
releasedAttimestampFreigabe; ab hier unveränderlich
platformsstring[]Plattform-Schlüssel
nintLäufe je Prompt und Plattform
promptCountintgemessene Prompts
runsAvailableboolob 4.7 Läufe liefert
techCheckIduuid | nullTech-Check, der mit dieser Messung lief
overview[pf]{ recommended, n, share }Summe empfohlener Läufe der Zielmarke über alle Prompts, n = Prompts × Läufe

4.6 GET /measurements/:id #

Die Messung vollständig. Beispiel api-examples/measurements.get.json (etwa 120 KB bei 10 Prompts). Struktur: Kopf, prompts[] mit Frage und Ergebnissen je Plattform, report mit den redaktionellen Blöcken.

Kopf

FeldTypBedeutung
id, customerId, label, sequence, periodStart, periodEnd, releasedAt, nwie 4.5
platforms{ key, label }[]Reihenfolge wie in den Ergebnissen
marketobject4.3
target{ brand, domain }Zielmarke
thresholdsobjectSchwellen unserer Bewertung, 3.5
promptsarraysiehe unten, sortiert nach id
reportobjectsiehe unten
runs{ available, count, href }Verweis auf 4.7
techCheckIduuid | null

prompts[]

FeldTypBedeutung
idintstabil über alle Messungen des Kunden
kindenum3.6
labelstringKurzlabel in der Reportsprache
textstringdie wörtliche Frage in der Messsprache; das ist der Prompt
stage{ level, label }Stufe 0 bis 3
axis{ key, label } | nullAchse, null bei Attribut-Fragen
attribute{ slot, label } | nullnur bei kind = attribute
results[pf]objectein Eintrag je Plattform, siehe unten; das ist das Ergebnis

results[pf]

FeldTypBedeutung
nintausgewertete Läufe; 0, wenn die Plattform für diesen Prompt keine Läufe hat
target{ recommended, mentioned, negative, share, ampel }Zielmarke; share = recommended / n
brands[]{ name, isTarget, recommended, mentioned, negative, n }alle erkannten Marken; Zielmarke immer an erster Stelle, auch mit Nullen; danach absteigend nach recommended, mentioned, Name
sources[]{ domain, kind, brand, citingRuns, n, urls[] }alle zitierten Domains, absteigend nach citingRuns; urls[] = { url, title, citingRuns }, bereinigt um Tracking-Parameter
examples[]{ classification, text }bis zu 3 Antworttexte, deterministisch gewählt: je eine je vorhandener Klassifikation in der Reihenfolge recommended, negative, mentioned, none, dann aufgefüllt; Markdown wie von der Plattform geliefert

Konsistenz: brands[0].recommended === target.recommended, jede Zahl ≤ n, recommended ≤ mentioned, negative ≤ mentioned, jede urls[].citingRuns ≤ citingRuns.

report

Die redaktionellen Blöcke der Vollmessung, Struktur identisch mit dem Verankerungs-Report. Alle Texte in market.reportLanguage. Zählwerte darin sind dieselben wie in prompts[], nur ausgewählt und angereichert.

BlockInhalt
contentkeyMessage (Kernbotschaft), notTargeted (bewusst nicht gemessen), methodNote, actions[] (title, desc, horizonschnell wirksam | nachhaltig, horizonHint, target), kompakt.findings[] (title, text)
funnel[]Trichter über die Stufen: je Prompt byPlatform[pf] = { hits, n } der Zielmarke und competitors[pf][] Top 3
baseline[pf]Vergleichswert der breiten Frage { hits, n, promptId }
attributes[]Attribut-Fragen mit klassdriver | mid | barrier je Plattform, relativ zur Baseline mit thresholds.driverFactor und barrierFactor
targetSegments[]Soll-Ist je Zielsegment: label, rationale, verdicterfuellt | weitgehend | gespalten | luecke, finding, cells[]
radar[]Verankerungsprofil je Segment { label, byPlatform }
landkartecolumns[] Marken der Peer-Gruppe, rows[] je Prompt mit values[pf][brand] = { hits, n } und winner[pf]
vokabular[]Stufe-0-Begriffe mit Zielmarke und stärkstem Wettbewerber
sources[pf][]Top 8 Domains je Plattform über alle Prompts, citingRuns von totalRuns, mit kind, brand und redaktioneller note (carries, supports, outdated, je Text oder null)

hits in den Report-Blöcken bedeutet immer empfohlene Läufe der genannten Marke.

4.7 GET /measurements/:id/runs #

Alle einzelnen Antworten der Messung, für eigene Auswertung und Anonymisierung. Parameter: promptId (int), platform (string), cursor, limit (Standard 50, Höchstwert 200). Sortierung nach id aufsteigend. Beispiel api-examples/measurements.runs.json zeigt eine Seite mit promptId = 403; der Testkey liefert je Prompt und Plattform 3 Läufe, in Produktion sind es n.

409 conflict, wenn runsAvailable false ist (Messungen vor der Einführung dieses Endpunkts).

FeldTypBedeutung
measurementIduuid
filter{ promptId, platform }die angewandten Filter oder null
totalintLäufe, die dem Filter entsprechen
items[].idintLauf-ID, stabil
items[].promptId, items[].platformSchlüssel in prompts[] und platforms[]
items[].completedAttimestampZeitpunkt der Antwort
items[].modelHintstring | nullModellangabe der Plattform, wörtlich, oft null
items[].targetClassificationenumEinordnung der Zielmarke in diesem Lauf, 3.3
items[].textstringdie vollständige Antwort als Markdown, ungekürzt
items[].mentions[]{ verbatim, brand, isTarget, category, evidence }jede erkannte Markennennung: verbatim = Schreibweise im Text, brand = kanonischer Name oder null, wenn die Zuordnung offen ist, evidence = Textstelle, auf die sich die Einordnung stützt
items[].citations[]{ url, domain, kind }zitierte Seiten des Laufs

4.8 Tech-Check #

Der Tech-Check prüft, ob die Website des Kunden für KI-Assistenten technisch erreichbar und lesbar ist. Er läuft automatisch mit jeder Messung und auf Anforderung, einmal je Kunde und Tag. Geprüft werden die Startseite, robots.txt, sitemap.xml und llms.txt; keine weiteren Seiten. Ergebnisse sind unveränderliche Snapshots mit Historie.

GET /customers/:id/tech-checks listet die Snapshots, neueste zuerst (api-examples/tech-checks.list.json). GET /tech-checks/:id liefert einen Snapshot (api-examples/tech-checks.get.json). POST /customers/:id/tech-checks ohne Body startet einen neuen Check und antwortet 202 mit status = queued und href (api-examples/tech-checks.create.response.json); 409 conflict, wenn heute schon einer lief. Der Snapshot ist nach wenigen Minuten unter href abrufbar, bis dahin 404.

FeldTypBedeutung
id, customerIduuid
measurementIduuid | nullgesetzt, wenn der Check mit einer Messung lief
triggermeasurement | api | adminAuslöser
checkedAttimestamp
url, finalUrlstringStartseite wie angefragt und nach Weiterleitungen
summary{ ok, warn, fail, info }Anzahl Checks je Status
checks[]{ key, status, value, detail }siehe unten
lighthouse{ source, fetchedAt, mobile, desktop }Kennzahlen aus der PageSpeed Insights API, je Gerät performance, accessibility, bestPractices, seo (0 bis 100), fcpMs, lcpMs, tbtMs, cls, speedIndexMs; null, wenn die Abfrage scheiterte

status je Check: ok unauffällig, warn sollte behoben werden, fail verhindert oder beschränkt die Nutzung durch KI-Assistenten, info Feststellung ohne Wertung. Die Wertung ist unsere; ein Abnehmer kann value eigenständig interpretieren.

keyvalueWertung
http_statusintok bei 200, sonst fail
redirect_chainstring[]ok bis 2 Weiterleitungen, sonst warn
tls{ validUntil, issuer }fail bei ungültigem Zertifikat
robots_txtURL | nullwarn, wenn fehlend
robots_ai_crawler:<Name>allow | disallow | partialje Crawler eine Zeile: GPTBot, OAI-SearchBot, ChatGPT-User, Google-Extended, Googlebot, PerplexityBot, ClaudeBot, Bytespider, CCBot, Applebot-Extended. fail bei disallow für Crawler, die Antworten speisen (GPTBot, OAI-SearchBot, ChatGPT-User, Google-Extended, Googlebot, PerplexityBot, ClaudeBot); info bei den übrigen
sitemap_xmlURL | nullwarn, wenn fehlend
llms_txtURL | nullwarn, wenn fehlend
canonicalURL | nullwarn, wenn fehlend oder auf fremde Seite
meta_robotsstring | nullfail bei noindex
json_ldstring[] Typenwarn, wenn kein branchentypischer Typ
raw_text_wordsintWörter sichtbarer Text im HTML ohne JavaScript; warn unter 200, fail unter 50
js_rendering_hint{ frameworks[], emptyRoot, scriptShare }Heuristik ohne Browser: fail bei leerem App-Container, warn bei hohem Skriptanteil; ausdrücklich ein Hinweis, keine Rendering-Prüfung

5. Hinweise für Anonymisierung und Weiterverarbeitung #

Prompt und Ergebnis liegen getrennt vor: der Prompt ist prompts[].text, das Ergebnis prompts[].results[pf]. Wer Ergebnisse ohne Marken- oder Ortsnamen zeigen will, muss diese Stellen behandeln:

6. Geplant, nicht Teil von 1.0 #

Wird bei Umsetzung als Nachtrag ergänzt, additiv:

7. Testzugang #

Ein Testkey (vm_test_…) liefert ab Stufe 2 auf allen Endpunkten die Beispieldaten aus docs/api-examples/: Partner „Beispiel-Partner GmbH" mit dem Kunden „Hotel Seeblick AG" (active, 2 Messungen, 2 Tech-Checks) und einem zweiten Kunden im Status requested. POST /customers und POST /customers/:id/tech-checks antworten mit den Beispielantworten, ohne etwas zu speichern. Die IDs der Beispieldateien sind fest, damit Integrationstests darauf verweisen können. Rate-Limit und Fehlerformat sind wie in Produktion.

Änderungshistorie #