Dành cho lập trình viên
Tài liệu API: đăng bài lên website tự xây
Website có trang quản trị riêng (Next.js, Laravel, PHP, Python…) nhận bài từ AI Brand Visibility qua một địa chỉ API do bạn tạo. Trang này mô tả chính xác dữ liệu chúng tôi gửi, cách xác thực và cách trả lời.
Cách hoạt động
- Bạn tạo một địa chỉ nhận bài (POST, HTTPS) trên website, ví dụ https://website-cua-ban.com/api/blog/receive, và một mã bí mật.
- Trong AI Brand Visibility: Cài đặt website › Kết nối › API tuỳ chỉnh, dán địa chỉ và mã bí mật (hoặc bấm “Tạo mã”), rồi bấm Kiểm tra & lưu — hệ thống gửi một yêu cầu thử với "event": "ping".
- Mỗi khi bạn duyệt bài (hoặc Autopilot đăng), chúng tôi gửi bài viết tới địa chỉ đó. Mỗi bài một yêu cầu, thời gian chờ tối đa 30 giây.
- Website lưu bài (theo slug) và trả về id và url của bài. Có url, hệ thống mới yêu cầu Google lập chỉ mục và tự chia sẻ lên mạng xã hội.
Xác thực
Mỗi yêu cầu có hai header: X-Webhook-Secret chứa đúng mã bí mật, và X-Signature = "sha256=" + HMAC-SHA256 của nội dung (body) gốc với khoá là mã bí mật. Kiểm tra một trong hai (nên dùng X-Signature) và trả 401 nếu sai.
Tính chữ ký trên body gốc (raw bytes) trước khi parse JSON — parse rồi stringify lại sẽ ra chữ ký khác. Dùng so sánh an toàn (timingSafeEqual / hash_equals / compare_digest).
Dữ liệu gửi
Body là JSON (UTF-8). Các trường:
POST https://your-site.com/api/blog/receive
Content-Type: application/json; charset=utf-8
X-Webhook-Secret: <your secret>
X-Signature: sha256=<HMAC-SHA256 of the raw body, key = your secret>
{
"event": "article.published",
"title": "Hướng dẫn chọn sàn gỗ công nghiệp",
"slug": "huong-dan-chon-san-go-cong-nghiep",
"primary_keyword": "sàn gỗ công nghiệp",
"content_html": "<h2 id=\"...\">...</h2><p>...</p>",
"content_md": "## ...",
"meta_description": "…",
"excerpt": "…",
"faq": [{ "q": "…", "a": "…" }],
"cover_image": "https://images.pexels.com/…jpg",
"images": [
{ "url": "https://images.pexels.com/…jpg", "alt": "…", "role": "cover" },
{ "url": "https://…/inline.jpg", "alt": "…", "role": "inline" }
],
"category": "tin-tuc",
"status": "publish",
"test": false
}| event | "ping" (kiểm tra kết nối), "article.published" (đăng công khai) hoặc "article.draft" (lưu nháp). |
| title | Tiêu đề bài viết. |
| slug | Đường dẫn gợi ý cho bài. Dùng làm khoá khi lưu: gửi lại cùng slug nghĩa là cập nhật bài đó. |
| primary_keyword | Từ khoá chính của bài (có thể null). |
| content_html | Nội dung HTML sẵn sàng hiển thị: heading có id, mục lục (nếu bật), phần Câu hỏi thường gặp. Nên dùng trường này. |
| content_md | Cùng nội dung ở dạng Markdown, nếu website của bạn tự chuyển Markdown. |
| meta_description | Mô tả meta; excerpt có cùng giá trị để dùng làm đoạn tóm tắt. |
| faq | Danh sách hỏi–đáp [{q, a}] (đã có sẵn trong content_html). |
| cover_image | Link ảnh bìa (có thể null). |
| images | Mọi ảnh của bài kèm vai trò cover/inline — dùng khi website muốn tải ảnh về và tự lưu (S3, thư mục uploads…) thay vì dùng link ngoài. |
| category | Danh mục bạn đã nhập ở phần kết nối (mặc định "tin-tuc"). |
| status | "publish" hoặc "draft" — chú ý: "publish", không phải "published". |
| test | true với yêu cầu kiểm tra kết nối: trả 200 và không lưu gì. |
Phản hồi
Trả mã 200, 201 hoặc 202 với JSON {id, url, status}. url là link công khai của bài (chỉ khi đã đăng). Không có url, bài vẫn được ghi nhận nhưng hệ thống không lập chỉ mục hay chia sẻ, để không bao giờ gửi link sai. Nếu lỗi, trả mã 4xx/5xx kèm {"error": "..."} — nội dung này hiện cho người dùng.
HTTP/1.1 201 Created
Content-Type: application/json
{ "id": "123", "url": "https://your-site.com/blog/huong-dan-chon-san-go-cong-nghiep", "status": "publish" }Code mẫu
Một địa chỉ nhận bài hoàn chỉnh: xác thực chữ ký, trả lời ping, lưu theo slug và trả về id/url. Thay phần lưu dữ liệu bằng cơ sở dữ liệu của bạn.
import crypto from "node:crypto";
import { NextResponse } from "next/server";
const SECRET = process.env.BLOG_WEBHOOK_SECRET ?? "";
export async function POST(req: Request) {
// Verify the signature on the RAW body, before parsing it.
const raw = await req.text();
const expected = "sha256=" + crypto.createHmac("sha256", SECRET).update(raw).digest("hex");
const got = req.headers.get("x-signature") ?? "";
if (got.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected))) {
return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
}
const body = JSON.parse(raw);
// The connection test: answer OK, save nothing.
if (body.event === "ping" || body.test) return NextResponse.json({ ok: true });
const published = body.status === "publish";
// Save or update by slug in your own database, e.g. with Prisma:
const post = await db.post.upsert({
where: { slug: body.slug },
create: { slug: body.slug, title: body.title, content: body.content_html, excerpt: body.excerpt, coverImage: body.cover_image, published },
update: { title: body.title, content: body.content_html, excerpt: body.excerpt, coverImage: body.cover_image, published },
});
return NextResponse.json(
{ id: String(post.id), url: published ? `https://your-site.com/blog/${post.slug}` : null, status: published ? "publish" : "draft" },
{ status: 201 },
);
}Lỗi thường gặp
401 Unauthorized
Mã bí mật trên website khác với mã đã lưu ở AI Brand Visibility, hoặc chữ ký được tính trên JSON đã parse thay vì body gốc. Dán lại cùng một mã ở cả hai nơi.
405 Method Not Allowed
Địa chỉ chưa nhận phương thức POST (hoặc server chuyển hướng sang địa chỉ khác và đổi thành GET). Khai báo route POST và dùng đúng địa chỉ cuối cùng, kể cả dấu / ở cuối.
“Địa chỉ trả về một trang web chứ không phải API”
Địa chỉ sai, hoặc bị chuyển tới trang đăng nhập / trang 404 của website. Mở thử địa chỉ bằng công cụ như curl với POST để xem nó trả gì.
Bài đã gửi nhưng không hiện trên website
Thường do website chỉ coi status "published" là công khai. Chúng tôi gửi "publish" — hãy chấp nhận "publish" (và "published" nếu cần) khi đặt trạng thái công khai.
Bài đăng rồi nhưng dashboard không có link
Website chưa trả về url trong phản hồi. Trả về link công khai thật của bài để có chỉ mục Google và chia sẻ tự động.
Quá thời gian chờ
Mỗi yêu cầu chờ tối đa 30 giây. Lưu bài trước rồi xử lý việc nặng (tải ảnh, build lại trang) sau, hoặc làm bất đồng bộ.
Ảnh không hiển thị trong bài
Nếu website dùng bộ tối ưu ảnh (ví dụ next/image), cho phép tên miền ảnh (images.pexels.com) hoặc tải ảnh trong mảng images về và lưu trên hệ thống của bạn.
Nền tảng có sẵn (không cần code)
Nếu website dùng một trong các nền tảng sau, chỉ cần kết nối trong Cài đặt website › Kết nối:
- WordPressTên đăng nhập + Application Password.
- ShopifyTên miền .myshopify.com + Admin API access token (quyền write_content).
- HaravanTên miền cửa hàng + access token của ứng dụng riêng.
- SapoTên miền cửa hàng + API key/secret của ứng dụng riêng.
- GhostAPI URL + Admin API key từ một custom integration.
- WebflowAPI token (CMS read & write) + Collection ID của blog.
- HashnodePersonal Access Token + Publication ID.