StoreFleet
Blog › Webhook Shopify cơ bản: topic, setup và cách tích hợp thật

Webhook Shopify cơ bản: topic, setup và cách tích hợp thật

Webhook Shopify cơ bản kể từ phía endpoint — danh sách topic thật sự dùng, setup shopify.app.toml, block compliance, payload, HMAC và cơ chế retry.

Linh Nguyen · Cập nhật

Điểm chính — AI tóm tắt
  • Compliance topic (customers/data_request, customers/redact, shop/redact) phải để trong một block [[webhooks.subscriptions]] riêng dùng key compliance_topics, không phải field topics thường — bắt buộc với mọi app trên App Store
  • Subscription khai trong shopify.app.toml áp dụng cho toàn app, trên mọi shop đã cài, và cần Shopify CLI 3.63 trở lên mới đăng ký được
  • Payload của products/delete chỉ có ID — không handle, không title — nên nếu không giữ local cache sản phẩm keyed theo ID, delete event trở nên vô dụng
  • Shopify cho endpoint 5 giây để trả 2xx, retry tối đa 8 lần trong khoảng 4 giờ, và tự xóa một subscription cứ fail liên tục trong khoảng 24 giờ
  • Xác thực webhook bằng HMAC-SHA256 trên raw request body trước khi parse JSON, dedupe theo X-Shopify-Webhook-Id, rồi chạy job đối soát với Admin API vì Shopify không đảm bảo mọi event đều đáp xuống

AI tổng hợp từ chính nội dung bài viết; tác giả đã soát lại.

Trong bài này
  1. Webhook Shopify là gì?
  2. Webhook topic Shopify: những cái bạn thật sự sẽ dùng
  3. Webhook compliance: compliance_topics là một block riêng
  4. Cách setup một webhook Shopify trong shopify.app.toml
  5. Payload thực chất trông như thế nào
  6. Webhook checkout và order payment: cái gì đã đổi trong 2026
  7. Cơ chế delivery webhook hoạt động ra sao
  8. Xác thực webhook thật đến từ Shopify
  9. Best practice để webhook đáng tin cậy
  10. Vì sao webhook quan trọng với vận hành đa store

Mọi thứ trong stack của chúng tôi — lớp xử lý order, vòng đời tracking vận đơn, con AI agent trên Discord tự trả lời "đơn của tôi đang ở đâu" — đều đứng trên một pipeline ingest webhook mà tôi tự build cho năm store Shopify chúng tôi vận hành. Nó là một backend Node.js, bắt event từ mọi store, dedupe, rồi ghi hết vào một nguồn dữ liệu duy nhất. Khi chạy trơn, cả hệ thống có cảm giác real-time. Còn khi hỏng, tôi học được điều này, nó hỏng một cách rất im lặng.

Nên bài này là webhook Shopify cơ bản viết từ phía endpoint, không phải bản tóm tắt docs. Webhook là gì, những topic bạn thật sự sẽ subscribe, cách đăng ký chúng trong shopify.app.toml, payload thực chất chứa gì, và cái cơ chế delivery đã từng cắn tôi đau. Ở chỗ nào chi tiết quan trọng, tôi để link về nguồn gốc trên shopify.dev để bạn tự đối chiếu với API version hiện tại, thay vì tin vào trí nhớ của tôi.

Webhook Shopify là gì?

Webhook là việc Shopify đẩy một thông báo tới app của bạn ngay khoảnh khắc có gì đó xảy ra trong store. Bạn subscribe một topic — ví dụ orders/create — và mỗi lần có event khớp, Shopify gửi một HTTP POST tới endpoint của bạn kèm payload JSON. App của bạn phản ứng: sync đơn, kích hoạt workflow, ping một con người.

Phương án còn lại là polling — cứ theo nhịp đồng hồ mà hỏi Admin API "có gì mới không?". Polling ngốn API quota, thêm độ trễ, và mở rộng rất tệ khi bạn có nhiều store. Webhook thì event-driven, đó là lý do mọi tích hợp nghiêm túc tôi từng build đều dựa vào nó cho luồng nhanh. Nhưng có một caveat mà tôi sẽ quay lại: luồng nhanh không phải là nguồn sự thật.

