Zum Inhalt springen
Dokumentation

Delivery-Webhook: Artikel auf der eigenen Website empfangen

Der signierte Webhook, der AutoPSEO-Artikel an Ihre Website sendet: Header, alle Events und Payloads, die erwarteten Antworten, Wiederholungen, wartende Befehle und IndexNow.

Property → Content → Wo Artikel veröffentlicht werden20 Min. Lesezeit

AutoPSEO sendet jeden Artikel, den es veröffentlicht, aktualisiert oder offline nimmt, als signierten HTTPS-POST an Ihre eigene Website. Ihre Website speichert den Artikel und rendert ihn mit ihren eigenen Templates. So liegt der Inhalt in Ihrem System und funktioniert auch dann weiter, wenn Sie AutoPSEO einmal verlassen. Diese Seite ist die Referenz für die Entwicklerin oder den Entwickler, die oder der den Empfänger baut: Signatur, jedes Event mit seinem Payload, die Antworten, die AutoPSEO auswertet, Wiederholungen, wartende Befehle und IndexNow.

Den Webhook einrichten

  1. Öffnen Sie die Seite Content der Property und gehen Sie zu Where articles are published.
  2. Tragen Sie die Webhook address on your site ein. Sie muss eine https://-URL auf einem öffentlichen Hostnamen sein: IP-Adressen und localhost werden abgelehnt. AutoPSEO folgt keinen Weiterleitungen, geben Sie deshalb die endgültige Adresse an.
  3. Tragen Sie die Your site's address ein, zum Beispiel https://example.com. Der Pfad jedes Content-Typs hängt darunter, ein Artikel eines Typs unter /answers liegt also unter https://example.com/answers/<slug>.
  4. Wählen Sie Save and send a test. AutoPSEO erzeugt ein Signing Secret (whsec_ gefolgt von 48 Hex-Zeichen) und sendet einen Ping an Ihre Adresse. Ein Empfänger, der Signaturen prüft, schlägt bei diesem ersten Ping fehl, weil er das Secret noch nicht kennt.
  5. Zeigen Sie das Signing secret mit Show oder Copy an, hinterlegen Sie es als Secret auf Ihrer Website und wählen Sie dann Send a test.
  6. Lesen Sie unter What your site handles nach: Jeder Befehl zeigt Connected, Not connected, Declared, not tested oder Not tested.

Neue Properties starten damit, dass AutoPSEO die Artikel unter seiner eigenen Adresse hostet (siehe Hosting). Hosting und Webhook können parallel laufen. Sobald Ihre Website die Artikel anzeigt, wählen Sie Stop hosting on AutoPSEO: Jeder live geschaltete Artikel wechselt dann an die Adresse Ihrer Website und wird als article.published an Ihren Webhook gesendet. Dafür braucht die Schaltfläche einen Webhook, der seinen letzten Test beantwortet hat, und die Adresse Ihrer Website. Solange das Hosting ausgeschaltet ist, lässt sich der Webhook nicht entfernen.

New secret ersetzt das Signing Secret sofort, und das alte Secret wird nicht mehr akzeptiert. Hinterlegen Sie das neue Secret direkt nach dem Erzeugen auf Ihrer Website.

AutoPSEO stellt Zustellungen nur für einen Webhook in die Warteschlange, dessen letzter Ping mit einem 2xx beantwortet wurde. Ein fehlgeschlagener Ping (beim Speichern, bei Send a test oder zu Beginn eines Site-Scans) setzt den Webhook auf „Fehler“, und Artikel, die veröffentlicht werden, während er im Fehlerzustand ist, werden für ihn nicht in die Warteschlange gestellt. Lassen Sie ping deshalb immer beantworten und wählen Sie nach einem behobenen Ausfall Send a test.

Header und Signatur

Jede Anfrage ist ein POST mit JSON-Body und diesen Headern:

HeaderWert
Content-Typeapplication/json
User-AgentAutoPSEO-Webhook/1
X-AutoPSEO-EventDer Event-Name, steht auch im Body als event
X-AutoPSEO-DeliveryDie Delivery-ID, steht auch im Body als id. Bei jeder Wiederholung einer Zustellung dieselbe
X-AutoPSEO-TimestampUnix-Zeit in Sekunden, zu der dieser Versuch signiert wurde
X-AutoPSEO-Signaturesha256= gefolgt vom Hex-kodierten HMAC-SHA256 über <timestamp>.<raw body>

