เอกสารสำหรับนักพัฒนา
คู่มือการผสาน KeyThai License API เข้ากับซอฟต์แวร์ของคุณ — API base: https://api.keythai.net/v1
เริ่มต้นใช้งาน
- สร้างบัญชี — สมัครที่ /signup แล้วยืนยันอีเมล ระบบจะสร้าง tenant และ Ed25519 keypair ให้อัตโนมัติ
- สร้าง Product — ใน dashboard กำหนดชื่อ, code และแพลตฟอร์มของซอฟต์แวร์
- สร้าง Policy — เลือกประเภท (subscription / trial / floating / perpetual), จำนวนเครื่องสูงสุด (max_activations) และอนุญาต offline หรือไม่
- สร้าง License — ออก license key รูปแบบ
KEYT-XXXX-XXXX-XXXX-XXXX-XXXX(key จะโชว์ครั้งเดียว ระบบเก็บเฉพาะ SHA-256 hash) - รับ API Key — สร้าง API key (
kt_live_...) ในหน้า API Keys เพื่อใช้เรียก License API
การยืนยันตัวตน
ทุก request ต้องแนบ API key ของ tenant ใน header แบบ Bearer token:
Authorization: Bearer kt_live_xxxxxxxxxxxxหาก API key ไม่ถูกต้องหรือไม่ได้แนบมา จะได้รับ 401 UNAUTHORIZED โดยมีการจำกัด rate limit และโควตา API รายวันตามแพ็กเกจ
API Reference
endpoint ของ License API ทั้งหมดอยู่ภายใต้ https://api.keythai.net/v1 โดย {key} คือ license key เต็ม
POST /v1/licenses/{key}/activate
ลงทะเบียน fingerprint ของเครื่อง (กินที่นั่ง 1 seat) — idempotent ต่อ fingerprint เดิม
curl -X POST https://api.keythai.net/v1/licenses/KEYT-AB12-3C4D-5E6F-7G8H-9J0K/activate \
-H "Authorization: Bearer kt_live_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"fingerprint": "a1b2c3d4e5f6...",
"name": "เครื่องของสมชาย",
"platform": "windows",
"hostname": "DESKTOP-1234"
}'Request body: fingerprint (จำเป็น), name, platform, hostname (เลือกใส่ได้)
{
"valid": true,
"status": "active",
"expires_at": 1767225600,
"machine_count": 1,
"max_activations": 3,
"policy": { "type": "subscription", "offline_allowed": true },
"issued_at": 1717286400,
"nonce": "8f3a...",
"signature": "base64-ed25519-signature..."
}ทุก field ยกเว้น signature คือ payload ที่ถูกเซ็น (ดูหัวข้อ “ตรวจลายเซ็น”) หากเกินจำนวนเครื่อง จะได้ 409 SEAT_LIMIT_REACHED
POST /v1/licenses/{key}/validate
ตรวจสอบสถานะ / วันหมดอายุ / fingerprint — ตอบกลับ payload พร้อมลายเซ็นเหมือน activate (fingerprint เลือกใส่ได้ ถ้าต้องการเช็คว่าเครื่องนี้ลงทะเบียนไว้)
curl -X POST https://api.keythai.net/v1/licenses/KEYT-AB12-3C4D-5E6F-7G8H-9J0K/validate \
-H "Authorization: Bearer kt_live_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "fingerprint": "a1b2c3d4e5f6..." }'POST /v1/licenses/{key}/deactivate
คืนที่นั่ง โดยลบ fingerprint ที่ระบุออกจาก license
curl -X POST https://api.keythai.net/v1/licenses/KEYT-AB12-3C4D-5E6F-7G8H-9J0K/deactivate \
-H "Authorization: Bearer kt_live_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "fingerprint": "a1b2c3d4e5f6..." }'POST /v1/licenses/{key}/heartbeat
อัปเดต liveness ของเครื่อง (สำหรับ floating license) — ควรเรียกเป็นระยะตาม heartbeat_interval
curl -X POST https://api.keythai.net/v1/licenses/KEYT-AB12-3C4D-5E6F-7G8H-9J0K/heartbeat \
-H "Authorization: Bearer kt_live_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "fingerprint": "a1b2c3d4e5f6..." }'GET /v1/keys
ดึง public JWK (Ed25519) สำหรับนำไป verify ลายเซ็นแบบ offline
curl https://api.keythai.net/v1/keys \
-H "Authorization: Bearer kt_live_xxxxxxxxxxxx"{
"keys": [
{
"kid": "key-2024-01",
"alg": "EdDSA",
"publicJwk": {
"kty": "OKP",
"crv": "Ed25519",
"x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"
}
}
]
}Management API
Management API ใช้ออกและจัดการ license แบบอัตโนมัติผ่าน API key ของคุณ — เช่น ออก key ทันทีหลังลูกค้าชำระเงินในระบบของคุณเอง โดยไม่ต้องเข้า dashboard ทุก request อยู่ภายใต้ rate limit และโควตา API ต่อวันของแพ็กเกจเหมือน License API
Scopes ของ API key
API key แต่ละตัวมี scopes กำหนดสิทธิ์: license:read (อ่านข้อมูล — list/detail/products/policies) และ license:write (สร้าง/แก้ไข/ถอนเครื่อง) — key ที่ไม่มี scope ที่ต้องการจะได้ 403 FORBIDDEN (key ใหม่ได้ทั้งสอง scope โดยค่าเริ่มต้น)
4 endpoint เดิม (activate / validate / deactivate / heartbeat) ไม่ต้องใช้ scope พิเศษ — ใช้ API key ที่ถูกต้องก็พอ และ plaintext license key แสดงครั้งเดียวตอนสร้างเท่านั้น
| Method | Path | Scope |
|---|---|---|
| POST | /v1/licenses | license:write |
| GET | /v1/licenses | license:read |
| GET | /v1/licenses/{key} | license:read |
| PATCH | /v1/licenses/{key} | license:write |
| DELETE | /v1/licenses/{key}/machines/{fp} | license:write |
| GET | /v1/products | license:read |
| GET | /v1/policies | license:read |
| GET | /v1/entitlements | license:read |
| POST | /v1/entitlements | license:write |
| DELETE | /v1/entitlements/{id} | license:write |
| PUT | /v1/policies/{policyId}/entitlements | license:write |
สร้าง license
curl -X POST https://api.keythai.net/v1/licenses \
-H "Authorization: Bearer kt_live_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{
"product": "my-app",
"policyId": "01J...",
"holderName": "สมชาย",
"holderEmail": "somchai@example.com",
"metadata": {"order": "INV-001"},
"quantity": 1
}'
# 201 → { "licenses": [{ "key": "KEYT-AB12-....", "id", "key_prefix",
# "key_last4", "status", "expires_at", "max_activations" }] }
# "key" คือ plaintext — แสดงครั้งเดียว! เกินโควตาแผน → 403 QUOTA_EXCEEDEDลิสต์ + ค้นหา + แบ่งหน้า
curl "https://api.keythai.net/v1/licenses?status=active&q=somchai&page=1&limit=25" \
-H "Authorization: Bearer kt_live_xxxxxxxxxxxx"
# q ค้นจาก key_prefix / key_last4 / holderName / holderEmail
# → { "data": [...ไม่มี plaintext key...], "total", "page", "limit" }เปลี่ยนสถานะ / แก้ไขข้อมูล
curl -X PATCH https://api.keythai.net/v1/licenses/KEYT-AB12-3C4D-5E6F-7G8H-9J0K \
-H "Authorization: Bearer kt_live_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"action": "suspend"}'
# action: suspend | resume | revoke (revoke = ถาวร แก้อีกไม่ได้)
# หรือแก้ field: {"holderName", "holderEmail", "metadata", "expiresAt"}ถอนเครื่อง (คืน seat)
curl -X DELETE "https://api.keythai.net/v1/licenses/KEYT-AB12-3C4D-5E6F-7G8H-9J0K/machines/a1b2c3d4..." \
-H "Authorization: Bearer kt_live_xxxxxxxxxxxx"
# → { "ok": true, "machine_count": N }Products / Policies
# ใช้หา id ของ product/policy ก่อนสร้าง license
curl https://api.keythai.net/v1/products -H "Authorization: Bearer kt_live_..."
curl "https://api.keythai.net/v1/policies?productId=01J..." -H "Authorization: Bearer kt_live_..."Entitlements (ธงเปิดฟีเจอร์)
Entitlement คือธงเปิดฟีเจอร์แบบหยาบ (เช่น export.pdf, premium) ที่ผูกกับ policy — license ทุกใบที่ออกจาก policy นั้นจะได้ codes เหล่านี้ โดยฝังอยู่ ภายใน payload ที่เซ็น ของ activate / validate และไฟล์ .lic ทำให้แอป เปิด/ปิดฟีเจอร์ได้แบบ offline เมื่อตรวจลายเซ็นผ่านแล้ว
กฎการเซ็น: field entitlements เป็น array ของสตริง เรียงตามตัวอักษร และจะถูก ตัดออกทั้งหมด (absent) เมื่อ policy ไม่มี entitlement เลย — payload ของ license ที่ไม่มี entitlement จึงเหมือนรูปแบบเดิมก่อนมีฟีเจอร์นี้ทุกประการ (.lic / SDK รุ่นเก่ายังใช้ได้)
ลิสต์ entitlement
curl "https://api.keythai.net/v1/entitlements?productId=01J..." \
-H "Authorization: Bearer kt_live_xxxxxxxxxxxx"
# productId เลือกใส่ได้ (ไม่ใส่ = ทุก product ของ tenant) — เรียงตาม code
# → { "data": [{ "id", "product_id", "code", "name", "created_at" }, ...] }สร้าง entitlement
curl -X POST https://api.keythai.net/v1/entitlements \
-H "Authorization: Bearer kt_live_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "productId": "01J...", "code": "export.pdf", "name": "ส่งออก PDF" }'
# 201 → { "id", "product_id", "code", "name", "created_at" }
# code: a-z 0-9 _ : - ยาว 1-64 ตัว — ซ้ำใน product เดิม → 409 VALIDATION_ERRORลบ entitlement
curl -X DELETE https://api.keythai.net/v1/entitlements/01J... \
-H "Authorization: Bearer kt_live_xxxxxxxxxxxx"
# → { "ok": true } (grant ใน policy ที่อ้าง entitlement นี้ถูกลบตามแบบ cascade)ผูก entitlement เข้ากับ policy
PUT /v1/policies/{policyId}/entitlements กำหนดชุด entitlement ของ policy แบบ แทนที่ทั้งชุด — ส่ง array ว่างเพื่อ ล้างทั้งหมด
# กำหนดชุด entitlement ของ policy แบบ "แทนที่ทั้งชุด" (replace set)
curl -X PUT https://api.keythai.net/v1/policies/01J.../entitlements \
-H "Authorization: Bearer kt_live_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{ "entitlementIds": ["01Jent1...", "01Jent2..."] }'
# → { "ok": true, "entitlement_ids": [...] }
# entitlementIds: [] = ล้าง grant ทั้งหมด; ทุก id ต้องอยู่ใน product เดียวกับ policy
# จากนั้น license ของ policy นี้จะพก codes เหล่านี้ใน payload ที่เซ็น (activate/validate)entitlements ใน response ที่เซ็น
{
"valid": true,
"status": "active",
"expires_at": 1767225600,
"machine_count": 1,
"max_activations": 3,
"policy": { "type": "subscription", "offline_allowed": true },
"issued_at": 1717286400,
"nonce": "8f3a...",
"entitlements": ["export.pdf", "premium"],
"signature": "base64-ed25519-signature..."
}ในฝั่งแอป เรียกตัวช่วยของ SDK เพื่อเช็คฟีเจอร์ — คืน false เสมอเมื่อไม่มี entitlements หรือ payload เป็น null/None จึงเรียกได้อย่างปลอดภัย:
// JS/TS (keythai-sdk)
import { hasEntitlement } from "keythai-sdk";
if (hasEntitlement(lic.payload, "export.pdf")) enablePdfExport();
# Python (keythai)
from keythai import has_entitlement
if has_entitlement(lic["payload"], "export.pdf"):
enable_pdf_export()
// C# (KeyThai.Client)
if (KeyThaiVerifier.HasEntitlement(lic.Payload, "export.pdf"))
EnablePdfExport();Error Codes
เมื่อเกิดข้อผิดพลาด API จะตอบกลับในรูปแบบ { "error": { "code", "message" } }
{
"error": {
"code": "SEAT_LIMIT_REACHED",
"message": "เปิดใช้งานครบจำนวนเครื่องสูงสุดแล้ว"
}
}| Code | HTTP | คำอธิบาย |
|---|---|---|
| INVALID_KEY | 404 | ไม่พบ license key นี้ในระบบ |
| LICENSE_SUSPENDED | 403 | license ถูกระงับการใช้งานชั่วคราว |
| LICENSE_REVOKED | 403 | license ถูกเพิกถอนถาวร |
| LICENSE_EXPIRED | 403 | license หมดอายุแล้ว |
| SEAT_LIMIT_REACHED | 409 | เปิดใช้งานครบจำนวนเครื่องสูงสุดแล้ว |
| MACHINE_NOT_FOUND | 404 | ไม่พบเครื่องนี้ (fingerprint) ใน license |
| RATE_LIMITED | 429 | เรียก API ถี่เกินไป ลองใหม่ภายหลัง |
| QUOTA_EXCEEDED | 429 | ใช้โควตา API รายวันของแพ็กเกจหมดแล้ว |
| UNAUTHORIZED | 401 | API key ไม่ถูกต้องหรือไม่ได้แนบมา |
| VALIDATION_ERROR | 400 | request body ไม่ถูกต้องตามรูปแบบ |
Device Fingerprint
fingerprint คือสตริงเฉพาะของแต่ละเครื่อง (ความยาว 8–255 ตัวอักษร) ใช้ผูก license กับเครื่อง แนะนำให้นำค่าเฉพาะของเครื่องมา hash ด้วย SHA-256 แล้วส่งเป็น hex string
- Windows: ใช้ค่า
MachineGuidจาก registryHKLM\SOFTWARE\Microsoft\Cryptographyแล้ว hash SHA-256 - macOS: ใช้
IOPlatformUUID(จาก ioreg) แล้ว hash - Linux: ใช้
/etc/machine-idหรือ/var/lib/dbus/machine-id - ทั่วไป / Node: รวม hostname + MAC address แล้ว hash SHA-256 เป็น fallback
สำคัญ: ควรให้ค่า fingerprint คงที่ตลอดอายุการใช้งานบนเครื่องเดียวกัน เพื่อให้ activate เป็น idempotent (ไม่กินที่นั่งเพิ่ม)
ตรวจลายเซ็น Ed25519 & ไฟล์ Offline .lic
ทุก response ของ activate / validate มี field signature ซึ่งเป็นลายเซ็น Ed25519 (base64) เซ็นทับ canonical JSON ของ payload (เรียง key ตามตัวอักษร, ตัด field ที่เป็น undefined ออก) โดยตัด field signature ออกก่อนเซ็น
import { canonicalJson } from "@keythai/shared"; // หรือคัดลอกฟังก์ชันลงในแอป
// payload = response โดยตัด field "signature" ออก
const { signature, ...payload } = res;
const message = new TextEncoder().encode(canonicalJson(payload));
const sig = Uint8Array.from(atob(signature), (c) => c.charCodeAt(0));
const key = await crypto.subtle.importKey(
"jwk", publicJwk, { name: "Ed25519" }, false, ["verify"],
);
const ok = await crypto.subtle.verify({ name: "Ed25519" }, key, sig, message);ไฟล์ .lic แบบ offline: export จาก dashboard ได้เป็น base64 ของ { payload, signature, kid } แอป desktop ที่ฝัง public key ไว้สามารถ verify ได้โดยไม่ต้องต่อเน็ต
// .lic = base64( JSON.stringify({ payload, signature, kid }) )
const decoded = JSON.parse(atob(licFileContents));
// decoded.payload คือ SignedLicensePayload
// 1) verify decoded.signature เทียบกับ canonicalJson(decoded.payload) + public key ที่ฝังไว้
// 2) ตรวจ payload.expires_at เทียบเวลาปัจจุบัน
// 3) ตรวจ fingerprint ของเครื่องตรงกับที่อนุญาตหรือไม่Webhooks
KeyThai สามารถยิง webhook ไปยัง endpoint ของคุณเมื่อเกิดเหตุการณ์กับ license ตั้งค่า endpoint และเลือกประเภทเหตุการณ์ที่ต้องการได้ที่ /dashboard/webhooks ระบบจะสร้าง signing secret (kt_whsec_...) ให้ครั้งเดียวตอนสร้าง endpoint — เก็บไว้ให้ดีเพื่อใช้ตรวจสอบลายเซ็น
ประเภทเหตุการณ์ (Event Types)
license.created— สร้าง license ใหม่license.activated— เปิดใช้งานเครื่อง (กิน seat)license.deactivated— คืน seat ของเครื่องlicense.suspended— ระงับ license ชั่วคราวlicense.revoked— เพิกถอน license ถาวรlicense.expired— license หมดอายุ
รูปแบบ Payload
ทุก request เป็น POST พร้อม body เป็น JSON ในรูปแบบ { id, event, created_at, data }
{
"id": "whd_01J...",
"event": "license.activated",
"created_at": 1717286400,
"data": {
"license": {
"id": "lic_01J...",
"key_prefix": "KEYT-AB12",
"status": "active",
"product_id": "prd_01J...",
"expires_at": 1767225600
}
}
}Headers
X-KeyThai-Event— ประเภทเหตุการณ์ เช่นlicense.activatedX-KeyThai-Delivery— id ของการส่งครั้งนี้ (ใช้ทำ idempotency ฝั่งคุณ)X-KeyThai-Signature— ลายเซ็น HMAC ในรูปแบบsha256=<hex>คำนวณจาก raw body ด้วย secret ของ endpoint
การตรวจสอบลายเซ็น (Node.js)
ใช้ raw request body (ก่อน parse เป็น JSON) คำนวณ HMAC-SHA256 ด้วย secret ของ endpoint แล้วเทียบกับค่าใน header X-KeyThai-Signature ด้วยการเทียบแบบ timing-safe เพื่อกัน timing attack
import { createHmac, timingSafeEqual } from "node:crypto";
function verifyKeyThaiWebhook(rawBody, signatureHeader, secret) {
const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
return timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader));
}นโยบายลองส่งซ้ำ (Retry)
endpoint ของคุณควรตอบกลับสถานะ 2xx ภายใน 10 วินาที หากล้มเหลว ระบบจะลองส่งซ้ำตามช่วงเวลา 1 นาที → 10 นาที → 1 ชั่วโมง → 6 ชั่วโมง สูงสุด 5 ครั้ง หลังจากนั้นจะถือว่าการส่งล้มเหลวถาวร
Auto-heartbeat (SDK)
สำหรับ license แบบ floating / นับที่นั่ง SDK ทั้งสามภาษามีตัวช่วยส่ง heartbeat อัตโนมัติในเบื้องหลัง: beat แรกส่งทันที แล้ววนซ้ำตาม interval ลูปจะหยุดเองเมื่อ license อยู่ในสถานะสุดท้าย (LICENSE_REVOKED, LICENSE_SUSPENDED, LICENSE_EXPIRED, MACHINE_NOT_FOUND) ส่วน error ชั่วคราวจะส่งเข้า onError แล้ววนต่อ เซิร์ฟเวอร์จะคืนที่นั่งเมื่อเงียบเกินประมาณ 3 เท่าของ heartbeat interval — ควร beat ที่ค่า policy หรือต่ำกว่า (อ่านจาก listPolicies() → heartbeat_interval_seconds; v0.2.0)
// JS/TS (keythai-sdk)
const stopper = client.startAutoHeartbeat(key, fingerprint, {
intervalSeconds: 60, // ค่าเริ่มต้น 300; อ่านจาก listPolicies() → heartbeat_interval_seconds
onBeat: (res) => console.log("alive:", res.status),
onError: (err) => console.warn("heartbeat failed:", err),
});
// เมื่อปิดแอป:
stopper.stop(); // idempotent# Python (keythai)
handle = client.start_auto_heartbeat(
key, fp,
interval_seconds=None, # None → policy heartbeat_interval_seconds มิฉะนั้น 300s
on_beat=lambda res: print("beat ok"),
on_error=lambda exc: print("heartbeat failed:", exc),
)
handle.stop() # idempotent (หรือ client.stop_auto_heartbeat())// C# (KeyThai.Client)
IDisposable beat = client.StartAutoHeartbeat(
key, fingerprint,
interval: TimeSpan.FromSeconds(60), // null → policy heartbeat_interval_seconds มิฉะนั้น 300s
onBeat: r => Console.WriteLine($"beat #{r.BeatNumber}"),
onError: ex => Console.WriteLine($"error: {ex.Message}"));
beat.Dispose(); // หยุด loop (หรือเมื่อ client ถูก dispose)หน้าขาย license (Sell Pages)
หน้าขาย license คือหน้าเช็คเอาต์สาธารณะที่ KeyThai โฮสต์ให้ที่ keythai.net/s/<slug> ลูกค้าเลือกแพ็กเกจ ชำระด้วย PromptPay แล้วแนบสลิป จากนั้นคุณกดอนุมัติใน dashboard ระบบจะออก license key ให้อัตโนมัติและส่งอีเมลให้ลูกค้า — ตั้งค่าได้ที่ /dashboard/sell-pages
เงินเข้า PromptPay ของผู้ขายโดยตรง — KeyThai ไม่เก็บ/ไม่ถือเงินของคุณ และไม่หักค่าธรรมเนียมการขาย ระบบเพียงสร้าง QR และช่วยจัดการคำสั่งซื้อ/ออก license เท่านั้น
ให้ลูกค้าย้ายเครื่องเอง (/manage)
เปิดตัวเลือก selfServiceReset ของ policy เพื่ออนุญาตให้ผู้ถือ license จัดการเองที่ /manage — กรอก license key ของตน ดูเครื่องที่ลงทะเบียน และถอน/ย้ายเครื่องด้วยตนเองโดยไม่ต้องติดต่อคุณ (ลดงานซัพพอร์ตเวลาเปลี่ยนเครื่อง) หากนโยบายไม่เปิดสิทธิ์นี้ การถอนเครื่องจะทำได้เฉพาะผ่าน dashboard หรือ Management API
ตัวอย่าง SDK
เรามีตัวอย่าง client พร้อมใช้งานในโฟลเดอร์ sdk-examples สำหรับภาษายอดนิยม:
- C# / .NET 8 —
sdk-examples/csharp/KeyThaiClient.cs(HttpClient + fingerprint จาก MachineGuid + verify Ed25519) - Node.js —
sdk-examples/node/keythai-client.mjs(fetch + WebCrypto offline verify) - Python —
sdk-examples/python/keythai_client.py(requests + cryptography/pynacl)