Hướng dẫn tích hợp Shield Cloud
Tài liệu dành cho mọi đội phát triển — dù bạn dùng JavaScript, Python, PHP, Go, Ruby, Java, C# hay chỉ cần cURL. Shield tự nhận domain sau khi bạn thêm website trong portal; không bắt buộc cấu hình biến môi trường site key.
Hướng dẫn khách hàng đầy đủ (PDF-style / Markdown)
Quy trình đăng ký → embed → verify (Node/PHP) → score/risk → Mode C. File: docs/HUONG-DAN-KHACH-HANG.md
- Portal → đăng ký → thêm domain → copy embed (
challenge.js+data-mode=auto) - Backend: Node
@kimidevvn/shield-cloud-nodehoặc PHP cURLPOST /v1/verify+sk_live - Kiểm tra cookie
shield-token; (khuyến nghị) reject nếutoken.scorerisk cao - Gọi API Shield qua
ShieldSDK.fetch(token one-time / rotate) - (Tuỳ chọn) Mode C Worker / custom domain — không bật sẵn trên localhost
Bắt đầu nhanh (3 bước)
Đăng ký tenant
Vào Portal, tạo tài khoản. Bạn nhận trial Pro 14 ngày ngay sau khi đăng ký.
Thêm domain website
Trong dashboard, thêm domain chính của hệ thống. Shield tự động hỗ trợ localhost.
Copy mã nhúng SDK
Chọn site, copy mã embed và dán vào <head> của web. Hoàn tất!
3 cách tích hợp (A / B / C)
Chọn theo độ sâu bảo vệ. MVP cho thuê = A + B; C là full WAF-as-a-service.
A. SDK-only
Embed script auto hoặc gọi ShieldSDK.getToken() trước login/API. Phù hợp SPA, form.
B. Middleware API
Shield gate /v1/login và /v1/encrypt — thiếu shield-token → 403 + x-shield-action: challenge.
Mode C — Cloudflare Worker / Nginx
Chặn cả site ở edge: cookie shield-token hợp lệ thì proxy origin; không có thì hiện interstitial challenge rồi reload. Toàn bộ code mẫu nằm ngay bên dưới — không cần clone repo.
Risk score — bảng thiết lập code
token.score = điểm rủi ro (càng cao càng nghi bot), nhúng lúc PoW. Backend đọc sau /v1/verify: if (score > maxRisk) reject. Chi tiết: docs/HUONG-DAN-KHACH-HANG.md §3.1.
1) Khuyến nghị maxRisk trên app của bạn
| Loại API | maxRisk | Reject khi | Ghi chú |
|---|---|---|---|
| Public / browse | 60 | score > 60 | Lỏng |
| Login / Register | 40 | score > 40 | Mặc định khuyến nghị |
| OTP / reset password | 35 | score > 35 | Hơi chặt |
| Checkout / thanh toán | 25 | score > 25 | Chặt |
| Admin / hủy tiền | 15 | score > 15 | Rất chặt |
2) Điểm cộng (signals lúc mint)
| Tín hiệu | Điểm |
|---|---|
| navigator.webdriver | +80 |
| Headless UA | +50 |
| document webdriver | +40 |
| Automation flags | +20 mỗi (max +60) |
| Giải PoW < 5ms | +25 |
| No plugins (desktop) | +20 |
| No languages | +15 |
| Chrome runtime missing | +15 |
| Canvas fail | +10 |
3) Ngưỡng Shield khi cấp token (theo gói)
| Gói | Captcha từ | Block từ | Captcha fallback |
|---|---|---|---|
| Starter | 50 | 70 | Không (block cứng) |
| Pro | 45 | 100 | Có |
| Business | 40 | 120 | Có |
// Login — khuyến nghị
if (visitor.token.score > 40) {
return res.status(403).json({ error: "high_risk_score", score: visitor.token.score });
}
// PHP
// define('SHIELD_MAX_RISK_AUTH', 40);
// shield_require(SHIELD_MAX_RISK_AUTH);Thêm domain — Shield tự nhận
Đây là cách mặc định và khuyến nghị. Khi bạn thêm website trong portal, Shield sẽ:
- Tự tìm site key theo hostname người đang truy cập
- Khớp cả www và không www
- Kiểm tra allowed_origins đã cấu hình sẵn khi tạo site
Không cần đặt biến môi trường ở Frontend
Chỉ cần thêm đúng domain trong portal. API trả về mode: auto khi tìm thấy site. Chỉ đặt SHIELD_SELF_SITE_KEY nếu bạn muốn ghi đè thủ công.
Nhúng SDK (Trình duyệt)
Trên website/web app, dán một dòng script. Production dùng challenge.js (obfuscated); dev có thể dùng shield-sdk.js.
<!-- Production: challenge.js (obfuscated). Dev: shield-sdk.js -->
<script src="https://shield-cloud.vercel.app/v1/challenge.js"
data-site-key="pk_live_SITE_KEY_CUA_BAN"
data-api="https://shield-cloud.vercel.app"
data-mode="auto"></script>
<!-- data-mode: auto | manual | off
data-difficulty: optional leading-zero bits (min = plan)
Custom domain: data-api="https://token.yourdomain.com"
Flow: PoW interstitial → cookie shield-token → gated APIs -->Sau khi load, window.ShieldSDK sẵn sàng. data-mode=auto sẽ chạy interstitial PoW và set cookie shield-token trước khi site hiển thị.
JS Challenge (PoW)
Interstitial kiểu WAF: thu thập signals, giải SHA-256 hashcash hoặc bandwidth, set cookie shield-token.
data-mode
- • auto — interstitial mỗi page (mặc định cho thuê)
- • manual — gọi getToken() / runChallenge()
- • off — tắt challenge
Loại challenge
- • sha256 — hashcash (mọi plan)
- • bandwidth — encode payload (Pro/Business)
- • captcha — fallback khi signals cao (Pro/Business)
Cấu hình mode, difficulty, types trong Portal → chọn site → JS Challenge.
Events: shield:ready, shield:error. Cookie: shield-token (TTL ~4 ngày).
Plan & difficulty
Difficulty PoW và tính năng challenge theo gói:
- Starter — PoW bits 8, SHA-256 only, block sớm khi signals cao
- Pro — bits 10, bandwidth + captcha fallback
- Business — bits 12, bandwidth + captcha + hạn mức cao
// 1) Issue
const issue = await fetch("https://shield-cloud.vercel.app/v1/pow/issue", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Shield-Site-Key": "pk_live_SITE_KEY_CUA_BAN",
},
credentials: "include",
body: JSON.stringify({ device_id: crypto.randomUUID() }),
});
// 2) Client solves SHA-256 or bandwidth (SDK handles this)
// 3) Verify → shield-token cookie
// Or simply:
const token = await window.ShieldSDK.getToken({ baseUrl: "https://shield-cloud.vercel.app" });
// 4) Call protected API
await window.ShieldSDK.fetch("https://shield-cloud.vercel.app/v1/login", { method: "POST", body: "..." });Bảo vệ Form Login
Luồng chuẩn: có shield-token (auto challenge) rồi mới init session + encrypt form.
- Đảm bảo script data-mode=auto đã pass (event shield:ready) hoặc gọi getToken().
- Gọi trackFormFocus() + initSession(api).
- Nếu needs_challenge (captcha layer-2) — gọi POST /v1/challenge.
- Gắn X-Shield-Token và dùng encryptedFetch gửi login.
// Trình duyệt — dùng Shield SDK (bắt buộc cho web)
// data-mode="auto" tự chạy interstitial PoW.
// Gated APIs: token one-time (jti) — luôn đọc lại getStoredToken() sau init.
async function shieldLogin(email, password) {
const api = "https://shield-cloud.vercel.app";
const siteKey = "pk_live_SITE_KEY_CUA_BAN";
await window.ShieldSDK.getToken({ baseUrl: api });
window.ShieldSDK.trackFormFocus();
// initSession is gated → burns jti and rotates shield-token
const init = await window.ShieldSDK.initSession(api);
if (!init.ok) throw new Error("Shield init thất bại");
// NEVER reuse token string from before init
const shieldToken = window.ShieldSDK.getStoredToken();
const extra = shieldToken ? { "X-Shield-Token": shieldToken } : {};
if (init.needs_challenge) {
const ch = await fetch(api + "/v1/challenge", {
method: "POST",
headers: {
"Content-Type": "application/json; charset=utf-8",
"X-Shield-Site-Key": siteKey,
},
credentials: "include",
body: JSON.stringify({
session_id: init.session_id,
captcha_token: "human-verified",
}),
});
const data = await ch.json();
if (!ch.ok) throw new Error("Challenge thất bại");
extra["X-Behavior-Token"] = data.behavior_token;
}
const res = await window.ShieldSDK.encryptedFetch(
api,
"/login",
{ username: email, password, time: Math.floor(Date.now() / 1000) },
String(init.session_id),
extra
);
return res.json();
}
// Prefer ShieldSDK.fetch for your own APIs (queue + rotate + retry)
// await window.ShieldSDK.fetch("/api/private", { credentials: "include" });
Package npm @kimidevvn/shield-cloud-node
SDK server publish riêng: verify shield-token bằng secret_key qua POST /v1/verify. Hỗ trợ Express, Next.js, Edge, Worker.
Cài đặt
npm install @kimidevvn/shield-cloud-nodeBiến môi trường: SHIELD_SECRET_KEY (sk_live_...) và SHIELD_API_BASE (origin Shield hoặc token.customer.com).
// npm install @kimidevvn/shield-cloud-node
import {
ShieldClient,
shieldMiddleware,
assertShield,
ShieldAuthError,
} from "@kimidevvn/shield-cloud-node";
// Env: SHIELD_SECRET_KEY=sk_live_... SHIELD_API_BASE=https://shield-cloud.vercel.app
const opts = {
secretKey: process.env.SHIELD_SECRET_KEY,
apiBase: process.env.SHIELD_API_BASE || "https://shield-cloud.vercel.app",
};
// --- Express ---
// app.use(shieldMiddleware(opts));
// app.get("/api/me", (req, res) => res.json({ visitor: req.shield }));
// --- Next.js App Router ---
export async function GET(req) {
try {
const visitor = await assertShield(req, opts);
return Response.json({
site: visitor.site,
sid: visitor.token.sid,
score: visitor.token.score,
});
} catch (e) {
if (e instanceof ShieldAuthError) return e.toResponse();
throw e;
}
}
// --- One-shot ---
const shield = new ShieldClient(opts);
const result = await shield.verify({ token: cookieOrHeader });
// result.token.score = risk (higher = more bot-like); optional: reject if > 40
if (!result.ok) throw new Error(result.error);Custom domain (token.customer.com)
Gắn subdomain API riêng cho site: CNAME về host Shield Cloud. Embed dùng data-api=https://token.yourdomain.com.
- Portal → site → JS Challenge → Custom token domain
- DNS: token.yourdomain.com CNAME → host Shield (cname_target trong API)
- API resolve site theo Host — có thể bỏ X-Shield-Site-Key khi gọi qua custom domain
# DNS
token.yourdomain.com. CNAME shield-cloud.vercel.app.
# Portal → site → JS Challenge → Custom token domain = token.yourdomain.com
# Embed (portal auto-generates this)
<script src="https://token.yourdomain.com/v1/challenge.js"
data-site-key="pk_live_SITE_KEY_CUA_BAN"
data-api="https://token.yourdomain.com"
data-mode="auto"></script>
# API resolves site by Host header on token.yourdomain.com
# GET/PATCH /saas/sites/:id → { challenge.tokenDomain, api_base, cname_target }Mode C — Cloudflare Worker / Nginx
Chặn cả site ở edge: cookie shield-token hợp lệ thì proxy origin; không có thì hiện interstitial challenge rồi reload. Toàn bộ code mẫu nằm ngay bên dưới — không cần clone repo.
Bước: (1) Tạo Worker / sidecar với secret sk_live + pk_live + API base + ORIGIN. (2) Gắn route domain. (3) Origin chỉ nhận traffic đã qua gate.
Secrets cần chuẩn bị
SHIELD_SECRET_KEY— sk_live_... (portal, không public)SHIELD_SITE_KEY— pk_live_... (embed interstitial)SHIELD_API_BASE—https://shield-cloud.vercel.appORIGIN— origin thật, vd. https://origin.yourdomain.com
Cloudflare Worker (copy & deploy)
Tạo file shield-gate.js, dán code, set secrets, deploy, gắn route domain.
// shield-gate.js — Cloudflare Worker (Mode C)
// Secrets: wrangler secret put SHIELD_SECRET_KEY | SHIELD_SITE_KEY | SHIELD_API_BASE | ORIGIN
// SHIELD_API_BASE example: https://shield-cloud.vercel.app
// ORIGIN example: https://origin.yourdomain.com
// Deploy: npx wrangler deploy → attach route *yourdomain.com/*
// Flow: valid cookie shield-token → proxy ORIGIN; else interstitial → reload
const COOKIE = "shield-token";
export default {
async fetch(request, env) {
const url = new URL(request.url);
const token = readCookie(request, COOKIE);
if (token && (await verifyToken(env, token))) {
return proxyOrigin(request, env);
}
const accept = request.headers.get("Accept") || "";
if (request.method === "GET" && accept.includes("text/html")) {
return challengePage(env, url);
}
return new Response(
JSON.stringify({ ok: false, code: 1124, error: "shield_token_required" }),
{
status: 403,
headers: { "Content-Type": "application/json", "x-shield-action": "challenge" },
}
);
},
};
function readCookie(request, name) {
const raw = request.headers.get("Cookie") || "";
const parts = ("; " + raw).split("; " + name + "=");
if (parts.length < 2) return null;
return decodeURIComponent(parts.pop().split(";").shift() || "");
}
async function verifyToken(env, token) {
const api = String(env.SHIELD_API_BASE || "").replace(/\/$/, "");
const secret = env.SHIELD_SECRET_KEY || "";
if (!api || !secret) return false;
try {
const res = await fetch(api + "/v1/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ secret_key: secret, token }),
});
if (!res.ok) return false;
const data = await res.json();
return Boolean(data.ok && data.valid);
} catch {
return false;
}
}
async function proxyOrigin(request, env) {
const originBase = String(env.ORIGIN || "").replace(/\/$/, "");
if (!originBase) return new Response('ORIGIN secret not set', { status: 500 });
const url = new URL(request.url);
const target = new URL(url.pathname + url.search, originBase);
const headers = new Headers(request.headers);
headers.set("X-Forwarded-Host", url.host);
headers.set("X-Shield-Gate", "1");
return fetch(new Request(target, {
method: request.method,
headers,
body: request.method === "GET" || request.method === "HEAD" ? null : request.body,
redirect: "manual",
}));
}
function challengePage(env, url) {
const api = String(env.SHIELD_API_BASE || "").replace(/\/$/, "");
const siteKey = env.SHIELD_SITE_KEY || "";
const next = url.pathname + url.search;
const html = `<!DOCTYPE html><html><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Security Check</title>
<style>body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;
background:#060a12;color:#e8eef7;font-family:system-ui,sans-serif}</style></head><body>
<p>Shield Security Check…</p>
<script src="${api}/v1/challenge.js" data-site-key="${siteKey}" data-api="${api}" data-mode="auto"></script>
<script>document.addEventListener("shield:ready",function(){location.replace(${JSON.stringify(next)});});</script>
</body></html>`;
return new Response(html, {
headers: { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" },
});
}# wrangler.toml (tối giản)
name = "shield-gate"
main = "shield-gate.js"
compatibility_date = "2024-11-01"
# Sau đó:
# npx wrangler secret put SHIELD_SECRET_KEY
# npx wrangler secret put SHIELD_SITE_KEY
# npx wrangler secret put SHIELD_API_BASE
# npx wrangler secret put ORIGIN
# npx wrangler deployNginx + Node auth sidecar (copy & chạy)
Chạy sidecar Node cạnh Nginx; dùng auth_request. Code đầy đủ bên dưới.
# === auth-sidecar.mjs (Node 18+) — chạy cạnh Nginx ===
# env:
# SHIELD_SECRET_KEY=sk_live_...
# SHIELD_API_BASE=https://shield-cloud.vercel.app
# PORT=9090
import http from "node:http";
const PORT = Number(process.env.PORT || 9090);
const API = String(process.env.SHIELD_API_BASE || "").replace(/\/$/, "");
const SECRET = process.env.SHIELD_SECRET_KEY || "";
function readCookie(header, name) {
if (!header) return null;
const parts = ("; " + header).split("; " + name + "=");
if (parts.length < 2) return null;
return decodeURIComponent(parts.pop().split(";").shift() || "");
}
async function verify(token) {
if (!token || !API || !SECRET) return false;
const res = await fetch(API + "/v1/verify", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ secret_key: SECRET, token }),
});
if (!res.ok) return false;
const data = await res.json();
return Boolean(data.ok && data.valid);
}
http.createServer(async (req, res) => {
if (!req.url || !req.url.startsWith("/auth")) {
res.writeHead(404);
return res.end("not found");
}
const token = req.headers["x-shield-token"] || readCookie(req.headers.cookie, "shield-token");
const ok = await verify(String(token || ""));
res.writeHead(ok ? 200 : 401, {
"x-shield-action": ok ? "allow" : "challenge",
});
res.end(ok ? "ok" : "challenge");
}).listen(PORT);
# Chạy: node auth-sidecar.mjs
# === nginx (fragment) ===
# location = /__shield_auth {
# internal;
# proxy_pass_request_body off;
# proxy_set_header Content-Length "";
# proxy_set_header Cookie $http_cookie;
# proxy_pass http://127.0.0.1:9090/auth;
# }
# location / {
# auth_request /__shield_auth;
# error_page 401 403 = @shield_challenge;
# proxy_pass http://origin_upstream;
# }
# location @shield_challenge {
# default_type text/html;
# # interstitial: script src=https://shield-cloud.vercel.app/v1/challenge.js data-site-key=pk_live_XXX data-api=https://shield-cloud.vercel.app data-mode=auto
# # on shield:ready → location.reload()
# }Tích hợp Backend đa ngôn ngữ
Kiến trúc hệ thống
- Frontend: nhúng challenge.js — PoW + fingerprint + cookie shield-token.
- Backend: npm @kimidevvn/shield-cloud-node hoặc POST /v1/verify với secret_key + token.
Xác thực Secret Key (Server)
Dùng sk_live_... + shield-token trên server. Không bao giờ đưa secret ra frontend. Khuyến nghị: npm i @kimidevvn/shield-cloud-node.
// npm install @kimidevvn/shield-cloud-node
import { ShieldClient, assertShield, shieldMiddleware } from "@kimidevvn/shield-cloud-node";
const shield = new ShieldClient({
secretKey: process.env.SHIELD_SECRET_KEY, // sk_live_...
apiBase: process.env.SHIELD_API_BASE || "https://shield-cloud.vercel.app",
});
// Express
// app.use(shieldMiddleware({ secretKey, apiBase }));
// Next.js Route Handler / Edge
export async function GET(req) {
const visitor = await assertShield(req, {
secretKey: process.env.SHIELD_SECRET_KEY,
apiBase: process.env.SHIELD_API_BASE || "https://shield-cloud.vercel.app",
});
return Response.json({ site: visitor.site, sid: visitor.token.sid });
}
// Manual
const result = await shield.verifyNodeRequest(req);
if (!result.ok) throw new Error(result.error);Lấy thông tin Tenant
Dùng Bearer Token để tự động hóa usage data, quản lý sites từ server của bạn.
curl "https://shield-cloud.vercel.app/saas/me" \
-H "Authorization: Bearer tn_API_TOKEN_CUA_BAN" \
-H "Accept: application/json; charset=utf-8"Captcha layer-2 (Risk cao)
Sau PoW, nếu signals nghi ngờ bot (Pro/Business): captcha fallback. Starter có thể block sớm. Còn Turnstile/demo captcha khi needs_challenge:
- Hiển thị captcha (Turnstile hoặc demo token).
- Gửi captcha_token lên POST /v1/challenge hoặc /v1/pow/captcha (pending_grant).
- Nhận behavior_token / shield-token và gắn header request tiếp theo.
curl -X POST "https://shield-cloud.vercel.app/v1/challenge" \
-H "Content-Type: application/json; charset=utf-8" \
-H "X-Shield-Site-Key: pk_live_SITE_KEY_CUA_BAN" \
-d '{"session_id":"SESSION_ID","captcha_token":"human-verified"}'Phân biệt Site Key và Secret Key
Site Key
pk_live_...Frontend/Trình duyệt (pk_live_...). An toàn khi public trên HTML.
Secret Key
sk_live_...Backend only (sk_live_...). Verify token, Worker, reverse-proxy. Không đưa ra client.
API Reference
JSON UTF-8. Content-Type: application/json. Header site: X-Shield-Site-Key.
| Endpoint | Description |
|---|---|
POST /v1/pow/issue | Cấp PoW challenge (sha256 / bandwidth) |
POST /v1/pow/verify | Verify solution + signals → shield-token |
POST /v1/pow/captcha | Captcha fallback (pending_grant) → token |
POST /v1/pow/status | Kiểm tra shield-token còn hạn |
POST /v1/init | Session + risk; có thể kèm challenge package |
POST /v1/encrypt | Mã hóa E2E payload (cần shield-token) |
POST /v1/login | Demo login được gate (cần shield-token) |
POST /v1/challenge | Captcha layer-2 → behavior_token |
POST /v1/verify | Server verify: secret_key + token |
GET /v1/challenge.js | SDK obfuscated (production) |
GET /v1/shield-sdk.js | SDK source (dev/debug) |
GET/PATCH /saas/sites/:id | Site + JS Challenge config + tokenDomain |
GET /saas/me | Tenant + usage |
GET /api/shield/config | Auto config theo domain |
Câu hỏi thường gặp (FAQ)
Q: Shield chưa báo Active trên portal?
Thêm đúng domain (localhost cho dev). Script load xong → badge Active.
Q: Lỗi origin_not_allowed?
Domain hiện tại không nằm trong allowed_origins của site. Kiểm tra portal.
Q: 403 shield_token_required / x-shield-action: challenge?
API login/encrypt yêu cầu cookie hoặc header shield-token. Dùng data-mode=auto hoặc getToken() trước khi gọi API.
Q: token_replay / nonce_replay là gì?
API Shield gated dùng token one-time (jti) + X-Nonce một lần. Copy curl chỉ dùng được 1 lần. Browser: dùng ShieldSDK.fetch — SDK tự rotate token từ header X-Shield-Token.
Q: token.score nghĩa là gì? Có chặn bot theo điểm không?
score = risk (càng cao càng nghi bot). webdriver +80, headless +50, solve quá nhanh +25… Ngưỡng app gợi ý: public 60 · login 40 · payment 25 · admin 15. Shield Starter block mint ≥70. Bảng đầy đủ: docs/HUONG-DAN-KHACH-HANG.md §3.1.
Q: Nên set maxRisk bao nhiêu trong code?
Login/register: if (score > 40) reject. Checkout: > 25. Admin/hủy tiền: > 15. Public: > 60 hoặc bỏ qua. Đừng đặt maxRisk = 0 (user sạch vẫn có thể 10–20).
Q: getToken() public — attacker có mint token giả không?
Không. getToken chỉ chạy challenge; server ký HMAC. Deobfuscate challenge.js không lộ secret. Playwright full browser vẫn lấy token thật (trả cost PoW) — cần rate limit + captcha + auth app.
Q: Tích hợp PHP thế nào (không npm)?
Embed challenge.js + POST /v1/verify bằng cURL với sk_live và cookie shield-token. Helper đầy đủ trong docs/HUONG-DAN-KHACH-HANG.md §4.5.
Q: Localhost đã bật Mode C chưa?
Chưa. Local = SelfWaf (Mode A interstitial) + middleware gate API. Mode C cần Cloudflare Worker hoặc Nginx sidecar (integrations/).
Q: Khác nhau challenge.js và shield-sdk.js?
challenge.js là bản obfuscate (production). shield-sdk.js là source dễ debug. API giống nhau (window.ShieldSDK).
Q: Publish @kimidevvn/shield-cloud-node thế nào?
npm login (granular token + 2FA), rồi npm run publish:node-sdk. Khách: npm i @kimidevvn/shield-cloud-node.
Q: Mode C Worker cần secret nào?
SHIELD_SECRET_KEY, SHIELD_SITE_KEY, SHIELD_API_BASE, ORIGIN — xem integrations/cloudflare-worker.
Q: Custom domain token.* không resolve?
CNAME đúng cname_target; lưu tokenDomain trong Portal; đợi DNS. Gọi API qua https://token.domain.com.
Q: Vượt quota request?
Request mới bị từ chối. Xem Usage dashboard và nâng Pro/Business.