Webhook topic Shopify: những cái bạn thật sự sẽ dùng

Shopify phơi ra topic trải khắp vòng đời của store. Danh sách chuẩn — kèm payload mẫu theo từng topic và từng API version — nằm ở tài liệu webhooks reference trên shopify.dev; bookmark lại đi, vì topic được thêm vào và field bị bỏ đi giữa các version (tôi sẽ cho bạn xem hai ví dụ năm 2026 ở dưới). Đây là những cái tôi thật sự subscribe:

Order, thanh toán & dispute

Khách hàng

Sản phẩm, variant & tồn kho

Fulfillment

Compliance — customers/data_request, customers/redact, shop/redact. Những cái này bắt buộc với app trên App Store, và như tôi giải thích ngay sau đây, chúng sống trong một block cấu hình riêng.

Webhook compliance: compliance_topics là một block riêng

Chỗ này hay làm người ta vấp, nên tôi tách hẳn thành một mục. Compliance topic không để trong field topics của một subscription bình thường. Chúng để trong một block [[webhooks.subscriptions]] riêng, dùng key compliance_topics:

[[webhooks.subscriptions]]
compliance_topics = ["customers/data_request", "customers/redact", "shop/redact"]
uri = "https://your-app.example.com/webhooks/compliance"

Có nhiều block [[webhooks.subscriptions]] trong cùng một file là chuyện bình thường và được mong đợi — một block cho topic thường của bạn, một block riêng cho compliance. Ba webhook quyền riêng tư này bắt buộc với mọi app phân phối trên Shopify App Store; yêu cầu được ghi rõ trong tài liệu privacy law compliance trên shopify.dev.

Đây là lý do tôi không xem nó như boilerplate. Chúng tôi chạy một app duy nhất, cài trên toàn bộ store, nghĩa là mỗi lần install đều mang theo đúng cái bề mặt compliance đó. Một handler thiếu hoặc hỏng không phải là chuyện của một store — nó là chuyện app-review cho cả fleet. Khi audit cấu hình, block compliance là thứ tôi kiểm tra đầu tiên.

Cách setup một webhook Shopify trong shopify.app.toml

Với đa số app, cách đăng ký webhook sạch nhất là khai báo trực tiếp (declarative) trong shopify.app.toml. Bạn set API version một lần, rồi thêm mỗi nhóm topic một block subscription:

[webhooks]
api_version = "2026-07"

[[webhooks.subscriptions]]
topics = ["products/create", "products/update", "products/delete"]
uri = "https://your-app.example.com/webhooks/products"

[[webhooks.subscriptions]]
topics = ["orders/create"]
uri = "pubsub://your-project:your-topic"

