เอกสารสำหรับนักพัฒนา

คู่มือการผสาน KeyThai License API เข้ากับซอฟต์แวร์ของคุณ — API base: https://api.keythai.net/v1

เริ่มต้นใช้งาน

  1. สร้างบัญชี — สมัครที่ /signup แล้วยืนยันอีเมล ระบบจะสร้าง tenant และ Ed25519 keypair ให้อัตโนมัติ
  2. สร้าง Product — ใน dashboard กำหนดชื่อ, code และแพลตฟอร์มของซอฟต์แวร์
  3. สร้าง Policy — เลือกประเภท (subscription / trial / floating / perpetual), จำนวนเครื่องสูงสุด (max_activations) และอนุญาต offline หรือไม่
  4. สร้าง License — ออก license key รูปแบบ KEYT-XXXX-XXXX-XXXX-XXXX-XXXX (key จะโชว์ครั้งเดียว ระบบเก็บเฉพาะ SHA-256 hash)
  5. รับ API Key — สร้าง API key (kt_live_...) ในหน้า API Keys เพื่อใช้เรียก License API

การยืนยันตัวตน

ทุก request ต้องแนบ API key ของ tenant ใน header แบบ Bearer token:

header
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 เดิม

request
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 (เลือกใส่ได้)

response 200
{
  "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 เลือกใส่ได้ ถ้าต้องการเช็คว่าเครื่องนี้ลงทะเบียนไว้)

request
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

request
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

request
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

request
curl https://api.keythai.net/v1/keys \
  -H "Authorization: Bearer kt_live_xxxxxxxxxxxx"
response 200
{
  "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 แสดงครั้งเดียวตอนสร้างเท่านั้น

MethodPathScope
POST/v1/licenseslicense:write
GET/v1/licenseslicense:read
GET/v1/licenses/{key}license:read
PATCH/v1/licenses/{key}license:write
DELETE/v1/licenses/{key}/machines/{fp}license:write
GET/v1/productslicense:read
GET/v1/policieslicense:read
GET/v1/entitlementslicense:read
POST/v1/entitlementslicense:write
DELETE/v1/entitlements/{id}license:write
PUT/v1/policies/{policyId}/entitlementslicense:write

สร้าง license

request
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

ลิสต์ + ค้นหา + แบ่งหน้า

request
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" }

เปลี่ยนสถานะ / แก้ไขข้อมูล

request
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)

request
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

request
# ใช้หา 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

request
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

request
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

request
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 ว่างเพื่อ ล้างทั้งหมด

request
# กำหนดชุด 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 ที่เซ็น

response 200 (activate/validate)
{
  "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 จึงเรียกได้อย่างปลอดภัย:

hasEntitlement / has_entitlement / HasEntitlement
// 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 response
{
  "error": {
    "code": "SEAT_LIMIT_REACHED",
    "message": "เปิดใช้งานครบจำนวนเครื่องสูงสุดแล้ว"
  }
}
CodeHTTPคำอธิบาย
INVALID_KEY404ไม่พบ license key นี้ในระบบ
LICENSE_SUSPENDED403license ถูกระงับการใช้งานชั่วคราว
LICENSE_REVOKED403license ถูกเพิกถอนถาวร
LICENSE_EXPIRED403license หมดอายุแล้ว
SEAT_LIMIT_REACHED409เปิดใช้งานครบจำนวนเครื่องสูงสุดแล้ว
MACHINE_NOT_FOUND404ไม่พบเครื่องนี้ (fingerprint) ใน license
RATE_LIMITED429เรียก API ถี่เกินไป ลองใหม่ภายหลัง
QUOTA_EXCEEDED429ใช้โควตา API รายวันของแพ็กเกจหมดแล้ว
UNAUTHORIZED401API key ไม่ถูกต้องหรือไม่ได้แนบมา
VALIDATION_ERROR400request body ไม่ถูกต้องตามรูปแบบ

Device Fingerprint

fingerprint คือสตริงเฉพาะของแต่ละเครื่อง (ความยาว 8–255 ตัวอักษร) ใช้ผูก license กับเครื่อง แนะนำให้นำค่าเฉพาะของเครื่องมา hash ด้วย SHA-256 แล้วส่งเป็น hex string

  • Windows: ใช้ค่า MachineGuid จาก registry HKLM\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 ออกก่อนเซ็น

verify (WebCrypto)
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 ได้โดยไม่ต้องต่อเน็ต

offline .lic
// .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 }

webhook payload
{
  "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.activated
  • X-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

verify webhook (Node.js)
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)

JavaScript / TypeScript
// 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
# 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# / .NET
// 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 8sdk-examples/csharp/KeyThaiClient.cs (HttpClient + fingerprint จาก MachineGuid + verify Ed25519)
  • Node.jssdk-examples/node/keythai-client.mjs (fetch + WebCrypto offline verify)
  • Pythonsdk-examples/python/keythai_client.py (requests + cryptography/pynacl)