evnxt.

Die Suche läuft in deinem Browser, es wird keine Eingabe an uns gesendet.

Dokumentation · Integrationen

Produkt-API

Über die Produkt-API liest du Websites, Beiträge, Keywords und Ergebnisse deines Workspaces und legst neue Beiträge an. Sie ist der Weg für eigene Werkzeuge, Agenten und Automatisierungen — dieselbe Logik wie in der Oberfläche, nur ohne Browser.

Beta, auf Anfrage freigeschaltet. Diese Seite ist immer sichtbar. Ob die API für deinen Workspace bereits erreichbar ist, siehst du unter Einstellungen → API. Steht dort „API noch nicht freigeschaltet“, antworten alle Endpunkte mit 404 — schreib uns, dann schalten wir sie frei.

Basisadresse und Version

Alle Endpunkte liegen unter:

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

Die Version steht im Pfad. Solange v1 existiert, ändern sich bestehende Feldnamen nicht; neue Felder können hinzukommen. Ein Client muss unbekannte Felder ignorieren.

Authentifizierung

Jede Anfrage trägt einen API-Schlüssel als Bearer-Token:

Authorization: Bearer evx_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Schlüssel legst du unter Einstellungen → API an (Eigentümer oder Administrator des Workspaces). Der Klartext wird genau einmal angezeigt — danach steht in der Oberfläche nur noch der Anfang zur Wiedererkennung, zum Beispiel evx_live_ab12cd34…. Gespeichert wird bei uns nur ein sha256-Hash; wir können dir einen verlorenen Schlüssel nicht zurückgeben, du legst dann einen neuen an.

Ein Schlüssel gehört zu genau einem Workspace. Es gibt keinen Kontowechsel in der API: Was der Schlüssel nicht sieht, existiert für ihn nicht.

Widerrufene Schlüssel antworten mit 401 und dem Code key_revoked.

Geltungsbereiche (Scopes)

Jeder Schlüssel trägt eine Liste von Geltungsbereichen. Fehlt der für einen Endpunkt nötige, antwortet er mit 403 und dem Code insufficient_scope.

Scope erlaubt
websites:read Websites lesen
articles:read Beiträge lesen
articles:write Beiträge anlegen, freigeben, veröffentlichen
keywords:read Keywords lesen
results:read Search-Console-Kennzahlen lesen
webhooks:manage Webhook-Endpunkte verwalten

Vergib nur, was der jeweilige Zweck braucht. Ein Reporting-Werkzeug braucht kein articles:write.

Rate-Limits

Es gibt zwei Schranken, beide je Schlüssel — nicht je Workspace. Ein aussetzender Integrationsversuch soll den regulären Betrieb desselben Kunden nicht mit ausbremsen.

Schranke Voreinstellung Obergrenze
Anfragen pro Minute 60 600
Anfragen pro Tag 10.000 100.000

Beide stellst du beim Anlegen eines Schlüssels unter Einstellungen → API ein und kannst sie dort später über Limits ändern anpassen.

Warum zwei? Das Minutenlimit begrenzt Lastspitzen, nicht Dauerlast: 60 Anfragen pro Minute wären rund 86.000 am Tag. Ein Skript in einer Endlosschleife bleibt damit dauerhaft unter dem Minutenlimit und läuft trotzdem den ganzen Tag.

Der Tageszähler läuft nach UTC, nicht nach Ortszeit: Sommerzeitwechsel hätten sonst einen 23- und einen 25-Stunden-Tag. Er steigt nur für Anfragen, die tatsächlich bedient werden — wer gerade in die Minutensperre läuft, verbraucht dafür nichts vom Tagesbudget.

Jede Antwort trägt:

