เอกสารประกอบ
Delivery webhook: รับบทความบนเว็บไซต์ของคุณเอง
Webhook ที่มีลายเซ็นสำหรับส่งบทความของ AutoPSEO ไปยังเว็บไซต์ของคุณ: header, แต่ละ event และ payload, คำตอบที่ต้องตอบกลับ, การลองส่งซ้ำ, คำสั่งที่รอดำเนินการ และ IndexNow
AutoPSEO ส่งบทความทุกชิ้นที่เผยแพร่ อัปเดต หรือนำออก ไปยังเว็บไซต์ของคุณเองในรูปแบบ HTTPS POST ที่มีลายเซ็นกำกับ เว็บไซต์ของคุณจัดเก็บบทความและแสดงผลด้วยเทมเพลตของคุณเอง เนื้อหาจึงอยู่ในระบบของคุณและยังใช้งานได้ต่อไปแม้ในวันที่คุณเลิกใช้บริการ หน้านี้เป็นเอกสารอ้างอิงสำหรับนักพัฒนาที่สร้างฝั่งรับ ครอบคลุมการลงลายเซ็น ทุก event พร้อม payload คำตอบที่ AutoPSEO อ่าน การลองส่งซ้ำ คำสั่งที่รอดำเนินการ และ IndexNow
ตั้งค่า webhook
- เปิดหน้า Content ของ property แล้วไปที่ Where articles are published
- กรอก Webhook address on your site ต้องเป็น URL แบบ
https://ที่ใช้ชื่อโฮสต์สาธารณะ ระบบไม่รับที่อยู่ IP และlocalhostAutoPSEO ไม่ตาม redirect ดังนั้นต้องระบุที่อยู่ปลายทางจริง - กรอก Your site's address เช่น
https://example.compath ของแต่ละประเภทเนื้อหาจะต่อท้ายที่อยู่นี้ ดังนั้นบทความของประเภทที่อยู่ที่/answersจะอยู่ที่https://example.com/answers/<slug> - เลือก Save and send a test AutoPSEO จะสร้าง signing secret (
whsec_ตามด้วยเลขฐานสิบหก 48 ตัว) และส่ง ping ไปยังที่อยู่ของคุณ ฝั่งรับที่ตรวจลายเซ็นจะไม่ผ่าน ping ครั้งแรกนี้ เพราะยังไม่มี secret - เลือก Show หรือ Copy เพื่อดู Signing secret เก็บไว้เป็น secret บนเว็บไซต์ของคุณ แล้วเลือก Send a test
- ดูที่ What your site handles: แต่ละคำสั่งจะแสดงสถานะ Connected, Not connected, Declared, not tested หรือ Not tested
property ใหม่จะเริ่มต้นโดยให้ AutoPSEO โฮสต์บทความไว้ที่ที่อยู่ของ AutoPSEO เอง (ดู Hosting) และการโฮสต์กับ webhook ทำงานคู่กันได้ เมื่อเว็บไซต์ของคุณแสดงบทความได้แล้ว ให้เลือก Stop hosting on AutoPSEO บทความที่เผยแพร่อยู่ทั้งหมดจะย้ายไปอยู่ที่ที่อยู่ของเว็บไซต์คุณ และถูกส่งไปยัง webhook ในรูปของ article.published ปุ่มนั้นต้องมี webhook ที่ตอบ test ล่าสุดสำเร็จ และต้องมีที่อยู่เว็บไซต์ของคุณ ขณะที่ปิดการโฮสต์อยู่ จะลบ webhook ไม่ได้
New secret จะแทนที่ signing secret ทันที และ secret เดิมจะใช้ตรวจสอบไม่ได้ในทันที ให้ตั้ง secret ใหม่บนเว็บไซต์ของคุณทันทีหลังสร้าง
AutoPSEO จะจัดคิวการส่งให้เฉพาะ webhook ที่ ping ครั้งล่าสุดได้รับคำตอบ 2xx ถ้า ping ล้มเหลว (ตอนบันทึก ตอน Send a test หรือตอนเริ่มสแกนเว็บไซต์) webhook จะถูกทำเครื่องหมายว่ามีข้อผิดพลาด และบทความที่เผยแพร่ระหว่างนั้นจะไม่ถูกจัดคิวส่งให้ webhook นี้ ดังนั้นให้ ping ตอบกลับได้อยู่เสมอ และเลือก Send a test หลังแก้ไขปัญหาที่ขัดข้อง
Header และลายเซ็น
ทุกคำขอเป็น POST ที่มี JSON เป็น body และมี header ดังนี้
| Header | ค่า |
|---|---|
Content-Type | application/json |
User-Agent | AutoPSEO-Webhook/1 |
X-AutoPSEO-Event | ชื่อ event ซึ่งอยู่ใน body ในฟิลด์ event ด้วย |
X-AutoPSEO-Delivery | id ของการส่ง ซึ่งอยู่ใน body ในฟิลด์ id ด้วย และเหมือนกันในทุกการลองส่งซ้ำของการส่งครั้งเดียวกัน |
X-AutoPSEO-Timestamp | เวลา Unix เป็นวินาทีที่ลงลายเซ็นการส่งครั้งนี้ |
X-AutoPSEO-Signature | sha256= ตามด้วย HMAC-SHA256 แบบเลขฐานสิบหกของ <timestamp>.<raw body> |
วิธีตรวจสอบคำขอ:
- นำค่า timestamp จาก header มาต่อด้วยจุด แล้วตามด้วย body ของคำขอแบบดิบตามที่ได้รับมาทุกประการ ก่อนทำการ parse JSON
- คำนวณ HMAC-SHA256 จากสตริงนั้น โดยใช้ signing secret ทั้งหมดตามที่แอปแสดง รวมคำนำหน้า
whsec_เป็นคีย์ในรูปไบต์ UTF-8 - แปลงผลลัพธ์เป็นเลขฐานสิบหก แล้วเปรียบเทียบกับส่วนของ
X-AutoPSEO-Signatureที่อยู่หลังsha256=ด้วยการเปรียบเทียบแบบใช้เวลาคงที่ หากไม่ตรงกันให้ปฏิเสธคำขอ
การส่งแต่ละครั้งจะลงลายเซ็นใหม่พร้อม timestamp ของตัวเอง คุณจึงปฏิเสธคำขอที่ timestamp ห่างจากนาฬิกาของคุณมากได้ (ปกติใช้ช่วงห้านาที) โดยไม่กระทบการลองส่งซ้ำ ส่วน HMAC เองนิยามไว้ใน RFC 2104
ทุก body มี id, event, occurredAt (ISO 8601, UTC) และ site: { propertyId, domain }
Events
| Event | ประเภท | AutoPSEO ส่งเมื่อใด |
|---|---|---|
ping | Check | เมื่อคุณบันทึก webhook, เมื่อ Send a test และตอนเริ่มสแกนเว็บไซต์ทุกครั้ง |
test | Check | ทันทีหลัง ping สำเร็จ ส่งหนึ่งครั้งต่อหนึ่งคำสั่ง |
article.published | Delivery | บทความเผยแพร่เป็นครั้งแรก และบทความที่เผยแพร่อยู่ทั้งหมดเมื่อคุณหยุดโฮสต์ |
article.updated | Delivery | revision ใหม่เผยแพร่ (รีเฟรช ปรับปรุง ย้อนกลับ เผยแพร่อีกครั้งหลังนำออก เพิ่มภาษา) หรือมีการเพิ่มภาพปก |
article.unpublished | Delivery | บทความถูกนำออก หรือถูกรวมเข้ากับอีกหน้าหนึ่ง |
path.add | Delivery | ค่าใหม่ของประเภทเนื้อหาแบบ typed ได้รับการอนุมัติ |
path.remove | Delivery | มีผู้ขอให้นำ path ของค่าหนึ่งออก |
path.check | Call | ก่อนที่ AutoPSEO จะวางแผนหน้าที่ที่อยู่ซึ่งเว็บไซต์ของคุณยังไม่มีหน้า หรือเสนอค่าใหม่ |
site.export | Call | ระหว่างสแกนเว็บไซต์ ในการซิงก์หัวข้อรายวัน และก่อนที่ AutoPSEO จะแก้ไขบทความที่เผยแพร่อยู่ |
articles.adopted | Call | หลังการสแกนนำเข้าบทความที่เว็บไซต์ของคุณส่งออก |
site.handover | Call | เมื่อมีผู้ส่งมอบการเขียนประเภทเนื้อหาให้ AutoPSEO หรือส่งกลับ |
การส่งมอบ (delivery) ผ่าน outbox: บันทึกไว้ใน database batch เดียวกับการเปลี่ยนแปลง ส่งทันที ลองส่งซ้ำ และรอเว็บไซต์ของคุณได้ ส่วน call เป็นการขอและตอบแบบปกติ AutoPSEO อ่านคำตอบทันที ช่วงเวลาที่คำสั่งเกี่ยวกับ path ถูกส่งอธิบายไว้ใน Typed pages and new paths
Payload ของบทความ
article.published และ article.updated มีบทความทั้งชิ้น รวมทุกภาษา:
{
"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
}
}
}
ตัวอย่างนี้ย่อรายการภาษาเยอรมันไว้ แต่ละ translation มีรูปแบบเดียวกับ copy
| ฟิลด์ | ความหมาย |
|---|---|
id | id ของบทความใน AutoPSEO ไม่เปลี่ยนแปลง จึงใช้เป็นคีย์ได้อย่างปลอดภัย |
slug | ส่วนท้ายของที่อยู่ ในหน้าแบบ typed จะเป็นหลายส่วนที่เชื่อมด้วย / เช่น google/software-engineer |
path | ที่อยู่เทียบกับเว็บไซต์ของคุณ คือ path ของประเภทเนื้อหาตามด้วย slug และคงที่เมื่อหน้านั้นถูกสร้างแล้ว |
type | { key, name, path } โดย key คงที่ และ path คือที่ที่บทความของประเภทนั้นอยู่ (ว่างหมายถึงรากของเว็บไซต์) |
lang, langs | ภาษาหลักและทุกภาษาของ revision นี้ |
rev | หมายเลข revision |
url | ที่อยู่แบบเต็มตามที่ AutoPSEO ทราบ: ที่อยู่บนเว็บไซต์ของคุณเมื่อปิดการโฮสต์แล้ว หรือ url ที่ฝั่งรับของคุณตอบกลับล่าสุด |
datePublished, dateModified | รูปแบบ YYYY-MM-DD วันที่เผยแพร่คงเดิมตลอด revision ต่อๆ ไป |
contentHash | ลายนิ้วมือของ revision |
writer | ผู้เขียน revision: platform (AutoPSEO), mcp (AI ของคุณเอง), user, import (ข้อความจากเว็บไซต์ของคุณเอง) หรือ aggregate (หน้ารายการ) |
copy | บทความในภาษาหลัก |
translations | ภาษาอื่นๆ โดยใช้รหัสภาษาเป็นคีย์ |
category | หมวดหมู่ของเว็บไซต์คุณสำหรับบทความนี้ เมื่อรูปแบบของคุณมีหมวดหมู่ |
topic | { externalId, keyword } เมื่อบทความถูกเขียนสำหรับหัวข้อที่เว็บไซต์ของคุณส่งมอบมา คุณจึงปิดหัวข้อนั้นได้ |
parts | ในหน้าแบบ typed คือค่าของแต่ละส่วน: { "company": "google", "role": "software-engineer" } |
kind | "list" ในหน้ารายการ copy ของหน้าดังกล่าวคือ { title, h1, items: [{ from, text }] } โดย from คือ path ของหน้าที่แต่ละรายการมา และ parts ระบุค่าที่หน้านั้นรวบรวม |
cover | ภาพปกที่ AutoPSEO สร้างให้บทความ ให้ดึงมาครั้งเดียวแล้วเก็บสำเนาของคุณเอง ที่อยู่จะไม่เปลี่ยน เพราะชื่อไฟล์คือ SHA-256 ของภาพ |
basedOn | ใน article.updated: เวอร์ชันของหน้าคุณที่ revision นี้เขียนขึ้นจากมัน (ดูเรื่อง conflict ด้านล่าง) |
redirectTo | ใน article.unpublished หลังการรวมหน้า: ปลายทางใหม่ของที่อยู่เดิม ให้ตอบด้วย 301 และอย่านำสิ่งใหม่มาไว้ตรงนั้น |
article.unpublished มีฟิลด์ใน article เหมือนกัน ยกเว้น copy, translations, category และ cover
ภายใน copy
| ฟิลด์ | ความหมาย |
|---|---|
title, metaDescription, h1 | ชื่อหน้า meta description และหัวเรื่อง |
directAnswer | คำตอบสั้นที่แสดงก่อน |
keyTakeaways | รายการสตริง |
sections | [{ heading, bodyHtml }] |
faq | [{ q, a }] |
sources | [{ title, url, lastVerified? }] |
question | ไม่บังคับ: คำถามของผู้ค้นหาด้วยถ้อยคำของเขาเอง เมื่อรูปแบบของคุณต้องการ |
category | ไม่บังคับ: หนึ่งในหมวดหมู่ของเว็บไซต์คุณ |
lists | ไม่บังคับ: รายการข้อความล้วนแยกตามคีย์ สำหรับประเภทเนื้อหาที่ต้องการ (เช่นตัวอย่างคำถามของคู่มือสัมภาษณ์) |
ในบทความที่ AutoPSEO เขียน bodyHtml ใช้เฉพาะ p, ul, li, strong, em, a และ code และลิงก์จะเก็บไว้เฉพาะ href ข้อความที่นำมาจากเว็บไซต์ของคุณเองจะคง HTML ของคุณไว้ หากเทมเพลตของคุณต้องการ ก็ให้ส่งผ่าน sanitiser ของคุณเองอีกชั้น
สิ่งที่ฝั่งรับของคุณตอบกลับ
2xx ใดๆ ถือว่าการส่งสำเร็จ AutoPSEO ยังอ่านออบเจ็กต์ JSON ที่ไม่บังคับจาก body ของคำตอบด้วย โดยต้องมีขนาดไม่เกิน 64 KB เพราะคำตอบที่ใหญ่กว่านั้นจะนับเป็นความล้มเหลว
| ฟิลด์ | ผล |
|---|---|
url | ที่อยู่แบบ https:// ที่บทความอยู่ในตอนนี้ AutoPSEO จะบันทึกเป็น URL ของบทความ |
version | เวอร์ชันของหน้าคุณ เป็นสตริงทึบความยาวไม่เกิน 128 ตัวอักษร จะกลับมาเป็น basedOn ในการอัปเดตครั้งถัดไป |
urls | ที่อยู่แบบ https:// ที่การส่งครั้งนี้เปลี่ยนแปลง สำหรับ IndexNow (ทุกภาษา หรือที่อยู่ที่คุณเลิกให้บริการ) [] หมายถึงไม่มีอะไรเปลี่ยน |
cover | "wanted" เมื่อคุณไม่มีภาพปกของบทความและการส่งครั้งนั้นไม่ได้แนบมา AutoPSEO จะสร้างภาพแล้วส่งใน article.updated |
คำตอบที่เป็นข้อผิดพลาดมีความหมายต่างกัน:
| คำตอบของคุณ | AutoPSEO ตีความว่า | สิ่งที่เกิดขึ้น |
|---|---|---|
4xx พร้อม { "error": "unknown_event" } หรือ 501 | คุณยังไม่รองรับคำสั่งนี้ | การส่งจะรอ โดยไม่ลองส่งซ้ำ |
409 พร้อม { "error": "conflict" } | การอัปเดตเขียนจากเวอร์ชันที่หน้าของคุณไม่มีแล้ว | การส่งล้มเหลวโดยไม่ลองซ้ำ และจะอ่านบทความจากเว็บไซต์ของคุณอีกครั้งก่อนการเปลี่ยนแปลงครั้งถัดไป |
4xx อื่นที่ไม่ใช่ 408 หรือ 429 พร้อม { "error": "<code>", "details": [] } ตอบ path.add หรือ path.remove | การปฏิเสธพร้อมเหตุผลของคุณ | บันทึกไว้ ดู Typed pages and new paths |
| อย่างอื่นทั้งหมด: ไม่ตอบภายใน 10 วินาที, ข้อผิดพลาดของเครือข่าย, redirect, 408, 429, 5xx หรือ 4xx อื่น | ความล้มเหลวชั่วคราว | ลองส่งซ้ำแบบ backoff |
การลองส่งซ้ำและการส่งที่ถูกแทนที่
การส่งจะเกิดขึ้นทันทีที่บันทึกการเปลี่ยนแปลง และตารางเวลาทุกห้านาทีจะส่งสิ่งที่ถึงกำหนด โดยเรียงจากเก่าสุดก่อน หลังความล้มเหลวชั่วคราว AutoPSEO จะรอ 1 นาที จากนั้น 5 นาที 15 นาที 1 ชั่วโมง 3 ชั่วโมง 6 ชั่วโมง และ 12 ชั่วโมงระหว่างความพยายามแต่ละครั้ง เมื่อล้มเหลวครั้งที่แปด การส่งจะถูกทำเครื่องหมายว่าล้มเหลว และหน้า Content จะนับไว้ในป้ายสีแดงข้าง webhook
เมื่อมีการส่งที่ใหม่กว่าสำหรับบทความเดียวกัน การส่งเก่าที่ยังค้างอยู่จะถูกทำเครื่องหมายว่า superseded และจะไม่ถูกส่ง ทุกการส่งมีบทความทั้งชิ้น เว็บไซต์ของคุณจึงต้องการเพียงการส่งล่าสุดเท่านั้น คำสั่งเกี่ยวกับ path ของค่าเดียวกันก็ทำงานแบบเดียวกัน
ฝั่งรับของคุณอาจเห็นการส่งเดียวกันสองครั้ง เช่นเมื่อเก็บบทความแล้วแต่ตอบหลังเกิน timeout 10 วินาที ให้กำจัดรายการซ้ำด้วย X-AutoPSEO-Delivery หรือทำให้ทุกการเขียนเป็นแบบ idempotent โดยจัดเก็บตาม article.id นอกจากนี้ article.updated อาจมาถึงบทความที่คุณลบไปแล้วหลัง article.unpublished เพราะการเผยแพร่บทความที่นำออกไปแล้วอีกครั้งจะส่งการอัปเดต ให้ถือว่า update คือ "สร้างหรือแทนที่"
คำสั่งที่เว็บไซต์ของคุณยังไม่รองรับ
เว็บไซต์สามารถนำโปรโตคอลมาใช้ทีละคำสั่งได้ AutoPSEO จะเก็บสถานะของแต่ละคำสั่งและรู้ได้สามทาง:
- Tests หลัง ping สำเร็จทุกครั้ง AutoPSEO จะส่ง
testหนึ่งครั้งต่อหนึ่งคำสั่ง body คือ{ id, event: "test", occurredAt, site, test: { event: "<command>", ... } }โดยtestมีตัวอย่างที่มีรูปแบบเหมือนคำสั่งจริงและเกี่ยวกับ<type path>/autopseo-connection-testซึ่งเป็นที่อยู่ที่ไม่มีเว็บไซต์ใดมี ให้ตรวจสอบเหมือนกับของจริง ไม่เปลี่ยนแปลงอะไร และตอบด้วย 2xx:{ "test": "<command>", "ok": true }สำหรับ Connected,{ "test": "<command>", "ok": false, "error": "unknown_event" }สำหรับ Not connected คำตอบที่มีok: falseพร้อมรหัสข้อผิดพลาดอื่น เช่นการนำออกที่อยู่ซึ่งคุณไม่มี ยังนับว่า connected ตัวอย่างอยู่ใต้testเสมอ ไม่เคยอยู่ระดับบนสุด ฝั่งรับที่ไม่รู้จักเรื่อง test จึงไม่พบบทความอยู่ในนั้น - Real deliveries and calls 2xx ทำให้คำสั่งเป็น connected ส่วน
unknown_eventทำให้เป็น not connected - The ping answer คำสั่งที่คำตอบของคุณระบุไว้ใน
autopseo.eventsจะแสดงเป็น Declared, not tested จนกว่า test หรือ delivery จะบอกเพิ่มเติม
การส่งสำหรับคำสั่งที่ยังไม่ connected จะรอ หน้า Content แสดงรายการที่รออยู่ เช่น "Your site does not take articles down yet" พร้อมปุ่ม Done on the site สำหรับสิ่งที่คุณดำเนินการเองด้วยมือ AutoPSEO จะไม่ส่งรายการนั้นอีก เมื่อ test หรือ delivery ครั้งถัดมาพบว่าคำสั่งนั้น connected ทุกอย่างที่รออยู่จะถูกส่ง
คำตอบของ ping
ตอบ ping ด้วย 2xx เว็บไซต์ที่รับบทความอย่างเดียวไม่ต้องทำอะไรเพิ่ม หากต้องการบอก AutoPSEO ว่าเว็บไซต์ของคุณทำอะไรได้อีก ให้ใส่ออบเจ็กต์ autopseo:
{
"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"
}
}
| ฟิลด์ | ความหมาย |
|---|---|
version | 1 ออบเจ็กต์จะถูกมองข้ามหากไม่มี version ที่เป็น 1 ขึ้นไป |
events | คำสั่งที่คุณรองรับ แสดงเป็น declared จนกว่าจะทดสอบ |
covers | true เมื่อหน้าของคุณแสดงภาพปกกับบทความแต่ละชิ้น ตัวเขียนของ AutoPSEO จะสร้างภาพปกระหว่างเขียน การส่งครั้งแรกจึงมีภาพปกไปด้วย และ AI ของคุณเองจะถูกขอให้ส่งภาพมาให้ ดู Covers |
indexNow | { key, keyLocation? }: คีย์ IndexNow ที่คุณให้บริการ (ตัวอักษร ตัวเลข และขีดกลาง 8 ถึง 128 ตัว) และเมื่อไฟล์คีย์ไม่ได้อยู่ที่ /<key>.txt ให้ระบุตำแหน่ง https:// ของไฟล์ |
export | สิ่งที่ site.export ส่งมอบ: articles, topics, profile |
adopt | คุณรับ articles.adopted เพื่อให้การสแกนเต็มรูปแบบรับช่วงบทความที่มีอยู่ของคุณได้ |
handover | คุณรับ site.handover |
types | คีย์ประเภทเนื้อหาที่คุณรับ (สูงสุด 50) |
writer | ผู้เขียนบทความใหม่ของคุณในปัจจุบัน: site หรือ autopseo |
IndexNow สำหรับที่อยู่ที่คุณให้บริการ
เมื่อคำตอบ ping ของคุณประกาศ indexNow AutoPSEO จะ ping IndexNow ที่ api.indexnow.org หลังจากการส่งที่คุณรับ และเว็บไซต์ของคุณไม่ต้อง ping เอง
- ส่ง
urlsที่คำตอบของคุณระบุ หากไม่มีurlsจะส่งurlในคำตอบของคุณ ไม่เช่นนั้นก็ใช้ URL ของบทความurls: []คือไม่ส่งอะไร - ใส่เฉพาะที่อยู่บนโฮสต์ของคุณเอง ตามที่ IndexNow protocol กำหนด: โฮสต์ของ
keyLocationหรือไม่เช่นนั้นก็โฮสต์ของที่อยู่เว็บไซต์ของคุณ - ในแต่ละรอบการส่งจะ ping หนึ่งครั้งต่อเว็บไซต์ ได้สูงสุด 10,000 ที่อยู่
- ยอมรับ 200 และ 202 ส่วน 429, 5xx หรือไม่มีคำตอบ จะเก็บที่อยู่ไว้และลองใหม่หลังหนึ่งชั่วโมง โดยเพิ่มเป็นสองเท่าทุกครั้งจนถึงหนึ่งวัน (นานกว่านั้นเมื่อ
Retry-Afterขอ) พร้อมที่อยู่ที่การส่งครั้งหลังเพิ่มเข้ามา ส่วน 403 (คีย์ไม่ถูกต้อง) หรือ 422 (ที่อยู่ไม่อยู่บนโฮสต์) จะบันทึกไว้แล้วทิ้ง - หน้า Content แสดง ping ครั้งล่าสุด: เวลา จำนวนที่อยู่ สถานะ HTTP และการลองครั้งถัดไป
- ขณะที่ AutoPSEO โฮสต์บทความด้วย หน้าที่โฮสต์จะถูก ping ด้วยคีย์ของ AutoPSEO เอง และจะไม่ใช้คีย์ของคุณ
เครื่องมือ IndexNow ภายใต้ Indexing เป็นตัวส่งแบบแมนนวลที่แยกต่างหาก ดู IndexNow
Conflict: ข้อความบนเว็บไซต์ของคุณคือต้นฉบับ
version ในคำตอบของคุณ (หรือ version ของบทความในการส่งออกของคุณ) คือสิ่งที่ AutoPSEO เชื่อว่าหน้าของคุณอยู่ที่เวอร์ชันนั้น ทุก article.updated จะแนบค่านี้มาเป็น basedOn หากหน้านั้นเปลี่ยนไปแล้วบนเว็บไซต์ของคุณ เช่นหลังบรรณาธิการแก้ไข ให้ตอบ 409 พร้อม { "error": "conflict" } และอย่าเก็บอะไร การส่งจะล้มเหลวโดยไม่ลองซ้ำ
ก่อนที่การเปลี่ยนแปลงที่อยู่ในคิวของบทความที่เผยแพร่อยู่จะถูกเขียน AutoPSEO จะอ่านบทความนั้นจากเว็บไซต์ของคุณอีกครั้งด้วย site.export และ paths หากเว็บไซต์ของคุณรองรับ site.export หากเวอร์ชันของคุณเปลี่ยนไปแล้ว ข้อความของคุณจะกลายเป็น revision ถัดไปของบทความ (writer import) และการอัปเดตที่ตามมาจะแนบเวอร์ชันปัจจุบันของคุณเป็น basedOn
path.add และ path.remove
สองการส่งนี้เกี่ยวกับค่าหนึ่งค่าของประเภทเนื้อหาแบบ typed เช่นบริษัทใน /interviews/{company}/{role} โดยมีออบเจ็กต์ path แทน article:
{
"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"] }
}
}
| ฟิลด์ | ความหมาย |
|---|---|
path | ที่อยู่ของค่านั้นบนเว็บไซต์ของคุณ |
type | ประเภทเนื้อหา ในรูป { key, name, path } |
part | ส่วนของที่อยู่ที่ค่านั้นเติมเต็ม |
value | { value, name } และใน path.add จะมี aliases, url, sameAs, description และ facts ที่ตัวเขียนของ AutoPSEO รวบรวม แต่ละ fact มี text, source, lastVerified และ tier เป็น official หรือ reported |
takes | ใน path.add: ค่าของส่วนอื่นๆ ที่ผู้คนค้นหาคู่กับค่านี้ |
redirectTo | ใน path.remove: ปลายทางที่ที่อยู่ของค่านั้นควรนำไปต่อจากนี้ ด้วย 301 |
ตอบ 2xx เมื่อคุณรับคำสั่งแล้ว ใน path.add คุณเพิ่ม url ที่ค่านั้นอยู่ในตอนนี้ได้ และใน path.remove เพิ่ม urls ที่คุณเลิกให้บริการ ซึ่งจะส่งต่อไปยัง IndexNow หากต้องการปฏิเสธ ให้ตอบ 4xx พร้อม { "error": "<code>", "details": ["<why>"] } ใน path.add รหัส invalid_value จะส่งค่ากลับไปยังตัวเขียนพร้อมเหตุผลของคุณ (การปฏิเสธครั้งที่สามถือเป็นที่สุด) ส่วนรหัสอื่นจะปฏิเสธค่านั้นถาวร ใน path.remove รหัส not_found นับว่าเสร็จแล้ว และรหัสอื่น เช่น 409 in_use จะคงทุกอย่างไว้ตามเดิม การทำเครื่องหมายคำสั่ง path ที่รออยู่ว่า Done on the site นับเป็นคำตอบ "ได้" ของเว็บไซต์คุณ
Call: คำขอที่ AutoPSEO อ่านคำตอบ
call ลงลายเซ็นเหมือน delivery โดย call จะรอได้สูงสุด 60 วินาทีเป็นค่าเริ่มต้น และ 30 วินาทีสำหรับการอ่านซ้ำและการตรวจสอบ path และรับคำตอบได้ถึง 24 MB คำตอบใดที่ไม่ใช่ 2xx ถือว่า call ล้มเหลว
site.export
คำขอมี export: { what, cursor, limit } และเพิ่ม since (เวลา ISO) สำหรับ topics หรือ paths สำหรับการอ่านซ้ำ ตอบ { "items": [], "next": "<cursor>" } AutoPSEO จะถามซ้ำโดยตั้ง cursor เป็น next ของคุณ จนกว่าจะว่างหรือเป็น null
what: "articles"ทำงานในการสแกนเว็บไซต์เต็มรูปแบบ หน้าละ 10 รายการ เมื่อคุณประกาศexportที่มีarticlesและadopt: trueคำตอบ 400 หมายถึง "ไม่รองรับ" ด้วยpaths: ["/answers/x"]และlimit: 1จะเป็นการอ่านบทความหนึ่งชิ้นซ้ำ ให้ตอบเฉพาะบทความนั้นwhat: "topics"ทำงานในทุกการสแกนและการซิงก์รายวัน หน้าละ 50 รายการ การซิงก์รายวันจะส่งsinceซึ่งเป็นเวลาของการดึงครั้งล่าสุดwhat: "profile"ทำงานในทุกการสแกน ตอบ{ "profile": { types, languages, rules, categories, format } }ซึ่งทุกฟิลด์ไม่บังคับ:typesเป็น[{ key, name, path }]และrulesมีbrandTerms,synonyms,noise,onTopicและoffTopic
บทความที่ส่งออกต้องมี externalId, slug (ตัวอักษรและตัวเลขของอักษรใดก็ได้ และขีดกลาง ยาวไม่เกิน 200 ตัวอักษร หลายส่วนเชื่อมด้วย /), path ที่ลงท้ายด้วย slug นั้น และ copy ที่มีอย่างน้อย title, h1 และหนึ่ง section ฟิลด์ที่ไม่บังคับ: lang (ค่าเริ่มต้น en), type (คีย์ประเภทเนื้อหา), url, keyword, category, datePublished, dateModified, version, translations, owner (id บทความของ AutoPSEO เมื่อรับช่วงแล้ว) และ maintenance: { kind: "refresh" or "optimize", reason } ซึ่งจะจัดคิวงานนั้น บทความที่ส่งออกแต่ละชิ้นจะกลายเป็นบทความที่ถูกจัดการ โดยใช้ข้อความของคุณเป็น revision แรก และไม่มีอะไรถูกส่งกลับ การส่งออกซ้ำปลอดภัย และบทความที่ AutoPSEO นำออกไปแล้วจะไม่กลับมาผ่านการส่งออก
รายการ topic คือ { keyword, status?, volume?, source?, externalId?, type?, reason? } โดย status เป็น queued, planned หรือ rejected
articles.adopted
หลังนำเข้า AutoPSEO จะขอให้คุณอนุญาตให้จัดการบทความใหม่: { "articles": [{ "externalId", "articleId", "slug", "path" }] } ตอบ { "adopted": ["<externalId>"], "refused": [{ "externalId": "<id>", "reason": "<why>" }] } บทความที่คุณไม่ยืนยันจะถูกนำออกจาก AutoPSEO อีกครั้ง เว้นแต่ AutoPSEO เขียน revision ของบทความนั้นไปแล้ว
site.handover
{ "handover": { "types": ["answers"], "writer": "autopseo" } } ส่งเมื่อมีผู้เลือก Let AutoPSEO write them หรือ Hand writing back to the site ในหน้า Your site ของ property ตอบ { "ok": true, "writer": "autopseo" } หลังส่งมอบให้ AutoPSEO เว็บไซต์ของคุณจะส่งหัวข้อที่ตรวจสอบแล้วให้ AutoPSEO แทนที่จะเขียนเอง
path.check
{ "paths": [{ "path": "/interviews/google/data-engineer" }, { "path": "/interviews/jane-street", "new": { "part": "company", "value": "jane-street", "name": "Jane Street" } }] } ตอบหนึ่งผลลัพธ์ต่อหนึ่ง path: { "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."] }] } การปฏิเสธจะถูกเก็บไว้พร้อมเหตุผลของคุณและจะไม่ถามอีก ส่วน path ที่คุณเว้นไว้จะถูกถามอีกในภายหลัง
ฝั่งรับขั้นต่ำ
Cloudflare Worker ที่ตรวจลายเซ็นด้วย Web Crypto API จัดการสามคำสั่งของบทความ และปฏิเสธคำสั่งที่เหลือด้วย unknown_event โดยเก็บบทความไว้ใน KV namespace ที่ผูกเป็น ARTICLES และเก็บ signing secret ไว้ใน Worker secret ชื่อ 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);
},
};
บน Node.js ให้ตรวจสอบด้วยวิธีเดียวกันโดยใช้ createHmac และ timingSafeEqual กับ body แบบดิบที่เฟรมเวิร์กของคุณได้รับ:
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));
}
ขั้นต่อไป: สิ่งที่ไปถึง webhook ถูกกำหนดโดยการเขียนและการตรวจทานและการบำรุงรักษา ส่วนภาพรวมของทั้งกระบวนการอยู่ในภาพรวมของ content engine