So prüfen Sie eine Anfrage:

  1. Nehmen Sie den Timestamp-Header, einen Punkt und den Roh-Body der Anfrage genau so, wie er angekommen ist, vor jedem JSON-Parsing.
  2. Berechnen Sie den HMAC-SHA256 über diese Zeichenfolge. Als Schlüssel dient das gesamte Signing Secret, so wie die App es anzeigt, einschließlich des Präfixes whsec_, als UTF-8-Bytes.
  3. Kodieren Sie das Ergebnis hexadezimal und vergleichen Sie es in konstanter Zeit mit dem Teil von X-AutoPSEO-Signature nach sha256=. Lehnen Sie die Anfrage ab, wenn beide voneinander abweichen.

Jeder Versuch wird neu mit eigenem Timestamp signiert. Sie können daher Anfragen ablehnen, deren Timestamp weit von Ihrer Uhrzeit abweicht (üblich sind fünf Minuten), ohne Wiederholungen zu verhindern. HMAC selbst ist in RFC 2104 definiert.

Jeder Body enthält id, event, occurredAt (ISO 8601, UTC) und site: { propertyId, domain }.

Events

EventArtWann AutoPSEO es sendet
pingPrüfungBeim Speichern des Webhooks, bei Send a test und zu Beginn jedes Site-Scans
testPrüfungDirekt nach einem erfolgreichen Ping, einer pro Befehl
article.publishedZustellungEin Artikel geht zum ersten Mal live; jeder live geschaltete Artikel, wenn Sie das Hosting beenden
article.updatedZustellungEine neue Revision geht live (Aktualisierung, Optimierung, Rollback, erneutes Veröffentlichen nach einer Depublizierung, hinzugefügte Sprachen) oder ein Cover wurde ergänzt
article.unpublishedZustellungEin Artikel wird offline genommen oder mit einer anderen Seite zusammengeführt
path.addZustellungEin neuer Wert eines typisierten Content-Typs wurde freigegeben
path.removeZustellungJemand hat darum gebeten, den Pfad eines Werts offline zu nehmen
path.checkAufrufBevor AutoPSEO eine Seite an einer Adresse plant, für die Ihre Website keine Seite hat, oder einen neuen Wert vorschlägt
site.exportAufrufBei einem Site-Scan, im täglichen Themenabgleich und bevor AutoPSEO einen live geschalteten Artikel ändert
articles.adoptedAufrufNachdem ein Scan Artikel importiert hat, die Ihre Website exportiert hat
site.handoverAufrufWenn jemand das Schreiben von Content-Typen an AutoPSEO übergibt oder zurückgibt

Zustellungen laufen über eine Outbox: Sie werden im selben Datenbank-Batch wie die Änderung erfasst, sofort gesendet, wiederholt und können auf Ihre Website warten. Aufrufe sind einfache Anfragen mit Antwort: AutoPSEO liest die Antwort unmittelbar aus. Wann die Pfadbefehle gesendet werden, erklärt Typisierte Seiten und neue Pfade.

Der Artikel-Payload

article.published und article.updated enthalten den gesamten Artikel, alle Sprachen eingeschlossen:

{
  "id": "0f6d2c1e-7a51-4c1b-9d0e-5b8f3a2e9c41",
  "event": "article.published",
  "occurredAt": "2026-10-12T08:30:00.000Z",
  "site": { "propertyId": "prop_123", "domain": "example.com" },
  "article": {
    "id": "a4c09e2b-6f1d-4e57-8b3a-1d2e3f4a5b6c",
    "slug": "plan-a-content-calendar",
    "path": "/answers/plan-a-content-calendar",
    "type": { "key": "answers", "name": "Answers", "path": "/answers" },
    "lang": "en",
    "langs": ["en", "de"],
    "rev": 1,
    "url": "https://example.com/answers/plan-a-content-calendar",
    "datePublished": "2026-10-12",
    "dateModified": "2026-10-12",
    "contentHash": "9f2c4e1a7b3d5f6081a2c3d4",
    "writer": "platform",
    "category": "Planning",
    "copy": {
      "title": "How to plan a content calendar",
      "metaDescription": "A content calendar in five steps, with a template.",
      "h1": "How to plan a content calendar",
      "directAnswer": "List your topics, give each a date and an owner, then review the plan weekly.",
      "keyTakeaways": ["Plan a quarter at a time.", "Give every piece one owner."],
      "sections": [{ "heading": "Start from your topics", "bodyHtml": "<p>Begin with the searches you want to win.</p>" }],
      "faq": [{ "q": "How far ahead should I plan?", "a": "One quarter is enough for most teams." }],
      "sources": [{ "title": "Creating helpful content", "url": "https://developers.google.com/search/docs/fundamentals/creating-helpful-content" }]
    },
    "translations": { "de": { "title": "Einen Redaktionsplan erstellen", "h1": "Einen Redaktionsplan erstellen" } },
    "cover": {
      "url": "https://app.autopseo.com/media/covers/prop_123/<sha256>.jpg",
      "sha256": "<sha256>",
      "contentType": "image/jpeg",
      "width": 1536,
      "height": 1024
    }
  }
}