Header Bedeutung
X-RateLimit-Limit erlaubte Anfragen pro Minute
X-RateLimit-Remaining im laufenden Minutenfenster noch offen
X-RateLimit-Reset Unix-Zeit (Sekunden), zu der das Minutenfenster zurückgesetzt wird
X-RateLimit-Daily-Limit erlaubte Anfragen pro Tag
X-RateLimit-Daily-Remaining heute noch offen
X-RateLimit-Daily-Reset Unix-Zeit (Sekunden) des nächsten Tageswechsels (Mitternacht UTC)
Retry-After nur bei 429: Sekunden bis zum nächsten Versuch

Bei Überschreitung kommt in beiden Fällen 429 mit dem Code rate_limit_exceeded. Die Meldung nennt, welche Schranke gegriffen hat; unterscheiden lässt es sich auch an Retry-After: beim Minutenlimit sind das höchstens 60 Sekunden, beim Tageslimit die Zeit bis Mitternacht UTC.

HTTP/1.1 429 Too Many Requests
Retry-After: 18143
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
X-RateLimit-Reset: 1789200060
X-RateLimit-Daily-Limit: 10000
X-RateLimit-Daily-Remaining: 0
X-RateLimit-Daily-Reset: 1789218000

Warte die im Retry-After genannte Zeit ab, statt sofort erneut zu senden.

Jede Antwort trägt außerdem eine X-Request-Id. Nenne sie bei Rückfragen — damit finden wir denselben Aufruf wieder.

Fehlerformat

Jeder Fehler hat dieselbe Form:

{
  "error": {
    "code": "validation_failed",
    "message": "Die Anfrage ist unvollständig oder fehlerhaft.",
    "fields": { "title": ["Das Feld title ist erforderlich."] }
  }
}

code ist der stabile, maschinenlesbare Teil. message richtet sich an Menschen und darf sich ändern. fields ist immer ein Objekt, bei nicht-Validierungsfehlern ein leeres.

Code HTTP Bedeutung
not_found 404 Es gibt diese Ressource nicht — oder sie gehört einem anderen Workspace. Beides ist absichtlich nicht unterscheidbar.
unauthenticated 401 kein oder unbekannter Schlüssel
key_revoked 401 Schlüssel wurde widerrufen
insufficient_scope 403 Scope fehlt
rate_limit_exceeded 429 Minuten- oder Tageslimit erreicht
validation_failed 422 Eingabe fehlerhaft
quota_exhausted 402 Artikelkontingent aufgebraucht
quality_blocked 422 Qualitätsprüfung nicht bestanden
mode_not_allowed 409 Veröffentlichungsmodus lässt diesen Schritt nicht zu
invalid_state 409 Beitrag ist im falschen Status
cms_error 502 Das CMS hat nicht mitgespielt (fields.kind nennt die Art)

Paginierung

Listen nehmen ?page (ab 1) und ?per_page (1 bis 100, Standard 25). Werte außerhalb werden zurechtgerückt, nicht abgelehnt. Die Antwort trägt:

{ "data": [ … ], "meta": { "page": 1, "per_page": 25, "total": 137, "total_pages": 6 } }

Endpunkte

Websites auflisten

GET /websites · Scope websites:read

curl -H "Authorization: Bearer $EVX_KEY" https://app.evnxt.de/api/v1/websites
{
  "data": [
    {
      "id": 12,
      "name": "Laufschuhe-Magazin",
      "base_url": "https://laufschuhe.example",
      "language": "de",
      "market": "DE",
      "publish_mode": "approval",
      "cms": { "type": "wordpress", "status": "active" }
    }
  ],
  "meta": { "total": 1 }
}

publish_mode ist manual, approval oder autopilot und entscheidet, welche Schreibschritte erlaubt sind (siehe unten).

Beiträge einer Website

GET /websites/{id}/articles · Scope articles:read

Optionaler Filter ?status= (zum Beispiel entwurf, angefragt, publiziert), dazu page und per_page.

