Store Builder

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 token wba_, 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.

  1. 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 ở /orgs nếu chưa có.
  2. Một cửa hàng bạn là thành viên, để cài lên trong lúc dựng.
  3. 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à https cô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ào localhost.

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_id thự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_uri khô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ờ:

  1. các scope chủ cửa hàng đã cấp, và
  2. 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ặc localStorage. 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: hello nêu phiên bản màn hình không nói được nhận lại unsupported cù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:

  1. URL được kiểm lại, ngoại lệ localhost bị 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ả hai 400 invalid_url.
  2. Trang nhúng của bạn được tải về, và header về nhúng khung được đọc. Trả X-Frame-Options: DENY hoặc SAMEORIGIN, hoặc Content-Security-Policy: frame-ancestors không cho phép nền tảng, thì bị từ chối 422 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ó localhost sau 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à clientId vẫ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à free và 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_scope là 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 verbs trong welcome mớ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