Der deutsche Eintrag ist hier gekürzt; jede Übersetzung hat dieselbe Struktur wie copy.

FeldBedeutung
idDie ID des Artikels in AutoPSEO. Sie ändert sich nie und ist daher ein sicherer Schlüssel
slugDer letzte Teil der Adresse. Bei typisierten Seiten enthält er mit / verbundene Segmente, etwa google/software-engineer
pathDie Adresse relativ zu Ihrer Website: der Pfad des Typs plus der Slug. Steht fest, sobald die Seite existiert
type{ key, name, path }. key ist stabil; path ist der Ort, an dem die Artikel des Typs liegen (leer für das Stammverzeichnis der Website)
lang, langsDie Hauptsprache und alle Sprachen dieser Revision
revDie Revisionsnummer
urlDie vollständige Adresse, wie AutoPSEO sie kennt: die Ihrer Website, sobald das Hosting ausgeschaltet ist, oder die url, die Ihr Empfänger zuletzt zurückgegeben hat
datePublished, dateModifiedYYYY-MM-DD. Das Veröffentlichungsdatum bleibt über spätere Revisionen hinweg unverändert
contentHashEin Fingerabdruck der Revision
writerWer die Revision geschrieben hat: platform (AutoPSEO), mcp (Ihre eigene KI), user, import (der eigene Text Ihrer Website) oder aggregate (eine Listenseite)
copyDer Artikel in der Hauptsprache
translationsDie anderen Sprachen, nach Sprachcode geordnet
categoryDie Kategorie Ihrer Website für den Artikel, wenn Ihr Format Kategorien führt
topic{ externalId, keyword }, wenn der Artikel zu einem Thema geschrieben wurde, das Ihre Website übergeben hat, damit Sie dieses Thema abschließen können
partsBei typisierten Seiten der Wert jedes Teils: { "company": "google", "role": "software-engineer" }
kind"list" bei einer Listenseite. Ihr Inhalt ist { title, h1, items: [{ from, text }] }, wobei from der Pfad der Seite ist, aus der der Eintrag stammt, und parts den Wert benennt, den sie zusammenführt
coverEin Cover, das AutoPSEO für den Artikel erstellt hat. Laden Sie es einmal herunter und behalten Sie eine eigene Kopie; die Adresse ändert sich nie, weil der Dateiname der SHA-256 des Bildes ist
basedOnBei article.updated: die Version Ihrer Seite, auf der diese Revision aufbaut (siehe Konflikte unten)
redirectToBei article.unpublished nach einer Zusammenführung: wohin die alte Adresse jetzt führt. Beantworten Sie sie mit einem 301 und nehmen Sie dort nichts Neues an

article.unpublished enthält dieselben article-Felder, außer copy, translations, category und cover.

Inhalt von copy

FeldBedeutung
title, metaDescription, h1Seitentitel, Meta-Beschreibung und Überschrift
directAnswerDie kurze Antwort, die zuerst angezeigt wird
keyTakeawaysEine Liste von Strings
sections[{ heading, bodyHtml }]
faq[{ q, a }]
sources[{ title, url, lastVerified? }]
questionOptional: die Frage der Suchenden in deren eigenen Worten, wenn Ihr Format sie verlangt
categoryOptional: eine der Kategorien Ihrer Website
listsOptional: Klartextlisten nach Schlüssel, für Content-Typen, die sie verlangen (etwa Beispielfragen eines Interview-Leitfadens)

