App đọc/ghi dữ liệu shop qua Open API hiện có (tài liệu Open API): cùng endpoint, format response, phân trang và quy tắc cập nhật. Code đang chạy với API key chỉ cần đổi cách lấy credential.
// Trước: key cố định, toàn quyền, dễ lộ khi nằm trong code frontend
fetch(`https://pos.pages.fm/api/v1/shops/${SHOP_ID}/orders?api_key=${STATIC_KEY}`);
// Trong app: SDK tự gắn app token
await pos.api.get(`/shops/${pos.shop.id}/orders`);App token
POS tự cấp app token khi extension chạy; app không cần và không nên có API key cố định.
| Shop API Key | User API Key | App token | |
|---|---|---|---|
| Cấp bởi | Admin tạo, dùng lâu dài | User tạo, dùng lâu dài | POS cấp tự động, hết hạn sau 15 phút |
| Danh tính | Tài khoản kết nối của shop | User thật | User thật + app |
| Quyền | Toàn quyền (có thể giới hạn) | Quyền của user | Quyền của user ∩ access của app |
| Phạm vi shop | Một shop | Mọi shop user tham gia | Chỉ shop đang mở |
| Nơi dùng | Server | Server, script | Bên trong app trên POS |
- Token gắn với một shop, một user, một app và version đang chạy.
- SDK gửi token qua header
Authorization: Bearer <token>. Tham số?api_key=<token>vẫn được chấp nhận để tương thích code Open API cũ, nhưng không khuyến nghị vì URL dễ lọt vào log. - Token bị thu hồi ngay khi: app bị tắt hoặc gỡ, admin đổi
access, user đăng xuất hoặc bị đổi phiên đăng nhập. User bị xóa khỏi shop hoặc đổi role thì có hiệu lực từ request kế tiếp.
Quyền hiệu lực
Quyền của app token = quyền của user đang dùng ∩ access của app (bản admin đã duyệt)Quyền của user áp dụng giống hệt khi user thao tác trên POS: role, quyền theo kho, quyền theo bảng/trường CRM, bộ phận, giờ làm việc, IP được phép, module đang bật và gói dịch vụ.
access của app thu hẹp thêm: method được dùng (mặc định chỉ GET), kho, nguồn đơn, trạng thái đơn, thời điểm tạo đơn, danh mục/thương hiệu sản phẩm, trường đơn bị ẩn. Xem Manifest › access.
Hệ quả:
- Cùng một app, người có quyền rộng thấy nhiều dữ liệu hơn người có quyền hẹp.
- Request chạy được bằng Shop API Key có thể bị từ chối với app token. Đây là hành vi đúng.
- App nên xử lý lỗi
403thay vì giả định mọi người dùng đều có quyền.
Nhóm luôn bị chặn
Các nhóm sau chỉ dùng được từ phiên đăng nhập trên giao diện POS, mọi loại API key và app token đều bị từ chối:
- Quản lý nhân sự: thêm/sửa nhân viên, phân quyền.
- Liên kết tài khoản Google.
- Quản lý API key.
- Quản lý app và cấp app token.
Endpoint được gọi
- Chỉ dùng các endpoint có trong tài liệu Open API. Endpoint nội bộ của giao diện POS có thể đổi bất kỳ lúc nào và sẽ bị chặn với app token.
- Mọi đường dẫn phải thuộc shop hiện tại:
/shops/{shop_id}/.... - Thiếu dữ liệu hoặc thao tác cần thiết: gửi yêu cầu bổ sung vào Open API thay vì dùng endpoint nội bộ.
Endpoint chính của các resource có extension point:
| Resource | Open API | Mã bản ghi trong đường dẫn |
|---|---|---|
order |
GET/POST /shops/{shop_id}/orders, GET/PUT /shops/{shop_id}/orders/{id} |
record.system_id |
product |
GET /shops/{shop_id}/products/variations, POST /shops/{shop_id}/products, GET/PUT /shops/{shop_id}/products/{id} |
record.id |
purchase |
GET/POST /shops/{shop_id}/purchases, GET/PUT /shops/{shop_id}/purchases/{id} |
record.id |
export |
GET/POST /shops/{shop_id}/export, GET/PUT /shops/{shop_id}/export/{id} |
record.id |
warehouse_transfer |
GET /shops/{shop_id}/transfers, POST /shops/{shop_id}/transfers/multi, GET/PUT /shops/{shop_id}/transfers/{id} |
record.id |
Dữ liệu ngữ cảnh
pos.data chứa dữ liệu tối thiểu POS đang hiển thị (ví dụ mã và trạng thái đơn hay phiếu nhập đang mở) để extension không phải gọi API ngay khi khởi động. Dữ liệu này đã được lọc theo access của app. Cần thêm dữ liệu thì gọi Open API.
Giới hạn tần suất
- Tính theo app + user + shop + endpoint; nhiều extension của cùng app dùng chung hạn mức: dưới 30 lượt đọc (
GET) và dưới 100 lượt ghi mỗi giây. - Vượt giới hạn: HTTP
429. Thử lại sau, tăng dần thời gian chờ (ví dụ 1 s, 2 s, 4 s), tối đa 3 lần. - Không gọi API trong vòng lặp render. Gom request, dùng phân trang và
AbortSignalđể huỷ request cũ khi người dùng đổi bộ lọc.
Audit
Mọi thao tác ghi bằng app token được lưu với user thật, app và version. Các trường tự động như người tạo, người sửa ghi theo user đang dùng.
Mã lỗi
Cột code là trường error_code trong body, SDK đưa vào PosApiError.code.
| HTTP | code |
Nghĩa | SDK xử lý |
|---|---|---|---|
| 401 | 111 |
Token hết hạn hoặc bị thu hồi: app bị tắt, kích hoạt version khác, đổi bộ phận được thấy app, user đổi phiên đăng nhập (xem App token) | Lấy token mới, thử lại 1 lần. Vẫn lỗi thì ném PosApiError (status: 401, code: 111); POS từ chối cấp token mới (app bị tắt hoặc user không còn được thấy app) thì ném PosRuntimeError với code: 'failed' |
| 403 | Không đủ quyền: role, kho, giờ làm, IP, nhóm luôn bị chặn | Ném PosApiError |
|
| 403 | 110 |
Endpoint không có trong Open API | Ném PosApiError |
| 404 | Không tìm thấy, hoặc dữ liệu nằm ngoài access của app |
Ném PosApiError |
|
| 404 | Method không có trong access.methods (nếu gửi trực tiếp không qua SDK) |
SDK chặn trước khi gửi | |
| 429 | 106 |
Vượt giới hạn tần suất | Ném PosApiError; app tự thử lại |
| 200 | tuỳ endpoint | Body có success: false: lỗi nghiệp vụ, xem message |
Ném PosApiError với status: 200 |
Gọi hệ thống bên ngoài
- Khai origin trong
network.allowed_origins; request tới origin khác bị chặn. - Server bên ngoài phải bật CORS cho origin sandbox của app, là header
Origintrong request app gửi tới khi chạy bản đã kích hoạt. Ở chế độ phát triển, origin là địa chỉ bạn nhập khi bật (ví dụhttp://localhost:5174); muốn thử thì bật CORS cho cả origin này. - Không gửi app token ra ngoài, trừ backend của trang tự host. Hệ thống bên ngoài cần dữ liệu POS thì app đọc qua Open API rồi gửi phần cần thiết.
- Không đặt secret trong code app. Mọi người dùng mở được app đều tải được bundle. Cần gọi hệ thống có secret thì đặt một backend trung gian của shop, xác thực người gọi bằng cách riêng.
Tác vụ chạy nền
App chỉ chạy khi có người đang mở POS. Tác vụ định kỳ, đồng bộ hoặc xử lý sự kiện khi không ai mở POS cần backend riêng của shop:
- Gọi Open API bằng User API Key (khuyến nghị, quyền theo user) hoặc Shop API Key như hiện nay.
- Nhận sự kiện thay đổi qua webhook của shop (Cấu hình › Webhook - API).
Dự kiến giai đoạn 2: service key theo app (chạy dưới danh nghĩa một user được chỉ định, áp access của app) và webhook đăng ký theo app có ký HMAC.
Dữ liệu riêng của app
Phiên bản này chưa có kho lưu trữ riêng cho app. Lựa chọn:
- Bảng CRM tuỳ biến của shop, đọc/ghi qua Open API: có sẵn phân quyền theo bảng và trường.
- Backend riêng của shop.
localStoragetrong trang iframe: chỉ lưu trên trình duyệt hiện tại, phù hợp cho tuỳ chọn giao diện.
