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.
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
- Öffnen Sie die Seite Content der Property und gehen Sie zu Where articles are published.
- Tragen Sie die Webhook address on your site ein. Sie muss eine
https://-URL auf einem öffentlichen Hostnamen sein: IP-Adressen undlocalhostwerden abgelehnt. AutoPSEO folgt keinen Weiterleitungen, geben Sie deshalb die endgültige Adresse an. - 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/answersliegt also unterhttps://example.com/answers/<slug>. - 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. - 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.
- 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:
| Header | Wert |
|---|---|
Content-Type | application/json |
User-Agent | AutoPSEO-Webhook/1 |
X-AutoPSEO-Event | Der Event-Name, steht auch im Body als event |
X-AutoPSEO-Delivery | Die Delivery-ID, steht auch im Body als id. Bei jeder Wiederholung einer Zustellung dieselbe |
X-AutoPSEO-Timestamp | Unix-Zeit in Sekunden, zu der dieser Versuch signiert wurde |
X-AutoPSEO-Signature | sha256= gefolgt vom Hex-kodierten HMAC-SHA256 über <timestamp>.<raw body> |
So prüfen Sie eine Anfrage:
- Nehmen Sie den Timestamp-Header, einen Punkt und den Roh-Body der Anfrage genau so, wie er angekommen ist, vor jedem JSON-Parsing.
- 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. - Kodieren Sie das Ergebnis hexadezimal und vergleichen Sie es in konstanter Zeit mit dem Teil von
X-AutoPSEO-Signaturenachsha256=. 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
| Event | Art | Wann AutoPSEO es sendet |
|---|---|---|
ping | Prüfung | Beim Speichern des Webhooks, bei Send a test und zu Beginn jedes Site-Scans |
test | Prüfung | Direkt nach einem erfolgreichen Ping, einer pro Befehl |
article.published | Zustellung | Ein Artikel geht zum ersten Mal live; jeder live geschaltete Artikel, wenn Sie das Hosting beenden |
article.updated | Zustellung | Eine neue Revision geht live (Aktualisierung, Optimierung, Rollback, erneutes Veröffentlichen nach einer Depublizierung, hinzugefügte Sprachen) oder ein Cover wurde ergänzt |
article.unpublished | Zustellung | Ein Artikel wird offline genommen oder mit einer anderen Seite zusammengeführt |
path.add | Zustellung | Ein neuer Wert eines typisierten Content-Typs wurde freigegeben |
path.remove | Zustellung | Jemand hat darum gebeten, den Pfad eines Werts offline zu nehmen |
path.check | Aufruf | Bevor AutoPSEO eine Seite an einer Adresse plant, für die Ihre Website keine Seite hat, oder einen neuen Wert vorschlägt |
site.export | Aufruf | Bei einem Site-Scan, im täglichen Themenabgleich und bevor AutoPSEO einen live geschalteten Artikel ändert |
articles.adopted | Aufruf | Nachdem ein Scan Artikel importiert hat, die Ihre Website exportiert hat |
site.handover | Aufruf | Wenn 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.
| Feld | Bedeutung |
|---|---|
id | Die ID des Artikels in AutoPSEO. Sie ändert sich nie und ist daher ein sicherer Schlüssel |
slug | Der letzte Teil der Adresse. Bei typisierten Seiten enthält er mit / verbundene Segmente, etwa google/software-engineer |
path | Die 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, langs | Die Hauptsprache und alle Sprachen dieser Revision |
rev | Die Revisionsnummer |
url | Die 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, dateModified | YYYY-MM-DD. Das Veröffentlichungsdatum bleibt über spätere Revisionen hinweg unverändert |
contentHash | Ein Fingerabdruck der Revision |
writer | Wer die Revision geschrieben hat: platform (AutoPSEO), mcp (Ihre eigene KI), user, import (der eigene Text Ihrer Website) oder aggregate (eine Listenseite) |
copy | Der Artikel in der Hauptsprache |
translations | Die anderen Sprachen, nach Sprachcode geordnet |
category | Die 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 |
parts | Bei 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 |
cover | Ein 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 |
basedOn | Bei article.updated: die Version Ihrer Seite, auf der diese Revision aufbaut (siehe Konflikte unten) |
redirectTo | Bei 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
| Feld | Bedeutung |
|---|---|
title, metaDescription, h1 | Seitentitel, Meta-Beschreibung und Überschrift |
directAnswer | Die kurze Antwort, die zuerst angezeigt wird |
keyTakeaways | Eine Liste von Strings |
sections | [{ heading, bodyHtml }] |
faq | [{ q, a }] |
sources | [{ title, url, lastVerified? }] |
question | Optional: die Frage der Suchenden in deren eigenen Worten, wenn Ihr Format sie verlangt |
category | Optional: eine der Kategorien Ihrer Website |
lists | Optional: 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.
| Feld | Wirkung |
|---|---|
url | Die https://-Adresse, unter der der Artikel jetzt liegt. AutoPSEO speichert sie als URL des Artikels |
version | Die Version Ihrer Seite, ein undurchsichtiger String mit höchstens 128 Zeichen. Er kommt beim nächsten Update als basedOn zurück |
urls | Die 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 Antwort | Wie AutoPSEO sie liest | Was passiert |
|---|---|---|
Ein 4xx mit { "error": "unknown_event" } oder ein 501 | Sie behandeln diesen Befehl noch nicht | Die Zustellung wartet, ohne Wiederholungen |
Ein 409 mit { "error": "conflict" } | Das Update wurde auf einer Version aufgebaut, die Ihre Seite nicht mehr hat | Die 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.remove | Eine Ablehnung mit Ihrer Begründung | Wird 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 4xx | Ein vorübergehender Fehler | Wird 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
testpro Befehl. Der Body ist{ id, event: "test", occurredAt, site, test: { event: "<command>", ... } }, wobeitestein Beispiel in der Form des echten Befehls enthält, das sich auf<type path>/autopseo-connection-testbezieht, 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 mitok: falseund einem anderen Fehlercode, etwa bei der Depublizierung einer Adresse, die Sie nicht haben, zählt trotzdem als verbunden. Das Beispiel steht untertest, 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_eventmarkiert ihn als nicht verbunden. - Die Ping-Antwort. Befehle, die Ihre Antwort in
autopseo.eventsauflistet, 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"
}
}
| Feld | Bedeutung |
|---|---|
version | 1. Das Objekt wird ignoriert, wenn die Version nicht mindestens 1 ist |
events | Die Befehle, die Sie behandeln, als „deklariert“ angezeigt, bis sie getestet sind |
covers | true, 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 |
export | Was site.export übergibt: articles, topics, profile |
adopt | Sie akzeptieren articles.adopted, sodass ein vollständiger Scan Ihre vorhandenen Artikel übernehmen kann |
handover | Sie akzeptieren site.handover |
types | Die Schlüssel der Content-Typen, die Sie akzeptieren (bis zu 50) |
writer | Wer 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. Ohneurlssendet es dieurlIhrer 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-Afteres 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"] }
}
}
| Feld | Bedeutung |
|---|---|
path | Die eigene Adresse des Werts auf Ihrer Website |
type | Der Content-Typ als { key, name, path } |
part | Der 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 |
takes | Bei path.add: die Werte der anderen Teile, nach denen Menschen zusammen mit ihm suchen |
redirectTo | Bei 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 Sieexportmitarticlesundadopt: truedeklariert haben. Eine 400-Antwort bedeutet „nicht unterstützt“. Mitpaths: ["/answers/x"]undlimit: 1wird 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 sendetsince, die Zeit des letzten Abrufs.what: "profile"läuft in jedem Scan. Antworten Sie mit{ "profile": { types, languages, rules, categories, format } }, jeweils optional:typesals[{ key, name, path }]undrulesmitbrandTerms,synonyms,noise,onTopicundoffTopic.
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.