In Artikeln, die AutoPSEO schreibt, verwendet bodyHtml nur p, ul, li, strong, em, a und code, und Links behalten nur ihr href. Von Ihrer eigenen Website übernommene Texte behalten Ihr HTML. Schicken Sie es zusätzlich durch Ihren eigenen Sanitizer, wenn Ihre Templates das verlangen.

Was Ihr Empfänger antwortet

Jedes 2xx schließt eine Zustellung ab. AutoPSEO liest außerdem ein optionales JSON-Objekt aus dem Antwort-Body; halten Sie es unter 64 KB, denn eine größere Antwort zählt als Fehler.

FeldWirkung
urlDie https://-Adresse, unter der der Artikel jetzt liegt. AutoPSEO speichert sie als URL des Artikels
versionDie Version Ihrer Seite, ein undurchsichtiger String mit höchstens 128 Zeichen. Er kommt beim nächsten Update als basedOn zurück
urlsDie https://-Adressen, die diese Zustellung geändert hat, für IndexNow (jede Sprache oder die Adressen, die Sie nicht mehr ausliefern). [] bedeutet, dass sich nichts geändert hat
cover"wanted", wenn Sie kein Cover für den Artikel haben und die Zustellung keines enthielt. AutoPSEO erstellt eines und sendet es in einem article.updated

Fehlerantworten haben unterschiedliche Bedeutungen:

Ihre AntwortWie AutoPSEO sie liestWas passiert
Ein 4xx mit { "error": "unknown_event" } oder ein 501Sie behandeln diesen Befehl noch nichtDie Zustellung wartet, ohne Wiederholungen
Ein 409 mit { "error": "conflict" }Das Update wurde auf einer Version aufgebaut, die Ihre Seite nicht mehr hatDie Zustellung schlägt ohne Wiederholungen fehl, und der Artikel wird vor seiner nächsten Änderung erneut von Ihrer Website gelesen
Ein 4xx außer 408 oder 429 mit { "error": "<code>", "details": [] } auf path.add oder path.removeEine Ablehnung mit Ihrer BegründungWird festgehalten; siehe Typisierte Seiten und neue Pfade
Alles andere: keine Antwort innerhalb von 10 Sekunden, ein Netzwerkfehler, eine Weiterleitung, 408, 429, 5xx oder ein anderes 4xxEin vorübergehender FehlerWird mit Backoff wiederholt

Wiederholungen und überholte Zustellungen

Eine Zustellung wird gesendet, sobald die Änderung gespeichert ist, und ein Fünf-Minuten-Takt sendet alles Fällige, die ältesten zuerst. Nach einem vorübergehenden Fehler wartet AutoPSEO zwischen den Versuchen 1 Minute, dann 5 Minuten, 15 Minuten, 1 Stunde, 3 Stunden, 6 Stunden und 12 Stunden. Beim achten fehlgeschlagenen Versuch wird die Zustellung als fehlgeschlagen markiert, und die Seite Content zeigt sie in einem roten Badge neben dem Webhook an.

Gibt es eine neuere Zustellung für denselben Artikel, wird eine ältere, noch ausstehende als überholt markiert und nie gesendet. Jede Zustellung enthält den gesamten Artikel, daher genügt Ihrer Website die neueste. Pfadbefehle zum selben Wert funktionieren genauso.

Ihr Empfänger kann eine Zustellung zweimal sehen, etwa wenn er den Artikel gespeichert, aber erst nach dem 10-Sekunden-Timeout geantwortet hat. Entdoppeln Sie anhand von X-AutoPSEO-Delivery oder machen Sie jeden Schreibvorgang idempotent, indem Sie unter article.id speichern. article.updated kann auch einen Artikel erreichen, den Sie nach einem article.unpublished gelöscht haben, weil das erneute Veröffentlichen eines offline genommenen Artikels ein Update sendet: Behandeln Sie Updates als „anlegen oder ersetzen“.

Befehle, die Ihre Website noch nicht behandelt