{
  "data": [
    {
      "id": 481,
      "website_id": 12,
      "status": "publiziert",
      "kind": "redaktionell",
      "slug": "laufschuhe-fuer-breite-fuesse",
      "title": "Laufschuhe für breite Füße",
      "published_at": "2026-09-14T08:12:00+02:00",
      "scheduled_at": null,
      "remote_url": "https://laufschuhe.example/blog/laufschuhe-fuer-breite-fuesse",
      "created_at": "2026-09-12T10:03:11+02:00"
    }
  ],
  "meta": { "page": 1, "per_page": 25, "total": 34, "total_pages": 2 }
}

Beitrag im Detail

GET /articles/{id} · Scope articles:read

Enthält zusätzlich body_html, die einzelnen Blöcke, die Aggregate aus Faktencheck und Qualitätsprüfung sowie die Quellen.

{
  "data": {
    "id": 481,
    "title": "Laufschuhe für breite Füße",
    "teaser": "Worauf es bei der Leistenbreite ankommt.",
    "body_html": "<h2>…</h2><p>…</p>",
    "author_name": "Redaktion",
    "generation_status": "fertig",
    "live_url": "https://laufschuhe.example/blog/laufschuhe-fuer-breite-fuesse",
    "blocks": [
      { "position": 1, "type": "text", "heading": "Leisten und Weite", "body_html": "<p>…</p>", "image_url": null, "status": "fertig" }
    ],
    "factcheck": { "version": 2, "score": 91, "coverage": 1.0, "blocks_checked": 7, "blocks_total": 7, "open_critical": 0 },
    "quality": { "version": 1, "score": 88, "blocking": [] },
    "sources": [
      { "url": "https://example.org/studie", "title": "Laufschuhstudie 2025", "verification": "verified" }
    ]
  }
}

factcheck.coverage unter 1.0 heißt: nicht jeder Textabschnitt wurde geprüft. quality.blocking listet die Befunde, die eine Freigabe verhindern — ist sie nicht leer, wird approve mit quality_blocked abgelehnt.

Beitrag anlegen

POST /websites/{id}/articles · Scope articles:write

Feld Pflicht Beschreibung
title ja Thema oder Arbeitstitel, 5 bis 180 Zeichen. Die KI macht daraus den endgültigen Titel.
keyword nein Hauptkeyword für die Recherche
kind nein redaktionell (Standard), produktratgeber oder vergleich
brief nein Freitext mit Wünschen, bis 2000 Zeichen
scheduled_at nein gewünschter Veröffentlichungstermin (ISO 8601)
curl -X POST https://app.evnxt.de/api/v1/websites/12/articles \
  -H "Authorization: Bearer $EVX_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title":"Laufschuhe für breite Füße","keyword":"laufschuhe breite füße"}'

Antwort 201:

{ "data": { "id": 482, "website_id": 12, "status": "entwurf", "title": "Laufschuhe für breite Füße", "slug": "laufschuhe-fuer-breite-fuesse-2", "published_at": null, "remote_url": null } }

Der Beitrag wird nicht sofort fertig. Er startet als Entwurf, die Produktion läuft im Hintergrund (Recherche, Gliederung, Bilder, Text, Prüfung). Den Fortschritt siehst du an generation_status in GET /articles/{id} — oder du abonnierst die Ereignisse article.ready_for_review und article.failed per Webhook.

Das Kontingent wird beim Anlegen reserviert, genau wie in der Oberfläche. Ist keines mehr frei, kommt 402:

{ "error": { "code": "quota_exhausted", "message": "Kontingent erschöpft: Paket Medium: 0 von 8 Credits verfügbar.", "fields": {} } }

Gesponserte Beiträge lassen sich über die API bewusst nicht bestellen — die hängen an Preis und Platzierungslogik und gehören in die Oberfläche.

Beitrag freigeben

POST /articles/{id}/approve · Scope articles:write

