Mục lục thư viện
Thư viện tài liệuBán hàng
Bán hàng qua Seller API
Seller API cho phép cửa hàng, bot Discord hay hệ thống nội bộ của bạn tự tạo và quản lý license qua HTTP, không cần mở dashboard. Hướng dẫn này đi từ cách tạo một khoá API an toàn, xác thực, ghi idempotent và rate limit, tới một ví dụ hoàn chỉnh bán license từ backend cửa hàng.
12 phút đọc
Trước khi bắt đầu
- Khoá API nằm ở Kinh doanh › Khoá Seller API. Chỉ Chủ sở hữu và Quản trị của workspace được tạo, xoay và thu hồi khoá.
- Mọi endpoint nằm dưới
/seller/v1trên địa chỉ API của bạn. Ví dụ trong bài dùnghttps://api.example.com; hộp thoại hiện khoá mới có sẵn một lệnhcurlmẫu với địa chỉ thật. - Mỗi khoá thuộc một workspace và chỉ chạm được tới ứng dụng của workspace đó.
- Số khoá theo gói dịch vụ, mặc định 2 trên Basic và 25 trên Premium. Con số áp cho workspace của bạn hiện ở chú thích cuối trang khoá và ở Tài khoản & tổ chức › Gói & sử dụng. Hạn mức tính mọi khoá chưa bị thu hồi.
Tạo khoá API
- Mở Kinh doanh › Khoá Seller API và bấm Tạo khoá.
- Trong khung Khoá API mới, điền các mục ở bảng dưới.
- Bấm Tạo khoá.
- Hộp thoại Khoá mới của bạn hiện khoá đầy đủ. Sao chép vào trình quản lý bí mật rồi bấm Đã cất.
| Mục | Ý nghĩa |
|---|---|
| Tên khoá | Tối đa 64 ký tự, nên đặt theo hệ thống dùng khoá, ví dụ "Cửa hàng Shopify". Tên được ghi kèm mỗi thao tác ghi mà khoá thực hiện. |
| Quyền | Bốn nút mẫu và lưới ô đọc / ghi cho từng nhóm tài nguyên. Khung mở sẵn mẫu Cửa hàng. Phải chọn ít nhất một quyền. |
| Giới hạn app | Tích các ứng dụng khoá được dùng, tối đa 50. Không chọn là mọi app của workspace, kể cả app tạo sau này. |
| IP được phép | IPv4, IPv6 hoặc dải CIDR, cách nhau bằng dấu phẩy hoặc khoảng trắng, tối đa 50 mục. Để trống là gọi được từ mọi nơi. |
| Hết hạn vào | Không bắt buộc. Khoá ngừng hoạt động lúc 23:59:59 UTC của ngày đã chọn. |
Quyền (scope)
Khoá chỉ làm được những gì scope cho phép, và scope :read không bao giờ thay đổi dữ liệu. Riêng GET /seller/v1/me và GET /seller/v1/apps không cần scope nào.
| Nhóm | đọc | ghi |
|---|---|---|
licenses | Danh sách, chi tiết, tra cứu theo key | Tạo key, thao tác, xoá |
users | Người dùng cuối | Tạo, cấm, bỏ cấm, đặt lại mật khẩu, đăng xuất mọi phiên, xoá |
devices | Thiết bị, lịch sử chuyển máy | Gỡ, chặn, bỏ chặn |
sessions | Phiên đang chạy | Kết thúc một phiên hoặc mọi phiên của app |
variables | Biến (giá trị bí mật trả về null) | Đặt và xoá biến |
releases | Bản phát hành | Phát hành bản nháp, chặn, bỏ chặn |
rules | Luật truy cập | Thêm, xoá luật |
webhooks | Webhook | Tạo, xoá, gửi thử |
stats | Số license theo trạng thái, người dùng, phiên, lượt xác thực 24 giờ qua | |
protect | Tóm tắt và sự kiện Protect 24/7 | |
resellers | Đại lý, số dư, sổ cái | Nạp hoặc trừ tín dụng |
Các mẫu có sẵn: Chỉ đọc (mọi scope :read), Bot Discord (licenses, users, devices, cả đọc lẫn ghi), Cửa hàng (licenses:read và licenses:write, đủ để bán và tra cứu key) và Toàn quyền (mọi scope).
Khoá chỉ hiện một lần
Sau đó, mỗi dòng trong danh sách chỉ hiện tên, 13 ký tự đầu của khoá, các scope, số IP được phép, số app bị giới hạn và Dùng lần cuối kèm IP gọi (cập nhật tối đa mỗi phút một lần). Tiền tố clv_live_ cũng giúp công cụ quét bí mật dễ nhận ra khoá nếu nó lỡ bị đẩy lên kho mã.
Xoay và thu hồi
- Xoay khoá (biểu tượng mũi tên vòng trên dòng khoá) tạo ngay một khoá mới cùng tên, scope, app, IP và ngày hết hạn, hiện một lần trong hộp thoại Khoá mới của bạn. Khoá cũ vẫn chạy thêm 7 ngày để bạn kịp cập nhật máy chủ; dòng của nó ghi Hết hạn ân hạn kèm thời điểm. Qua 7 ngày, khoá cũ bị từ chối.
- Thu hồi hỏi xác nhận, rồi khoá bị từ chối ngay từ lời gọi kế tiếp. Không hoàn tác được.
- Hai nút này chỉ có trên khoá Đang hoạt động. Scope, app, IP và ngày hết hạn không sửa được sau khi tạo; muốn đổi, tạo khoá mới rồi thu hồi khoá cũ.
Xác thực
Gửi khoá trong header Authorization ở mọi lời gọi:
curl https://api.example.com/seller/v1/me \
-H "Authorization: Bearer $CLAVIS_API_KEY"
/me trả về ID, tên, tiền tố, scope, các app được phép, loại khoá (seller) và workspaceId. Đây là cách nhanh nhất để kiểm tra khoá đã cấu hình đúng. Khi một lời gọi bị từ chối:
| Tình huống | HTTP | Mã lỗi |
|---|---|---|
Khoá nằm trên URL (api_key, apikey, key, token, access_token, secret trong query string) | 400 | key_in_url |
| Thiếu khoá, sai định dạng, không tồn tại, đã thu hồi, đã hết hạn hoặc đã qua 7 ngày sau khi xoay | 401 | unauthorized |
| IP gọi không nằm trong IP được phép | 403 | ip_not_allowed |
| Vượt rate limit | 429 | rate_limited |
| Khoá thiếu scope của endpoint | 403 | forbidden_scope |
| App hay đối tượng không tồn tại, hoặc nằm ngoài Giới hạn app | 404 | not_found |
Mọi lỗi có cùng một dạng. Hãy ghi requestId vào log của bạn để đối chiếu khi cần:
{ "error": { "code": "forbidden_scope", "message": "This key needs the \"licenses:write\" scope", "requestId": "…" } }
Ghi idempotent
Mạng có thể rớt đúng lúc máy chủ đã tạo key xong; gọi lại mà không có gì bảo vệ sẽ sinh key thứ hai. Header Idempotency-Key giải quyết việc này cho mọi lời gọi ghi (POST, PUT, DELETE):
- Dùng một giá trị gắn với nghiệp vụ, ví dụ mã đơn hàng
order-1042, tối đa 255 ký tự. - Trong 24 giờ, gửi lại cùng khoá với cùng phương thức, đường dẫn và body sẽ nhận lại đúng câu trả lời đầu tiên, kèm header
Idempotent-Replayed: true, và không có gì được làm lần hai. - Cùng khoá nhưng body khác:
422 idempotency_mismatch. Lời gọi đầu còn đang chạy:409 idempotency_in_progress, chờ một chút rồi gửi lại. - Câu trả lời 5xx không được lưu, nên gọi lại với cùng khoá sẽ chạy lại thao tác. Lỗi do chính thao tác trả về, như
plan_limithayunknown_policy, thì được lưu và phát lại; sửa nguyên nhân xong hãy dùng khoá mới. - Bản ghi idempotency thuộc về từng khoá API. Khoá mới sau khi xoay không thấy bản ghi của khoá cũ.
Rate limit và phân trang
- Mỗi khoá được 120 lời gọi mỗi phút, 600 nếu workspace dùng Premium. Bộ đếm làm mới vào đầu mỗi phút.
- Câu trả lời cho lời gọi đã qua bước kiểm tra khoá và IP có
RateLimit-Limit,RateLimit-RemainingvàRateLimit-Reset(số giây tới lúc làm mới). Vượt mức nhận429 rate_limited; chờ theoRateLimit-Resetrồi gọi lại. - Các danh sách lớn (license, người dùng, thiết bị, phiên) nhận
limittừ 1 tới 100, mặc định 50, cùngcursor, và trả{ "data": [...], "nextCursor": "…" }. TruyềnnextCursorvàocursorcho tới khi nó lànull.
Ví dụ: bán license từ backend cửa hàng
Kịch bản: khách thanh toán xong, cổng thanh toán báo cho backend của bạn, backend tạo một key, gửi cho khách và lưu ID license vào đơn hàng.
Chuẩn bị
- Tạo khoá với mẫu Cửa hàng, tích đúng ứng dụng ở Giới hạn app và điền IP máy chủ cửa hàng.
- Lưu khoá vào biến môi trường
CLAVIS_API_KEYtrên máy chủ. Không bao giờ đặt khoá trong mã chạy trên trình duyệt hay trong phần mềm phát cho khách. - Lấy App ID
app_…ở trang Tổng quan của ứng dụng, hoặc gọiGET /seller/v1/apps. - Chọn gói license. Bỏ
policyIdthì dùng gói mặc định của ứng dụng. Dashboard chưa hiện ID của gói; để lấypol_…của một gói khác, tạo một key bằng gói đó ở Phát hành & bản quyền › License, rồi tra key ấy bằngPOST /seller/v1/licenses/lookup: câu trả lời cópolicyId.
Tạo key bằng curl
curl -X POST https://api.example.com/seller/v1/apps/app_…/licenses \
-H "Authorization: Bearer $CLAVIS_API_KEY" \
-H "Idempotency-Key: order-1042" \
-H "Content-Type: application/json" \
-d '{"policyId": "pol_…", "count": 1, "email": "buyer@example.com", "note": "Order 1042", "metadata": {"orderId": "1042"}}'
Thành công trả 201, keys và ids cùng thứ tự:
{
"data": {
"keys": ["PRO-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX"],
"ids": ["lic_…"],
"warnings": []
}
}
| Trường | Ý nghĩa |
|---|---|
count | Số key, từ 1 tới hạn mức "Key mỗi lô" của gói dịch vụ. |
policyId | Gói license; phải thuộc ứng dụng trên đường dẫn, nếu không trả unknown_policy. |
durationSeconds | Ghi đè thời hạn của gói, từ 60 giây tới 10 năm, null là vĩnh viễn. Tính từ lần kích hoạt đầu tiên. |
maxDevices | Ghi đè số máy, không vượt "Thiết bị mỗi key". |
prefix | 1 tới 12 chữ in hoa hoặc chữ số, ví dụ PRO. |
note | Ghi chú, tối đa 500 ký tự. Nên ghi mã đơn để tìm lại. |
email | Email người mua, dùng cho cổng khách hàng và thư nhắc hết hạn. |
metadata | Object phẳng, giá trị là chuỗi, số hoặc boolean; trả lại ở GET /seller/v1/licenses/{id}. |
expiresAt | Ngày ISO 8601, chỉ có ở Seller API. Key chưa kích hoạt bị từ chối sau ngày này (cộng ân hạn của gói). Khi kích hoạt lần đầu, ngày hết hạn được tính lại từ thời hạn của key, hoặc thành vĩnh viễn nếu key không có thời hạn. Muốn bán theo kỳ, dùng durationSeconds. |
test | true để tạo key TEST: không tính hạn mức ngày, tối đa 10 key một lần, hết hạn 24 giờ sau khi tạo. Bị từ chối với app_released khi app đã phát hành công khai. |
Cùng việc đó bằng JavaScript
Đoạn mã dưới chạy trên Node.js 18 trở lên. Hàm clavis gửi khoá, gắn Idempotency-Key và tự gọi lại khi gặp 429, lỗi 5xx hoặc idempotency_in_progress:
const API = "https://api.example.com/seller/v1";
const APP_ID = "app_…";
async function clavis(path, { method = "GET", body, idempotencyKey } = {}) {
for (let attempt = 0; ; attempt++) {
const res = await fetch(API + path, {
method,
headers: {
Authorization: `Bearer ${process.env.CLAVIS_API_KEY}`,
...(body ? { "Content-Type": "application/json" } : {}),
...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
if (res.status === 204) return null;
const json = await res.json();
if (res.ok) return json;
const retry = res.status === 429 || res.status >= 500 || json.error?.code === "idempotency_in_progress";
if (!retry || attempt === 3) {
throw new Error(`${json.error?.code}: ${json.error?.message} (requestId ${json.error?.requestId})`);
}
const wait = res.status === 429 ? Number(res.headers.get("RateLimit-Reset") ?? 1) : 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, wait * 1000));
}
}
// Gọi khi cổng thanh toán xác nhận đơn đã trả tiền
export async function fulfilOrder(order) {
const { data } = await clavis(`/apps/${APP_ID}/licenses`, {
method: "POST",
idempotencyKey: `order-${order.id}`,
body: {
policyId: order.policyId,
count: 1,
email: order.email,
note: `Order ${order.id}`,
metadata: { orderId: String(order.id) },
},
});
if (data.warnings.length) console.warn("Clavis:", data.warnings);
return { licenseId: data.ids[0], key: data.keys[0] };
}
// Hoàn tiền: thu hồi key
export async function refundOrder(order) {
await clavis(`/licenses/${order.licenseId}/actions`, {
method: "POST",
idempotencyKey: `refund-${order.id}`,
body: { action: "revoke" },
});
}
// Gia hạn 30 ngày, mỗi lần gia hạn một Idempotency-Key riêng
export async function renewLicense(licenseId, renewalId) {
await clavis(`/licenses/${licenseId}/actions`, {
method: "POST",
idempotencyKey: `renew-${renewalId}`,
body: { action: "extend", seconds: 30 * 86400 },
});
}
Lỗi thường gặp khi bán
unknown_policy(400):policyIdkhông thuộc ứng dụng này.plan_limit: lô vượt "Key mỗi lô" hoặc số máy vượt "Thiết bị mỗi key" (400), hoặc đã hết hạn mức key trong ngày trên Basic (402). Trên Premium, key vẫn được tạo vàwarningscódaily_key_limit_exceeded. Xem Gói Basic, Premium và hạn mức.validation_failed(400): body sai schema,detailschỉ ra trường lỗi.
Tra cứu và thao tác trên license
Bot hỗ trợ hay trang quản trị của bạn thường bắt đầu từ chiếc key khách gửi tới:
curl -X POST https://api.example.com/seller/v1/licenses/lookup \
-H "Authorization: Bearer $CLAVIS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"key": "PRO-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX"}'
- Key đi trong body, không bao giờ trên URL. Máy chủ so bằng mã băm, bỏ qua chữ hoa thường và khoảng trắng thừa; không thấy thì trả
404 not_found. - Gửi
{"mark": "0123456789.abcdef"}thay cho key để tìm license đứng sau một dấu người mua (Forensic Watermarking). Câu trả lời có thêmmarkDevice(id,name,platform,active) khi nửa sau của dấu khớp khoá thiết bị hiện tại của một máy, nếu không thìnull. - License trả về có
id,preview,status,maxDevices,devices(số máy đang giữ suất),activatedAt,expiresAt,note,email,policyIdvàappId.statuslà trạng thái hiệu lực: key đang dùng đã quá hạn được trả về làexpired. GET /seller/v1/licenses/{id}trả thêmpolicy(id,name,entitlements) vàmetadata.GET /seller/v1/apps/{appId}/licenseslọc theostatus(unused,active,expired,suspended,revoked),policyIdvàq(khớp một phần ghi chú hoặc email, không phân biệt hoa thường).
Thao tác dùng POST /seller/v1/licenses/{id}/actions và trả về license sau khi đổi:
action | Trường thêm | Tác dụng |
|---|---|---|
suspend | Tạm khoá key Chưa dùng hoặc Đang dùng, kết thúc mọi phiên. | |
reinstate | Về Đang dùng nếu từng kích hoạt, ngược lại về Chưa dùng. | |
revoke | Dừng vĩnh viễn, kết thúc mọi phiên. | |
extend | seconds, từ 60 giây tới 10 năm | Gia hạn, cùng quy tắc với nút Gia hạn trong dashboard. |
reset_devices | Gỡ mọi máy và kết thúc mọi phiên. | |
note | note, tối đa 500 ký tự, null để xoá | Đổi ghi chú. |
email | email, null để xoá | Gắn hoặc đổi email người mua. |
DELETE /seller/v1/licenses/{id} xoá key: key bị thu hồi, mọi phiên kết thúc, key biến khỏi danh sách, và API trả 204. Muốn gỡ đúng một máy thay vì cả key, dùng POST /seller/v1/devices/{id}/actions với {"action": "revoke"} (scope devices:write); lần gỡ này ghi là người bán gỡ và không trừ lượt chuyển máy của khách.
Nhật ký request
Trên mỗi dòng khoá, kể cả khoá đã thu hồi, bấm biểu tượng Nhật ký gọi để mở khung Nhật ký request:
- 50 lời gọi gần nhất, mới nhất trước, tự làm mới mỗi 10 giây.
- Mỗi dòng có phương thức, đường dẫn (không kèm query string), mã trạng thái (tô đỏ từ 400 trở lên) và thời gian xử lý tính bằng ms.
- Chỉ lời gọi đã qua bước kiểm tra khoá, IP và rate limit mới được ghi. Lời gọi bị từ chối với
key_in_url,unauthorized,ip_not_allowedhayrate_limitedkhông xuất hiện; nếu tích hợp không chạy mà nhật ký trống, hãy kiểm tra các nguyên nhân này trước.
Danh sách đầy đủ endpoint, mã lỗi và tài liệu OpenAPI (GET /seller/v1/openapi.json) có ở tham chiếu Seller API.