Eine Website kann das Protokoll Befehl für Befehl umsetzen. AutoPSEO führt den Status jedes Befehls und erfährt ihn auf drei Wegen:

  • Tests. Nach jedem erfolgreichen Ping sendet AutoPSEO einen test pro Befehl. Der Body ist { id, event: "test", occurredAt, site, test: { event: "<command>", ... } }, wobei test ein Beispiel in der Form des echten Befehls enthält, das sich auf <type path>/autopseo-connection-test bezieht, eine Adresse, die keine Website hat. Prüfen Sie es wie den echten Befehl, ändern Sie nichts und antworten Sie mit einem 2xx: { "test": "<command>", "ok": true } für Connected, { "test": "<command>", "ok": false, "error": "unknown_event" } für Not connected. Eine Antwort mit ok: false und einem anderen Fehlercode, etwa bei der Depublizierung einer Adresse, die Sie nicht haben, zählt trotzdem als verbunden. Das Beispiel steht unter test, nie auf oberster Ebene, sodass ein Empfänger, der nichts von Tests weiß, darin keinen Artikel findet.
  • Echte Zustellungen und Aufrufe. Ein 2xx markiert den Befehl als verbunden; unknown_event markiert ihn als nicht verbunden.
  • Die Ping-Antwort. Befehle, die Ihre Antwort in autopseo.events auflistet, erscheinen als Declared, not tested, bis ein Test oder eine Zustellung mehr aussagt.

Eine Zustellung für einen Befehl, der nicht verbunden ist, wartet. Die Seite Content listet auf, was wartet, zum Beispiel „Your site does not take articles down yet“, mit einer Schaltfläche Done on the site für alles, was Sie von Hand erledigt haben; AutoPSEO sendet dieses dann nie. Findet ein späterer Test oder eine spätere Zustellung den Befehl verbunden, wird alles gesendet, was darauf gewartet hat.

Die Ping-Antwort

Beantworten Sie ping mit einem 2xx. Eine Website, die nur Artikel empfängt, braucht nichts weiter. Damit AutoPSEO erfährt, was Ihre Website sonst noch leistet, nehmen Sie ein autopseo-Objekt auf:

{
  "ok": true,
  "autopseo": {
    "version": 1,
    "events": ["article.published", "article.updated", "article.unpublished"],
    "covers": true,
    "indexNow": { "key": "3f1c9a7e5b2d4f60", "keyLocation": "https://example.com/3f1c9a7e5b2d4f60.txt" },
    "export": ["articles", "topics", "profile"],
    "adopt": true,
    "handover": true,
    "types": ["answers"],
    "writer": "site"
  }
}
FeldBedeutung
version1. Das Objekt wird ignoriert, wenn die Version nicht mindestens 1 ist
eventsDie Befehle, die Sie behandeln, als „deklariert“ angezeigt, bis sie getestet sind
coverstrue, wenn Ihre Seiten zu jedem Artikel ein Cover zeigen. Der Writer von AutoPSEO erstellt das Cover dann beim Schreiben, sodass bereits die erste Zustellung es enthält, und Ihre eigene KI wird gebeten, eines zu liefern. Siehe Covers
indexNow{ key, keyLocation? }: der IndexNow-Schlüssel, den Sie ausliefern (8 bis 128 Buchstaben, Ziffern und Bindestriche), und, wenn die Schlüsseldatei nicht unter /<key>.txt liegt, ihre https://-Adresse
exportWas site.export übergibt: articles, topics, profile
adoptSie akzeptieren articles.adopted, sodass ein vollständiger Scan Ihre vorhandenen Artikel übernehmen kann
handoverSie akzeptieren site.handover
typesDie Schlüssel der Content-Typen, die Sie akzeptieren (bis zu 50)
writerWer Ihre neuen Artikel heute schreibt: site oder autopseo

IndexNow für die Adressen, die Sie ausliefern