Gibt einen Beitrag frei, der zur Prüfung steht (Status angefragt), und veröffentlicht ihn im angebundenen CMS. Erlaubt in den Modi approval und autopilot.

  • 409 mode_not_allowed — die Website steht auf manual; dort gibt es keine Freigabe-Veröffentlichung.
  • 409 invalid_state — der Beitrag steht nicht zur Prüfung.
  • 422 quality_blocked — die Qualitätsprüfung ist nicht bestanden. fields.reasons nennt die Gründe im Klartext.
  • 502 cms_error — das CMS hat abgelehnt; fields.kind ist auth, conflict, transient oder rejected.

Beitrag veröffentlichen

POST /articles/{id}/publish · Scope articles:write

Für Websites im Modus manual — dort ist Veröffentlichen von Hand der vorgesehene Weg — und als zweiter Anlauf für bereits freigegebene Beiträge, bei denen das CMS beim ersten Mal nicht erreichbar war. Der Vorgang ist idempotent: ein bereits veröffentlichter Beitrag wird nicht ein zweites Mal angelegt.

In den Modi approval und autopilot antwortet der Endpunkt für noch nicht freigegebene Beiträge mit 409 mode_not_allowed — die Erstveröffentlichung läuft dort über approve.

Keywords einer Website

GET /websites/{id}/keywords · Scope keywords:read

Optionaler Filter ?status=candidate|selected|rejected|planned.

{
  "data": [
    {
      "id": 3312,
      "website_id": 12,
      "keyword": "laufschuhe breite füße",
      "language": "de",
      "market": "DE",
      "intent": "informational",
      "status": "selected",
      "source": "labs_ideas",
      "score": 78,
      "search_volume": 1900,
      "difficulty": 24,
      "data_as_of": "2026-09-01"
    }
  ],
  "meta": { "page": 1, "per_page": 25, "total": 184, "total_pages": 8 }
}

search_volume und difficulty sind null, wenn es zu diesem Begriff keine Messung gibt. null heißt unbekannt, nicht „null Suchanfragen“.

Ergebnisse einer Website

GET /websites/{id}/results · Scope results:read

Search-Console-Kennzahlen der letzten 28 Tage, ein Eintrag je Tag.

{
  "data": [
    { "date": "2026-09-01", "clicks": 142, "impressions": 5310, "ctr": 0.0267, "position": 12.4 }
  ],
  "meta": { "connected": true, "site_url": "sc-domain:laufschuhe.example", "status": "active", "window_days": 28, "last_sync_at": "2026-09-15T04:22:00+02:00", "total": 28 }
}

Ist keine Property verbunden, kommt trotzdem 200 — mit leerer Liste und Hinweis:

{ "data": [], "meta": { "connected": false, "note": "Für diese Website ist keine Search-Console-Property verbunden — es liegen keine Ergebnisse vor." } }

Das ist Absicht: Die Website gibt es ja, sie hat nur noch keine Ergebnisse. Ein Fehler würde eine Integration abbrechen lassen, wo schlicht nichts zu holen ist.

Webhooks verwalten

GET /webhooks, POST /webhooks, DELETE /webhooks/{id}, POST /webhooks/{id}/test · Scope webhooks:manage

Details, Ereignisliste und Signaturprüfung stehen auf der Seite Webhooks.

Häufige Missverständnisse

„Fremde ID gibt 403." Nein — 404. Wir bestätigen nie, dass eine ID existiert, die dir nicht gehört.

„Nach POST /articles ist der Beitrag fertig." Nein. Er ist bestellt. Die Produktion dauert Minuten bis Stunden.

approve heißt: im CMS sichtbar." Erst wenn die Antwort kein cms_error enthält und remote_url gefüllt ist. Eine Empfangsbestätigung ist kein Veröffentlichungsnachweis.

Screenshot folgtEinstellungen → API mit angelegtem Schlüssel und Webhook-Endpunkt
Zur Übersicht Kostenlos starten Etwas fehlt oder stimmt nicht? Schreib an anfrage@wojcik.de.