uri có thể là một endpoint HTTPS, một Google Pub/Sub URI (pubsub://project:topic), hoặc một Amazon EventBridge ARN. Vài option đáng biết:

Hai điều tôi ước mình thấm sớm hơn. Thứ nhất, subscription khai trong config file áp cho mọi shop mà app được cài — chúng là app-level, không phải per-shop. Đó đúng là thứ bạn muốn cho một fleet, nhưng nó cũng nghĩa là một topic bạn thêm vào là một topic mà mọi install giờ sẽ gửi cho bạn. Thứ hai, luồng declarative này cần Shopify CLI 3.63 trở lên; CLI cũ hơn sẽ lặng lẽ không nhận các subscription này.

Còn khi bạn cần subscription động theo từng shop — topic hoặc endpoint khác nhau cho mỗi store, tạo lúc runtime — thì dùng mutation webhookSubscriptionCreate của GraphQL Admin API thay vì config file. Chúng tôi dùng config file cho phần baseline chung, còn mutation cho bất cứ thứ gì riêng của từng store.

Payload thực chất trông như thế nào

Payload khác nhau cực nhiều theo topic, và giả định rằng chúng giàu dữ liệu hơn thực tế là một sai lầm kinh điển. Tôi đã dính.

Payload của products/delete chính là toàn bộ payload:

{ "id": 788032119674292922 }

Không handle. Không title. Không variant. Chỉ mỗi ID. Pipeline ingest của tôi từng giả định rằng một delete event sẽ nói cho tôi biết cái gì vừa bị xóa — nó không nói. Nếu bạn không giữ một local cache sản phẩm keyed theo ID đó, delete event trở nên vô dụng; bạn biết có thứ gì đó biến mất nhưng không biết là thứ gì. Bài học đó tốn của tôi cả một buổi chiều debug trước khi tôi thêm cache. Hãy tự đối chiếu với payload mẫu trong webhooks reference cho API version của bạn.

Ngược lại hoàn toàn là orders/create — vấn đề ngược lại — nó khổng lồ. Payload gồm id, admin_graphql_api_id, checkout_token, cart_token, contact_email, currency, các field subtotal hiện tại (current_subtotal_price và biến thể money-bag _set của nó), các tổng tài chính, và cả mảng line_items đầy đủ, cùng nhiều thứ nữa. Bạn hiếm khi cần hết đống đó; đây đúng là chỗ include_fields phát huy giá trị.

Webhook checkout và order payment: cái gì đã đổi trong 2026

Nếu bạn khớp checkout với order — recover checkout bỏ dở, attribution — đọc phần này trước khi nó vỡ trên production. Theo changelog của Shopify, từ 2026-01-30 trên API version 2026-04 trở đi, field id đã bị gỡ khỏi payload của checkouts/create và checkouts/update, và checkout_id bị gỡ khỏi bảy order webhook topic.

Việc migrate đơn giản một khi bạn đã biết: dùng token trên webhook checkout và checkout_token trên webhook order để khớp, thay cho các ID số cũ. Nếu logic join của bạn vẫn key theo checkout_id, nó sẽ âm thầm null ngay khoảnh khắc bạn bump API version.

Với payment và refund, order_transactions/create vẫn là webhook cần theo dõi. Và đừng nhầm bất cứ thứ nào ở trên với checkout_and_accounts_configurations/update, một topic khác đã bị gỡ hoàn toàn từ 2026-01-01 — nó biến mất, không phải đổi tên.

Cơ chế delivery webhook hoạt động ra sao

Đăng ký một subscription là bạn đang chỉ định một topic, một đích đến (HTTPS, Google Pub/Sub, hoặc EventBridge), một format (JSON, mặc định), và filter tùy chọn. Rồi delivery diễn ra thế này: một event kích hoạt, Shopify POST payload JSON tới endpoint của bạn, và endpoint có 5 giây để trả về một mã 2xx. Trượt cái đó — timeout, 5xx, lỗi mạng — thì Shopify thử lại tối đa 8 lần trong khoảng 4 giờ theo exponential backoff.

Hai hành vi ở đây quan trọng hơn cả cái happy path.

Thứ nhất, một subscription cứ fail liên tục sẽ bị auto-delete. Theo tài liệu troubleshoot webhook, nếu endpoint của bạn fail delivery lặp đi lặp lại trong khoảng 24 giờ, Shopify gỡ luôn subscription. Đây là failure mode làm tôi sợ nhất trên stack của mình: một endpoint 500 cả đêm không chỉ làm rơi event, nó âm thầm unsubscribe bạn, và sáng hôm sau mọi thứ trông yên bình vì chẳng còn gì cố gửi cho bạn nữa. Alert theo tỷ lệ delivery-failure không phải chuyện tùy chọn.

Thứ hai, một delivery được retry mang theo payload gốc từ lúc event xảy ra, không phải snapshot làm mới. Nên tới lúc một retry đáp xuống, dữ liệu có thể đã cũ so với store. Dùng header X-Shopify-Triggered-At để biết event thật sự xảy ra khi nào, và để nhận ra rằng bạn đang xử lý quá khứ, không phải hiện tại.

Xác thực webhook thật đến từ Shopify

Mỗi delivery hợp lệ tới một endpoint HTTPS đều mang các header chứng minh nó đến từ Shopify (còn delivery qua Pub/Sub và EventBridge thì xác thực qua chính nền tảng cloud đó):

Để xác thực, tính HMAC-SHA256 của request body bằng app secret của bạn rồi so với header. Cái bẫy đã tốn của tôi thời gian thật: HMAC được tính trên raw request body. Nếu framework của bạn parse JSON trước khi bạn xác thực — ví dụ express.json() chạy như global middleware — thì đống byte bạn đem hash không còn khớp với đống byte Shopify đã ký, và mọi lần xác thực đều fail dù thực chất chẳng có gì sai. Hãy capture raw body trước, xác thực, rồi mới parse:

const digest = crypto
  .createHmac('sha256', SHOPIFY_API_SECRET)
  .update(rawBody) // raw bytes, before JSON parsing
  .digest('base64');

Các thư viện chính thức của Shopify bọc sẵn phần này cho bạn qua shopify.webhooks.validate(), và bám theo hướng dẫn verify-deliveries là con đường an toàn nhất. Tách riêng ra, hãy dùng X-Shopify-Webhook-Id để dedupe — nếu ID đó bạn đã xử lý rồi, bỏ bản trùng đi. Trên pipeline của chúng tôi, cái ID đó chính là idempotency key khiến hành vi "retry tối đa 8 lần" trở nên vô hại.

Best practice để webhook đáng tin cậy

Năm thói quen trụ được trên production:

  1. Đối soát — đừng tin webhook như nguồn sự thật. Shopify không đảm bảo mọi event đều đáp xuống. Job đối soát của chúng tôi query Admin API theo updated_at định kỳ và backfill bất cứ thứ gì luồng webhook bỏ sót — và nó thật sự bắt được các lỗ hổng, nhất là sau một sự cố hoặc một lần auto-unsubscribe. Webhook là luồng nhanh; job đối soát mới là thứ làm dữ liệu đúng.
  2. Giả định event đến không đúng thứ tự. Các topic khác nhau không đồng bộ theo thời gian. Hãy dựng lại trình tự từ updated_at hoặc X-Shopify-Triggered-At, không bao giờ từ thứ tự đến.
  3. Trả 2xx nhanh, xử lý async. Với timeout 5 giây, làm tối thiểu inline — xác thực, enqueue, trả về — rồi để một background worker (Redis, một job queue) gánh phần nặng.
  4. Lọc ngay từ nguồn. filter và include_fields cắt kích thước payload và tải cho handler trước khi dữ liệu kịp tới tay bạn.
  5. Log mọi delivery. Payload, header, kết quả xử lý. Khi một tích hợp giở chứng, những log này là thứ duy nhất cho bạn biết event chưa bao giờ tới, hay đã tới nhưng bị xử lý sai.

Vì sao webhook quan trọng với vận hành đa store

Một store thì webhook là tiện nghi. Năm store thì nó là kiến trúc tỉnh táo duy nhất. Polling năm store là nhân đôi tải API lẫn độ trễ; một pipeline webhook cho phép mỗi store push tới cùng một lớp ingest ngay khi có gì đó đổi.

Đó là pattern đáng chạy theo. Webhook từ mọi store đáp xuống một backend, được dedupe và đối soát, rồi lấp đầy một dashboard hợp nhất — để bạn quản lý nhiều store Shopify từ một màn hình duy nhất thay vì năm tab admin. Trên lớp dữ liệu đó là những mảnh mà pipeline này mở ra: sync đơn hàng sang Google Sheets, theo dõi vận đơn hàng loạt qua 17TRACK, cảnh báo shipment kẹt, tài chính hợp nhất trên nhiều store, và giám sát cùng cảnh báo sức khỏe store. Nếu bạn muốn điều khiển việc thực thi chứ không chỉ quan sát, lớp tự động hóa qua Shopify Bot API hành động trên chính các event này, và có một bài mổ xẻ đầy đủ Shopify Flow vs Zapier vs bot tự viết nếu bạn đang cân nhắc nên đặt logic ở đâu.

Dù bạn chạy 5 hay 50 store, một tích hợp duy nhất dựa trên webhook thay thế cho hàng chục workflow rời rạc.