Wenn Ihre Ping-Antwort indexNow deklariert, sendet AutoPSEO nach den Zustellungen, die Sie annehmen, einen Ping an IndexNow unter api.indexnow.org, und Ihre Website muss selbst nichts pingen.

  • Es sendet die urls, die Ihre Antwort aufgelistet hat. Ohne urls sendet es die url Ihrer Antwort, sonst die URL des Artikels. urls: [] sendet nichts.
  • Nur Adressen auf Ihrem eigenen Host werden aufgenommen, wie es das IndexNow-Protokoll verlangt: der Host von keyLocation, sonst der Host der Adresse Ihrer Website.
  • Jeder Zustelllauf sendet pro Website einen Ping mit bis zu 10.000 Adressen.
  • 200 und 202 werden akzeptiert. Bei einem 429, einem 5xx oder ausbleibender Antwort bleiben die Adressen erhalten und werden nach einer Stunde erneut versucht, jeweils mit verdoppelter Wartezeit bis zu einem Tag (länger, wenn Retry-After es verlangt), zusammen mit allem, was spätere Zustellungen ergänzt haben. Ein 403 (Schlüssel ungültig) oder 422 (Adressen nicht auf dem Host) wird festgehalten und verworfen.
  • Die Seite Content zeigt den letzten Ping: wann, wie viele Adressen, den HTTP-Status und den nächsten Versuch.
  • Solange AutoPSEO die Artikel auch hostet, werden die gehosteten Seiten mit dem eigenen Schlüssel von AutoPSEO gepingt, und Ihrer wird nicht verwendet.

Das Tool IndexNow unter Indexing ist ein eigenständiger, manueller Submitter; siehe IndexNow.

Konflikte: Der Text Ihrer Website ist das Original

Die version Ihrer Antwort (oder die version eines Artikels in Ihrem Export) ist der Stand, auf dem AutoPSEO Ihre Seite vermutet. Jedes article.updated trägt sie als basedOn. Hat sich die Seite auf Ihrer Website seitdem geändert, zum Beispiel durch die Korrektur einer Redakteurin oder eines Redakteurs, antworten Sie mit 409 und { "error": "conflict" } und speichern Sie nichts. Die Zustellung schlägt ohne Wiederholungen fehl.

Bevor eine eingereihte Änderung an einem live geschalteten Artikel geschrieben wird, liest AutoPSEO diesen Artikel mit site.export und paths erneut von Ihrer Website, sofern Ihre Website site.export behandelt. Hat sich Ihre Version weiterentwickelt, wird Ihr Text zur nächsten Revision des Artikels (Writer import), und das darauf folgende Update trägt Ihre aktuelle Version als basedOn.

path.add und path.remove

Diese beiden Zustellungen betreffen einen einzelnen Wert eines typisierten Content-Typs, zum Beispiel ein Unternehmen in /interviews/{company}/{role}. Sie enthalten statt article ein path-Objekt:

{
  "id": "7c1e9d2a-3b4f-4e8a-9c0d-1e2f3a4b5c6d",
  "event": "path.add",
  "occurredAt": "2026-10-12T08:30:00.000Z",
  "site": { "propertyId": "prop_123", "domain": "example.com" },
  "path": {
    "path": "/interviews/jane-street",
    "type": { "key": "interviews", "name": "Interviews", "path": "/interviews" },
    "part": "company",
    "value": {
      "value": "jane-street",
      "name": "Jane Street",
      "aliases": [],
      "url": "https://<its own site>/",
      "sameAs": ["https://<its careers page>/"],
      "description": "<what it is, in a few words>",
      "facts": [
        { "text": "<one sentence, as the source states it>", "source": "https://<the page that says it>", "lastVerified": "2026-10-01", "tier": "official" }
      ]
    },
    "takes": { "role": ["software-engineer"] }
  }
}
FeldBedeutung
pathDie eigene Adresse des Werts auf Ihrer Website
typeDer Content-Typ als { key, name, path }
partDer Teil der Adresse, den der Wert ausfüllt
value{ value, name }, bei path.add zusätzlich aliases, url, sameAs, description und facts, die der Writer von AutoPSEO zusammengetragen hat. Jeder Fakt hat text, source, lastVerified und einen tier von official oder reported
takesBei path.add: die Werte der anderen Teile, nach denen Menschen zusammen mit ihm suchen
redirectToBei path.remove: wohin die Adressen des Werts künftig mit einem 301 führen sollen

Antworten Sie mit einem 2xx, wenn Sie den Befehl angenommen haben; bei path.add können Sie die url ergänzen, unter der der Wert jetzt liegt, und bei path.remove die urls, die Sie nicht mehr ausliefern und die an IndexNow gehen. Um abzulehnen, antworten Sie mit einem 4xx und { "error": "<code>", "details": ["<why>"] }. Bei path.add schickt invalid_value den Wert mit Ihrer Begründung an den Writer zurück (die dritte Ablehnung ist endgültig), und jeder andere Code lehnt ihn endgültig ab. Bei path.remove zählt not_found als erledigt, und jeder andere Code, etwa 409 in_use, lässt alles unverändert. Wenn Sie einen wartenden Pfadbefehl als Done on the site markieren, gilt das als Zustimmung Ihrer Website.

