Xây một app (/oauth + /api/v1)
Dựng thứ mà các chủ cửa hàng khác cài được: đăng ký app, đưa một cửa hàng đi qua OAuth, gọi API bằng token nhận được, rồi gửi một phiên bản đi duyệt.
App là một chương trình mà chủ cửa hàng cài lên cửa hàng của họ. Nó có bộ credential OAuth riêng, quyền truy cập theo scope vào dữ liệu của cửa hàng đó, và một trang của riêng nó được nhúng khung bên trong các màn hình Quản lý của chủ cửa hàng.
Trang này dành cho lập trình viên đang dựng app. API công khai là nửa còn lại — nó mô tả khoá API
wbk_mà một chủ cửa hàng tự tạo cho công cụ của họ. Khi app của bạn đã cầm tokenwba_, mọi điều trang đó nói về/api/v1áp dụng nguyên vẹn cho bạn.
Chỗ nào nêu tên một nút bạn phải bấm, nhãn tiếng Anh đi kèm trong (en: …).
Toàn bộ luồng, theo thứ tự:
tạo một app → client id wbc_ + client secret wbs_
tạo một phiên bản → các scope, embed URL, redirect URI mà người duyệt xem xét
tự cài nó lên → mã uỷ quyền wbo_ (sandbox: chạy được khi còn là bản nháp)
đổi mã lấy token → access token wba_ + refresh token wbr_
gọi /api/v1 → dữ liệu của chủ cửa hàng, trong scope họ đã cấp
mở trang nhúng → frame token wbf_, phân giải ngược lại với chúng tôi
gửi đi duyệt → được duyệt, và mọi chủ cửa hàng đều cài được
Có một CLI, và nó rút ngắn hai đầu của danh sách trên. Mọi bước là một
request HTTP và trang này mô tả tất cả — đó là hợp đồng, và là thứ bạn đọc khi
một lời gọi trả 401. Nhưng sb lo giúp phần cơ học:
| Bước | Làm tay | Với sb |
|---|---|---|
| viết một app nhận callback và render trang nhúng | §5 và §7, bạn tự viết | sb init my-app — một app chạy được có cả hai |
| tạo app, tạo phiên bản, tự cài | §2, §3, §4 | vẫn làm tay — sb không gọi mạng cho tới khi có những id đó |
| đẩy scope, URL, block và mã island của một phiên bản | §3 và Xây một block, mỗi cửa một lệnh curl |
sb deploy |
| tải lại island mỗi lần sửa | mỗi lần lưu một lệnh curl |
sb dev |
| gửi đi duyệt | §8 | sb deploy --submit |
Không việc gì sb làm mà curl không làm được. Không muốn cài thêm công cụ
thì cứ đọc tiếp — không gì bên dưới giả định bạn có nó.
1. Trước khi bắt đầu
Bạn cần ba thứ, và thứ thứ ba là chỗ phần lớn các lần thử đầu tiên dừng lại.
- Một tài khoản và một tổ chức. Một app được phát hành bởi một tổ chức,
không phải cá nhân — tạo tổ chức ở
/orgsnếu chưa có. - Một cửa hàng bạn là thành viên, để cài lên trong lúc dựng.
- Hai URL: một redirect URI (nơi mã uỷ quyền được trả về) và một embed
URL (trang được nhúng khung trong Quản lý). Cả hai phải là
httpscông khai vào lúc gửi duyệt — nhưng khi phiên bản còn là bản nháp, cả hai được phép trỏ vàolocalhost.
Phát triển với localhost — chạy trên bản nháp, dừng ở lúc gửi duyệt
Mọi URL bạn đăng ký đi qua đúng chốt chặn SSRF mà webhook dùng: nó phân giải tên miền và từ chối địa chỉ loopback, riêng tư, link-local, multicast và unspecified. Phép kiểm tra chạy khi tạo bản nháp, khi sửa nó, và lần nữa lúc gửi duyệt.
Có đúng một ngoại lệ: khi phiên bản còn là bản nháp, một địa chỉ loopback
không mã hoá được chấp nhận — cho cả redirect URI lẫn embed URL. Nên đây là
một 201:
curl -X POST https://api.sbuilder.io.vn/api/orgs/org_yourorg/apps/app_1a2b3c4d/versions \
-H "Authorization: Bearer <user access token của bạn>" \
-H "Content-Type: application/json" \
-d '{"version":"1.0.0","scopes":["products.read"],"embedUrl":"http://localhost:5173/embed","redirectUri":"http://localhost:3000/callback"}'
Bạn có trọn vòng lặp phát triển trên máy mình: tự cài bản nháp (§4), mã uỷ quyền đáp xuống server local, bạn đổi nó lấy token (§5), và server dev local là thứ trang nhúng tải lên (§7, với một bản cài sandbox).
Hình dạng chính xác được phép, vì nó hẹp hơn "giờ localhost chạy rồi":
| Đăng ký trên một bản nháp | Kết quả |
|---|---|
http://localhost:3000/callback |
chấp nhận |
http://127.0.0.1:3000/callback (mọi địa chỉ trong 127.0.0.0/8) |
chấp nhận |
http://[::1]:3000/callback |
chấp nhận |
https://localhost:3000/callback |
từ chối — ngoại lệ chỉ dành cho kết nối không mã hoá |
http://apps.example.com/callback |
từ chối — không mã hoá tới host công khai vẫn là không mã hoá |
http://169.254.169.254/…, http://10.0.0.5/…, http://192.168.1.10/… |
từ chối — metadata và dải riêng tư không phải loopback |
Nó dừng ở lúc gửi duyệt. Gửi duyệt một bản nháp còn trỏ vào localhost bị
từ chối 400:
{ "error": "the embed URL or redirect URI is not usable — it must be a public https address", "code": "invalid_url" }
Một phiên bản trong hàng chờ duyệt là phiên bản một người duyệt sẽ mở và một
chủ cửa hàng sẽ cài, và cả hai không ngồi ở máy bạn. Vậy lần sửa cuối trước khi
gửi duyệt là đổi cả hai URL sang https công khai — bản nháp vẫn sửa được, nên
đây là một lệnh PUT, không phải phiên bản mới.
Một tunnel (ngrok, Cloudflare Tunnel) vẫn đáng có, cho đúng thứ loopback
không cho được: thử đúng những URL bạn sẽ gửi duyệt, qua https thật.
2. Tạo app — wbc_ và wbs_
Trong app: Nhà phát triển (en: Developer) ở thanh bên trái, tại
/developer → Tạo ứng dụng (en: Create an app). Đặt tên; không hỏi gì
thêm.
Lời gọi tương đương, nếu bạn muốn viết script — các route dành cho nhà phát
triển nằm trên API riêng tư và xác thực bằng user access token của chính bạn
(cái mà POST /api/auth/login trả về), không phải credential của app:
curl -X POST https://api.sbuilder.io.vn/api/orgs/org_yourorg/apps \
-H "Authorization: Bearer <user access token của bạn>" \
-H "Content-Type: application/json" \
-d '{"name":"Shipping Helper"}'
{
"app": {
"id": "app_1a2b3c4d",
"developerOrgId": "org_yourorg",
"name": "Shipping Helper",
"clientId": "wbc_9f8e7d6c",
"status": "active"
},
"clientSecret": "wbs_Zm9vYmFyYmF6cXV4..."
}
201. Đây là phản hồi duy nhất từng mang clientSecret. Nó được lưu băm
SHA-256, nên không ai — kể cả người vận hành nền tảng — đọc lại được. Đưa
wbs_… thẳng vào kho bí mật của bạn ngay khi ra khỏi phản hồi này.
Nếu nó lộ: xoay khoá
Trong app: mở app → Đổi mã bí mật (en: Rotate secret).
curl -X POST https://api.sbuilder.io.vn/api/orgs/org_yourorg/apps/app_1a2b3c4d/secret \
-H "Authorization: Bearer <user access token của bạn>"
200, cùng quy tắc như lúc tạo: phản hồi là nơi duy nhất khoá mới tồn tại.
clientId không đổi — bản cài của mọi chủ cửa hàng gọi tên nó.
Xoay khoá không vô hiệu hoá thứ đã cấp. Không chủ cửa hàng nào bị đăng xuất, không bản cài nào phải uỷ quyền lại, mọi token
wba_bạn đang giữ vẫn chạy: client secret xác thực CHÍNH BẠN ở bước đổi token, còn access token được đối chiếu với bản cài. Thứ đổi là lần đổi token kế tiếp — khoá cũ hết tác dụng ở đó, nên cập nhật cấu hình trước khi access token hiện tại hết hạn.
Mọi route trong mục này từ chối người gọi không phải thành viên của {orgId}
bằng 403 not_a_member, trước khi tra cứu gì — nên người lạ cầm id tổ chức
không từ mã trạng thái mà biết một app có tồn tại.
3. Tạo một phiên bản — thứ mà việc duyệt xem xét
Việc duyệt gắn vào phiên bản, không bao giờ gắn vào app. Mọi thứ người duyệt và chủ cửa hàng đọc nằm trên đó: scope nó xin, trang nó nhúng, địa chỉ nó được nhận mã, và phần giới thiệu.
Trong app: mở app → Phiên bản mới (en: New version).
curl -X POST https://api.sbuilder.io.vn/api/orgs/org_yourorg/apps/app_1a2b3c4d/versions \
-H "Authorization: Bearer <user access token của bạn>" \
-H "Content-Type: application/json" \
-d '{"version":"1.0.0","scopes":["products.read","orders.read"],"embedUrl":"https://apps.example.com/embed","redirectUri":"https://apps.example.com/oauth/callback","summary":"Rate-shops carriers at checkout.","description":"Longer copy the merchant reads before consenting."}'
{ "version": { "id": "apv_5e6f7a8b", "appId": "app_1a2b3c4d", "status": "draft", "...": "..." } }
201, và trạng thái luôn là draft — server tự đặt, vì một client gửi
lên được phiên bản mang nhãn đã-duyệt là đã bỏ qua khâu duyệt.
| Trường | Luật sẽ cắn bạn |
|---|---|
scopes |
Ít nhất một, từ bộ từ vựng <miền>.read / <miền>.write mà §4 của API công khai liệt kê. Xin ít nhất có thể. |
embedUrl |
https công khai — hoặc localhost không mã hoá khi còn là bản nháp (§1). Trang phải cho phép nền tảng nhúng khung — kiểm lúc gửi duyệt, §8. |
redirectUri |
https công khai — hoặc localhost không mã hoá khi còn là bản nháp. Đúng một cái, khớp chính xác — không tiền tố, không ký tự đại diện. |
summary, description |
Thứ chủ cửa hàng đọc trên màn hình đồng ý. Tuỳ chọn; màn hình đồng ý trống vẫn là màn hình đồng ý, chỉ tệ hơn. |
Sửa: PUT …/versions/{versionId} chỉ viết được bản nháp. Phiên bản đã gửi
hoặc đã xét duyệt trả 409 not_draft — duyệt ghim chặt payload. Tạo phiên bản
mới.
4. Tự cài bản nháp — vòng lặp phát triển
Bạn không phải chờ duyệt mới chạy được luồng này. Lập trình viên tự cài app chưa duyệt là ngoại lệ được cho phép (bản cài sandbox), thu hẹp hai lần: bạn phải vốn với tới được cửa hàng đó, và thuộc tổ chức phát hành app.
Trong cổng nhà phát triển: mở app, tìm phiên bản, bấm Cài lên cửa hàng
của tôi (en: Install on my store). Nó chọn một cửa hàng bạn thuộc về, ghép
URL bên dưới từ client_id, version_id và redirect_uri đã đăng ký, thêm
state, rồi cho bạn xem trước khi mở. Phần còn lại của mục này là thứ cái nút
đó dựng nên — thứ bạn cần khi tự viết script hoặc đọc một lời từ chối.
Đưa trình duyệt của chủ cửa hàng — lúc này là của bạn — tới /oauth/authorize:
https://api.sbuilder.io.vn/oauth/authorize
?client_id=wbc_9f8e7d6c
&version_id=apv_5e6f7a8b
&redirect_uri=https://apps.example.com/oauth/callback
&site_id=<id của cửa hàng>
&state=<giá trị chống giả mạo của riêng bạn>
site_idthực tế không tuỳ chọn. Màn hình đồng ý không phê được nếu thiếu nó. Khi chủ cửa hàng bắt đầu cài từ màn hình Ứng dụng (en: Apps) của cửa hàng họ, danh sách app điền sẵn; khi app của bạn khởi động luồng, bạn tự cung cấp.stateđược trả nguyên vẹn về redirect URI. Dùng nó.- Server kiểm tra trước khi chuyển hướng — client lạ, phiên bản thuộc app
khác, hay
redirect_urikhông phải cái đã đăng ký bị từ chối ngay tại đây dưới dạng JSON, không bao giờ bị đẩy sang địa chỉ bạn yêu cầu.
Trình duyệt đáp xuống màn hình đồng ý: tên app, tổ chức của bạn, và đúng những scope phiên bản xin. Phiên bản chưa duyệt được ghi nhãn chưa qua xét duyệt.
Khi họ chấp thuận, họ được đưa tới:
https://apps.example.com/oauth/callback?code=wbo_...&state=<của bạn>
Chủ cửa hàng có thể chỉ cấp một tập con những gì bạn xin — và không gì
báo cho bạn biết là tập con nào. Phản hồi token không mang trường scope, và
không có endpoint tra cứu, nên tín hiệu duy nhất là 403 insufficient_scope
trên đúng lời gọi cần quyền bạn không được cấp. Thiết kế cho điều đó: xin ít
nhất có thể, và hạ cấp một tính năng thay vì cho rằng scope bạn xin là scope
bạn có.
5. Đổi mã lấy token — wba_ và wbr_
Từ server tới server, bằng credential của bạn. Không trình duyệt, không phiên người dùng.
curl -X POST https://api.sbuilder.io.vn/oauth/token \
-H "Content-Type: application/json" \
-d '{"grantType":"authorization_code","clientId":"wbc_9f8e7d6c","clientSecret":"wbs_...","code":"wbo_...","redirectUri":"https://apps.example.com/oauth/callback"}'
{
"accessToken": "wba_ins_1a2b3c4d.site_9f8e.1771234567.AbCdEf...",
"refreshToken": "wbr_...",
"tokenType": "Bearer",
"expiresAt": "2026-08-17T10:00:00Z"
}
Mã uỷ quyền sống 60 giây và dùng một lần. redirectUri được đối chiếu như
một phần của việc đổi mã — và một lần không khớp không tiêu mất mã, nên gõ
nhầm không huỷ một mã mà lần thử lại dùng được.
Gia hạn, trước hoặc sau expiresAt:
curl -X POST https://api.sbuilder.io.vn/oauth/token \
-H "Content-Type: application/json" \
-d '{"grantType":"refresh_token","clientId":"wbc_9f8e7d6c","clientSecret":"wbs_...","refreshToken":"wbr_..."}'
Refresh token có xoay vòng. Cái bạn gửi bị đốt và cái mới quay về trong
cùng phản hồi; lưu cái mới, nếu không lần gia hạn kế tiếp nhận
400 invalid_refresh_token.
Tự gỡ mình khỏi một cửa hàng — phía app của một lần gỡ cài đặt:
curl -X POST https://api.sbuilder.io.vn/oauth/revoke \
-H "Content-Type: application/json" \
-d '{"clientId":"wbc_9f8e7d6c","clientSecret":"wbs_...","accessToken":"wba_..."}'
204. Nhận access token thay vì id bản cài, cố ý: token gọi tên bản cài và
credential của bạn chứng minh nó là của bạn, nên không app nào gỡ được app
khác.
6. Gọi thử một endpoint
curl https://api.sbuilder.io.vn/api/v1/products \
-H "Authorization: Bearer wba_..."
{ "products": [ { "id": "prod_...", "name": "..." } ], "total": 42 }
Đó là toàn bộ khác biệt giữa một app và khoá của chủ cửa hàng: cái credential.
Không có {siteId} trong đường dẫn /api/v1 — cửa hàng được ngầm định
bởi token. Mọi tài nguyên, phong bì, truy vấn và lỗi được mô tả một lần tại
API công khai.
Mỗi lời gọi bị chặn bởi hai giới hạn, và cái thứ hai hay làm người ta bất ngờ:
- các scope chủ cửa hàng đã cấp, và
- vai trò hiện tại của thành viên đã cài app.
App không bao giờ làm được nhiều hơn người đã cài nó, phân giải theo từng
request — một bản cài do quản trị viên thực hiện, người này về sau bị hạ xuống
chỉ xem, mất quyền ghi ở lời gọi kế tiếp. Lời từ chối là 403 insufficient_scope: chủ cửa hàng phải cài lại và chấp thuận quyền ấy.
# Gửi nhầm nửa kia của cặp token là lỗi đầu tiên rất hay gặp, và nó được gọi tên:
curl https://api.sbuilder.io.vn/api/v1/products -H "Authorization: Bearer wbr_..."
# → 401 {"error":"that is a refresh token; exchange it at /oauth/token for an wba_ access token first",
# "code":"refresh_token_not_accepted"}
7. Render trang nhúng — wbf_
Khi chủ cửa hàng mở app của bạn trong Quản lý, nền tảng cấp một frame token sống ngắn và tải embed URL trong một iframe, kèm hai tham số:
https://apps.example.com/embed?wb_frame_token=wbf_...&wb_site_id=site_9f8e
Token đó không phải uỷ quyền. Nó trả lời "ai đang xem" — một bản cài, một
cửa hàng, thành viên đang nhìn màn hình — và không gì khác. App được làm gì
vẫn bị giới hạn bởi token wba_.
Đừng tin nó ở dạng vừa nhận. Phân giải từ server của bạn, bằng credential của bạn:
curl -X POST https://api.sbuilder.io.vn/oauth/frame \
-H "Content-Type: application/json" \
-d '{"clientId":"wbc_9f8e7d6c","clientSecret":"wbs_...","token":"wbf_..."}'
{ "installId": "ins_1a2b3c4d", "siteId": "site_9f8e", "memberId": "8c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f" }
401 invalid_frame_token nếu token giả mạo hoặc hết hạn, 401 unknown_client
nếu nó được cấp cho app khác — không app nào phân giải frame token của app
khác. Bản cài được đọc lại khi trả lời, nên chủ cửa hàng vừa tắt app mười giây
trước nhận lời từ chối ở đây, không phải câu đồng ý đã cũ.
Ba ràng buộc trên trang được nhúng
- Iframe bị sandbox mà không có
allow-same-origin. Trang render được, chạy script, gửi form, mở link — nhưng nằm trong một origin mờ đục, nên không đọc hay ghi được cookie hoặclocalStorage. Mang phiên làm việc trong token bạn được trao, đừng mang trong cookie. - Token sống ngắn (10 phút) và cấp lại mỗi lần mở. Đổi nó lấy phiên của riêng bạn ngay; đừng giữ nó như một phiên.
- Trang phải cho phép được nhúng khung. §8 — đây là thứ khiến các lần gửi duyệt bị từ chối.
Cầu nối app — nói chuyện với màn hình Quản lý bao quanh bạn
Trang của bạn có thể hỏi màn hình bao quanh vài việc qua postMessage. Kênh
này có đánh phiên bản, có xác thực, và cố ý rất nhỏ. Không động từ
nào với tới server của nền tảng: mỗi động từ chỉ đổi thứ gì đó trên màn hình
của chủ cửa hàng. Cần dữ liệu cửa hàng thì câu trả lời là một lời gọi API.
Mở đầu bằng một cái bắt tay. Màn hình bỏ qua mọi động từ cho tới khi bạn đã chào và được đón:
const params = new URLSearchParams(location.search);
const nonce = params.get('wb_bridge_nonce'); // xem bên dưới — luôn gửi lại
const CHANNEL = 'wb.app.bridge';
const ADMIN_ORIGIN = 'https://<origin quản trị bạn được cho lúc đăng ký>';
let ready = false;
window.addEventListener('message', (e) => {
// Origin quản trị là hằng số bạn được cho biết một lần — đừng đọc nó từ một
// tham số URL: người nhúng khung bạn mới là người cung cấp tham số đó.
if (e.origin !== ADMIN_ORIGIN) return;
const m = e.data;
if (!m || m.channel !== CHANNEL) return;
if (m.type === 'welcome') { ready = true; resize(); }
if (m.type === 'unsupported') {
console.warn('app bridge versions supported here:', m.supported);
}
});
parent.postMessage({ channel: CHANNEL, v: 1, nonce, type: 'hello' }, ADMIN_ORIGIN);
function resize() {
if (!ready) return;
parent.postMessage(
{ channel: CHANNEL, v: 1, nonce, type: 'resize', height: document.body.scrollHeight },
ADMIN_ORIGIN,
);
}
new ResizeObserver(resize).observe(document.body);
Gửi lại wb_bridge_nonce trong mọi thông điệp. Nó là tham số thứ ba bên
cạnh wb_frame_token và wb_site_id, và là một credential khác với frame
token: không bao giờ rời trình duyệt, cấp mới mỗi lần mở, không uỷ quyền gì.
Nó tồn tại vì iframe của bạn bị sandbox không có allow-same-origin, nên mọi
thông điệp bạn gửi tới màn hình với event.origin === "null" — chuỗi mà mọi
iframe sandbox trên internet đều gửi. Thứ màn hình kiểm là thông điệp có đến
từ đúng cửa sổ nó đã nhúng và mang nonce chỉ tài liệu của bạn từng thấy.
Mất nonce là cầu nối im lặng. Phía bạn thì kiểm origin đúng cách: màn hình
không bị sandbox, nên câu trả lời của nó tới kèm origin thật.
| Động từ | Payload | Nó làm gì |
|---|---|---|
resize |
height — số, pixel CSS |
Đặt chiều cao iframe. Kẹp trong 200–4000; giá trị ngoài khoảng bị kéo vào, không bị từ chối. Không phải số hữu hạn thì bỏ qua. |
toast |
message — một dòng, tối đa 200 ký tự. tone — error, hoặc bất cứ gì khác cho sắc thái trung tính |
Hiện một dòng trong khung của chủ cửa hàng, tiêu đề là tên app của bạn. Thông điệp rỗng hoặc quá dài bị từ chối, không cắt. Tối đa một lần mỗi giây — phần dư bị bỏ. |
confirm |
id — khoá đối chiếu của bạn, tối đa 64 ký tự. message — tối đa 300 ký tự |
Hỏi chủ cửa hàng một câu có/không trong hộp thoại tiêu đề là tên app, rồi trả { type: 'confirm-result', id, confirmed }. |
navigate |
to — một đường dẫn |
Đưa trang quản trị tới to, bên trong khu quản lý của chính cửa hàng này. |
confirm: mỗi lần một câu. Gửi câu thứ hai khi câu đầu còn mở thì quay về
ngay với { confirmed: false, refused: 'busy' } — một lời từ chối, không phải
chủ cửa hàng nói không. Huỷ, Escape, bấm ra ngoài đều trả confirmed: false.
Nếu họ đóng màn hình khi câu hỏi còn mở, không có câu trả lời nào: gắn
trạng thái vào id và để nó hết hạn.
navigate: giới hạn là /manage/<wb_site_id>/… — đúng cửa hàng đã mở
bạn. Bị từ chối, im lặng phía bạn và có tiếng trong console của chủ cửa hàng:
URL tuyệt đối, //host, màn hình của cửa hàng khác, trình sửa trang, đoạn
.., và bất cứ gì dài quá 512 ký tự. Không gì vượt qua được kiểm tra quyền —
route họ đáp xuống kiểm lại vai trò y như khi họ tự bấm.
Hạ cấp nhẹ nhàng. Không có welcome nghĩa là bạn đang bị nhúng bởi thứ
không nói giao thức này. Cứ chạy: render ở chiều cao cố định hợp lý. Đừng để
trang treo chờ bắt tay.
Phiên bản giao thức là
1, được thương lượng:hellonêu phiên bản màn hình không nói được nhận lạiunsupportedcùng danh sách nó nói được. Động từ mới tới không cần tăng số phiên bản; thay đổi ý nghĩa động từ đã có thì không.
8. Gửi đi duyệt
Trong app: mở phiên bản nháp → Gửi duyệt (en: Submit for review). Gửi duyệt khoá phiên bản; đổi gì sau đó thì tạo phiên bản mới.
curl -X POST https://api.sbuilder.io.vn/api/orgs/org_yourorg/apps/app_1a2b3c4d/versions/apv_5e6f7a8b/submit \
-H "Authorization: Bearer <user access token của bạn>"
200, phiên bản chuyển sang pending. Hai phép kiểm tra chạy ở đây, và cái
thứ hai không chạy ở đâu khác:
- URL được kiểm lại, ngoại lệ
localhostbị rút — phép kiểm tra §1, chạy lần thứ ba và chặt hơn. Hai thứ hỏng ở đây: bản nháp còn trỏlocalhost, và một tên miền công khai lúc bạn lưu nhưng phân giải ra địa chỉ riêng tư lúc gửi. Cả hai400 invalid_url. - Trang nhúng của bạn được tải về, và header về nhúng khung được đọc. Trả
X-Frame-Options: DENYhoặcSAMEORIGIN, hoặcContent-Security-Policy: frame-ancestorskhông cho phép nền tảng, thì bị từ chối422 embed_refuses_framing.
Phép thứ hai đáng chuẩn bị trước, vì phương án còn lại tệ hơn: chủ cửa hàng
phát hiện dưới dạng khung trắng vĩnh viễn, vài ngày sau, không có gì trong log.
Phục vụ trang nhúng với frame-ancestors nêu tên origin Quản lý của nền tảng
(* cũng qua). Lỗi mạng hay 4xx/5xx từ server của bạn không bị coi là
từ chối, nên server sập tạm thời không làm phiên bản vĩnh viễn không gửi được.
Sau đó một người duyệt chấp thuận, hoặc từ chối kèm lý do — lý do đi kèm trong lần đọc app mà cổng nhà phát triển vốn gọi, nên bạn thấy vì sao.
Khi đã duyệt, phiên bản cài được bởi bất kỳ chủ cửa hàng nào, và quy tắc URL
chỉ-dành-cho-bản-nháp chấm dứt: redirect_uri của app đã duyệt phải là
https, không ngoại lệ.
9. Gỡ rối: nhật ký
Mọi lời gọi /api/v1 mà các bản cài của bạn thực hiện được ghi lại, theo
route — không theo URL, nên không gì trong đó mang định danh của một chủ
cửa hàng khác.
curl "https://api.sbuilder.io.vn/api/orgs/org_yourorg/apps/app_1a2b3c4d/requests?limit=50" \
-H "Authorization: Bearer <user access token của bạn>"
{
"requests": [
{ "id": "…", "method": "GET", "path": "/api/v1/products/{id}", "status": 403, "code": "insufficient_scope", "at": "2026-08-17T09:41:00Z" }
],
"total": 1
}
limit mặc định 50, tối đa 200. Nhật ký giữ 7 ngày theo mặc định.
Bản ghi được đẩy vào kênh có bộ đệm sau khi phản hồi đã viết ra, và bị bỏ
chứ không xếp hàng khi kênh đầy — ghi log không bao giờ làm chậm hay hỏng lời
gọi. Vì thế một server chưa nối nhật ký trả 503 request_log_unavailable thay
vì danh sách rỗng, vì danh sách rỗng đọc thành "app của bạn chưa gọi lần nào".
Lịch sử cài / gỡ của app nằm ở GET …/apps/{appId}/events, cùng hình dạng
phân trang, giữ 90 ngày.
10. Những lỗi đáng rẽ nhánh
Bề mặt OAuth tách các lời từ chối thay vì gộp vào một invalid_request. Mỗi
dòng có cách sửa khác nhau.
| Status | code |
Nghĩa là |
|---|---|---|
| 401 | unknown_client |
client_id không rõ, client_secret sai, hoặc credential không sở hữu thứ nó hỏi tới |
| 400 | redirect_uri_mismatch |
Không đúng URI đã đăng ký trên phiên bản này, hoặc không đúng cái mã đã được cấp cho |
| 400 | insecure_redirect_uri |
http thuần trên phiên bản đã rời trạng thái nháp |
| 400 | code_already_used |
Mã uỷ quyền đã dùng — một sự kiện an ninh, không phải chuyện thời điểm |
| 400 | code_expired |
Cũ hơn 60 giây. Bắt đầu lại luồng |
| 400 | invalid_refresh_token |
Không rõ, đã xoay vòng, hoặc đã thu hồi |
| 400 | unsupported_grant_type |
grantType phải là authorization_code hoặc refresh_token |
| 400 | scope_not_requested |
Đồng ý cấp thứ phiên bản chưa từng xin |
| 400 | no_scopes |
Đồng ý không cấp gì |
| 403 | forbidden |
Người đang đồng ý không cài được lên cửa hàng đó |
| 409 | not_installable |
Phiên bản chưa duyệt (không có ngoại lệ sandbox), hoặc app bị đình chỉ |
| 401 | install_unavailable |
Bản cài đã biến mất hoặc đã tắt |
| 401 | invalid_frame_token |
Token wbf_ sai định dạng, giả mạo hoặc hết hạn. Token cấp cho app khác là unknown_client |
| 404 | unknown_endpoint |
Không có route /oauth nào như vậy |
Trên các route dành cho nhà phát triển:
| Status | code |
Nghĩa là |
|---|---|---|
| 403 | not_a_member |
Bạn không phải thành viên của tổ chức đó |
| 404 | (không có code) | Không có app như vậy trong tổ chức này — cố ý trả lời y hệt id không tồn tại |
| 400 | invalid_url |
Embed URL hoặc redirect URI không phải địa chỉ công khai dùng được — hoặc localhost đang được gửi duyệt (§1) |
| 400 | no_scopes / invalid_name / no_developer |
Phiên bản không xin gì / app không có tên / không có tổ chức sở hữu |
| 409 | not_draft |
Phiên bản đã gửi hoặc đã xét duyệt. Tạo cái mới |
| 422 | embed_refuses_framing |
Trang nhúng không cho nền tảng nhúng khung (§8) |
| 503 | request_log_unavailable |
Nhật ký request chưa cấu hình trên server này |
Mọi thứ token wba_ chạm tới trên /api/v1 dùng bảng của bề mặt đó —
§5 của API công khai.
11. Thời hạn
| Thứ | Sống | Vì sao |
|---|---|---|
Mã uỷ quyền wbo_ |
60 giây, dùng một lần | Người giữ hợp pháp đổi nó ngay, từ server tới server |
Access token wba_ |
1 giờ | Không thu hồi riêng lẻ được, nên ngắn; bản cài được đọc lại mỗi lời gọi, nên gỡ cài đặt có hiệu lực ngay |
Refresh token wbr_ |
60 ngày, có xoay vòng | Quá hạn thì chủ cửa hàng đồng ý lại |
Frame token wbf_ |
10 phút | Cấp lại mỗi lần mở |
| Nhật ký request | mặc định 7 ngày | Đủ để gỡ rối, không thành cơ sở dữ liệu thứ hai |
12. Những gì chưa có
- Không có
localhostsau giai đoạn bản nháp. Tunnel vẫn là cách duy nhất để thử đúng URL bạn sẽ gửi duyệt. - Không xoá được app. App bạn không muốn nữa bị quản trị viên đình chỉ, và
clientIdvẫn giữ chỗ. Xoay client secret thì có — §2. - Chỉ có app miễn phí. Không có đường thanh toán; giá của phiên bản là
freevà server tự đặt. - Một redirect URI mỗi phiên bản, khớp chính xác. Môi trường thứ hai (staging) cần một phiên bản thứ hai — giữ làm bản nháp vĩnh viễn và cài qua sandbox — hoặc một app thứ hai.
- Không tra cứu được scope.
403 insufficient_scopelà tín hiệu duy nhất (§4). - Không có danh bạ nhà phát triển công khai, và không chuyển được app giữa các tổ chức.
- Cầu nối app có bốn động từ, và chỉ bốn (§7). Nó không phải SDK: không gì
trên đó với tới server của nền tảng. Danh sách
verbstrongwelcomemới là sự thật về những gì bản triển khai đang nhúng bạn chấp nhận.
Mọi thứ một token với tới được: API công khai để đọc, bảng điều khiển API để bắn một request vào server thật.
Cập nhật 26/9/2026