Aufrufe: Anfragen, deren Antwort AutoPSEO liest

Aufrufe werden wie Zustellungen signiert. Ein Aufruf wartet standardmäßig bis zu 60 Sekunden, bei erneuten Lesevorgängen und Pfadprüfungen 30 Sekunden, und liest Antworten bis zu 24 MB. Jede Antwort außer einem 2xx lässt den Aufruf fehlschlagen.

site.export

Die Anfrage enthält export: { what, cursor, limit }, dazu since (eine ISO-Zeit) für Themen oder paths für ein erneutes Lesen. Antworten Sie mit { "items": [], "next": "<cursor>" }; AutoPSEO fragt erneut, wobei cursor auf Ihr next gesetzt ist, bis es leer oder null ist.

  • what: "articles" läuft in vollständigen Site-Scans mit 10 pro Seite, wenn Sie export mit articles und adopt: true deklariert haben. Eine 400-Antwort bedeutet „nicht unterstützt“. Mit paths: ["/answers/x"] und limit: 1 wird ein einzelner Artikel erneut gelesen; antworten Sie dann nur mit diesem Artikel.
  • what: "topics" läuft in jedem Scan und im täglichen Abgleich, mit 50 pro Seite; der tägliche Abgleich sendet since, die Zeit des letzten Abrufs.
  • what: "profile" läuft in jedem Scan. Antworten Sie mit { "profile": { types, languages, rules, categories, format } }, jeweils optional: types als [{ key, name, path }] und rules mit brandTerms, synonyms, noise, onTopic und offTopic.

Ein exportierter Artikel braucht externalId, einen slug (Buchstaben und Ziffern beliebiger Schriften sowie Bindestriche, höchstens 200 Zeichen, Segmente mit / verbunden), einen path, der auf diesen Slug endet, und copy mit mindestens title, h1 und einem Abschnitt. Optionale Felder: lang (Standard en), type (ein Schlüssel eines Content-Typs), url, keyword, category, datePublished, dateModified, version, translations, owner (die Artikel-ID von AutoPSEO, sobald übernommen) und maintenance: { kind: "refresh" or "optimize", reason }, was diese Aufgabe in die Warteschlange stellt. Jeder exportierte Artikel wird zu einem verwalteten Artikel, dessen erste Revision Ihr Text ist, und es wird nichts zurückgesendet. Ein erneuter Export ist unbedenklich, und ein Artikel, den AutoPSEO offline genommen hat, kommt über einen Export nie zurück.

Ein Themeneintrag ist { keyword, status?, volume?, source?, externalId?, type?, reason? }, wobei status queued, planned oder rejected ist.

articles.adopted

Nach dem Import bittet AutoPSEO Sie, die neuen Artikel verwalten zu dürfen: { "articles": [{ "externalId", "articleId", "slug", "path" }] }. Antworten Sie mit { "adopted": ["<externalId>"], "refused": [{ "externalId": "<id>", "reason": "<why>" }] }. Ein Artikel, den Sie nicht bestätigen, wird wieder aus AutoPSEO entfernt, es sei denn, AutoPSEO hat bereits eine Revision davon geschrieben.

site.handover

{ "handover": { "types": ["answers"], "writer": "autopseo" } }, gesendet, wenn jemand auf der Seite Your site der Property Let AutoPSEO write them oder Hand writing back to the site wählt. Antworten Sie mit { "ok": true, "writer": "autopseo" }. Nach einer Übergabe an AutoPSEO reicht Ihre Website die Themen, die sie prüft, an AutoPSEO weiter, statt sie selbst zu schreiben.

path.check

{ "paths": [{ "path": "/interviews/google/data-engineer" }, { "path": "/interviews/jane-street", "new": { "part": "company", "value": "jane-street", "name": "Jane Street" } }] }. Antworten Sie mit einem Ergebnis pro Pfad: { "results": [{ "path": "/interviews/google/data-engineer", "ok": true }, { "path": "/interviews/jane-street", "ok": false, "error": "not_allowed", "details": ["We only list companies with a careers page."] }] }. Eine Ablehnung wird mit Ihrer Begründung festgehalten und nicht erneut abgefragt; ein Pfad, den Sie auslassen, wird später erneut abgefragt.

Ein minimaler Empfänger

Ein Cloudflare Worker, der die Signatur mit der Web Crypto API prüft, die drei Artikelbefehle behandelt und alle anderen mit unknown_event ablehnt. Er hält Artikel in einem KV-Namespace, der als ARTICLES gebunden ist, und das Signing Secret in einem Worker-Secret namens AUTOPSEO_SECRET.

const HANDLED = ["article.published", "article.updated", "article.unpublished"];
const MAX_SKEW_SECONDS = 300;
const SITE = "https://example.com";

const json = (data, status = 200) =>
  new Response(JSON.stringify(data), { status, headers: { "content-type": "application/json" } });

function hexToBytes(hex) {
  if (!/^[0-9a-f]{64}$/.test(hex)) return null;
  const out = new Uint8Array(32);
  for (let i = 0; i < 32; i++) out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
  return out;
}

async function verified(request, body, secret) {
  const timestamp = request.headers.get("X-AutoPSEO-Timestamp") || "";
  const signature = (request.headers.get("X-AutoPSEO-Signature") || "").replace(/^sha256=/, "");
  if (!/^\d+$/.test(timestamp)) return false;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > MAX_SKEW_SECONDS) return false;
  const bytes = hexToBytes(signature);
  if (!bytes) return false;
  const enc = new TextEncoder();
  const key = await crypto.subtle.importKey("raw", enc.encode(secret), { name: "HMAC", hash: "SHA-256" }, false, ["verify"]);
  // verify() compares in constant time.
  return crypto.subtle.verify("HMAC", key, bytes, enc.encode(`${timestamp}.${body}`));
}

export default {
  async fetch(request, env) {
    if (request.method !== "POST") return new Response("Method not allowed", { status: 405 });
    const body = await request.text();
    if (!(await verified(request, body, env.AUTOPSEO_SECRET))) return json({ error: "bad_signature" }, 401);
    let msg;
    try {
      msg = JSON.parse(body);
    } catch {
      return json({ error: "bad_json" }, 400);
    }

    if (msg.event === "ping") return json({ ok: true, autopseo: { version: 1, events: HANDLED } });

    if (msg.event === "test") {
      const command = msg.test && msg.test.event;
      // Check the sample as you would the real command, and store nothing.
      return HANDLED.includes(command)
        ? json({ test: command, ok: true })
        : json({ test: command, ok: false, error: "unknown_event" });
    }

    if (msg.event === "article.published" || msg.event === "article.updated") {
      const a = msg.article;
      const stored = JSON.parse((await env.ARTICLES.get(a.id)) || "null");
      if (a.basedOn && stored && stored.version && stored.version !== a.basedOn) {
        return json({ error: "conflict", details: ["The page changed on the site."] }, 409);
      }
      // Any change to the page, here or in your own editor, gets a new version.
      const version = crypto.randomUUID();
      await env.ARTICLES.put(a.id, JSON.stringify({ ...a, version }));
      return json({ url: SITE + a.path, version });
    }

    if (msg.event === "article.unpublished") {
      const a = msg.article;
      if (a.redirectTo) await env.ARTICLES.put(a.id, JSON.stringify({ path: a.path, redirectTo: a.redirectTo }));
      else await env.ARTICLES.delete(a.id);
      return json({ ok: true });
    }

    return json({ error: "unknown_event" }, 400);
  },
};

Unter Node.js prüfen Sie auf dieselbe Weise mit createHmac und timingSafeEqual, über den Roh-Body, den Ihr Framework erhalten hat:

import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyAutopseo(rawBody, headers, secret) {
  const timestamp = String(headers["x-autopseo-timestamp"] || "");
  const given = String(headers["x-autopseo-signature"] || "");
  if (!/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const expected = "sha256=" + createHmac("sha256", secret).update(timestamp + ".").update(rawBody).digest("hex");
  return given.length === expected.length && timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}

Als Nächstes: Was den Webhook erreicht, bestimmen Schreiben und Prüfen und Pflege; den gesamten Ablauf zeigt die Übersicht zur Content-Engine.

Bringen Sie Ihre SEO-Inhalte auf Autopilot

Verbinden Sie Search Console und Ihre Website. AutoPSEO scannt sie und plant sofort die ersten Themen. Keine Kreditkarte erforderlich.