กำลังโหลด...

API Gateway Integration Guide

กลับหน้าหลัก API Gateway

CU E-Receipt — API Gateway Integration Guide

สำหรับ: External Developer / System Integrator เวอร์ชัน: 2.2 วันที่: พฤษภาคม 2569 ติดต่อ: thannaphat.j@zenithcomp.co.th


1. ภาพรวมระบบ

ระบบ CU E-Receipt API Gateway ให้ External System สามารถ:

  • ดึงรายการโครงการ ที่พร้อมชำระเงินทุกประเภทในเส้นเดียว
  • สร้าง Payment Session เพื่อรับ URL สำหรับ redirect ผู้ใช้ไปชำระเงิน
  • ส่งรายการย่อย สำหรับแสดงในใบเสร็จ PDF
  • รับ Webhook แจ้งผลการชำระเงินแบบ Real-time
  • รับเลขใบเสร็จ พร้อม receipt URL สำหรับดาวน์โหลด PDF

การลงทะเบียน: ทำผ่าน Web UI ของ CU E-Receipt เท่านั้น ผู้ดูแลระบบจะตรวจสอบและอนุมัติ จากนั้น client_id, client_secret และ webhook_secret จะถูกส่งทาง email โดยอัตโนมัติ

Flow Overview

External System                     CU E-Receipt
─────────────────                   ────────────────────────────────
[ลงทะเบียนผ่าน Web UI ───────────── อนุมัติโดย Admin]
[รับ credentials ทาง email ◄──────── ส่ง client_id, client_secret, webhook_secret]

══════════════ เปิดใช้งานแล้ว ══════════════

[1]   ขอ Access Token ─────────────► POST /oauth/token
                                      ◄─── {access_token}

[1.5] ดึงรายการโครงการ ────────────► POST /api/v1/projects  {"type":"content"}
      (Bearer Token + HMAC Signature) ◄─── [{project_code, project_name, type, amount, ...}]

[2]   สร้าง Payment Session ────────► POST /api/v1/payment/create
      (Bearer Token + HMAC Signature) ◄─── {payment_url, booking_ref}

[3]   Redirect user ────────────────► payment_url (หน้าชำระเงิน)
      user ชำระเงิน

[4]                           ระบบ ──► POST callback_url (Webhook)
                                        {event: payment.success, ref_no, receipt_no, ...}

ref_no = เลขอ้างอิงจาก system ของคุณ — ใช้ track payment ระหว่าง 2 ระบบ booking_ref = เลข booking ของระบบ CU — ใช้อ้างอิงฝั่ง CU


2. การขอ Access Token

ใช้ OAuth 2.0 Client Credentials flow — ขอ token ใหม่ทุกครั้งก่อนเรียก API

POST /oauth/token

POST /oauth/token
Content-Type: application/json

{
  "grant_type": "client_credentials",
  "client_id": "550e8400-e29b-41d4-a716-446655440000",
  "client_secret": "AbCd1234...64chars...",
  "scope": "payment:create"
}

Response 200:

{
  "token_type": "Bearer",
  "expires_in": 1800,
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9..."
}
Field Value
expires_in 1,800 วินาที = 30 นาที
Token type JWT (RS256)
Refresh token ไม่มี

วิธีใช้งาน: ขอ Access Token ใหม่ทุกครั้ง ก่อน เรียก /api/v1/payment/create ไม่ต้อง track expiry หรือ cache — แค่ขอแล้วใช้ทันที แล้วทิ้ง


3. ดึงรายการโครงการที่พร้อมชำระเงิน

POST /api/v1/projects

ระบบ auto-filter ตาม fac_code ของ Gateway Client — ไม่ต้องระบุคณะเอง ใช้ project_code และ type ที่ได้รับจาก response นี้ไปส่งใน POST /api/v1/payment/create (ต้องส่งคู่กันเสมอ)

ทำไมใช้ POST? filter ถูกส่งใน JSON body ซึ่งถูก sign ด้วย HMAC — ป้องกันการแก้ไข filter ระหว่างทาง

Required Headers

Header Description
Authorization Bearer {access_token}
Content-Type application/json
X-Timestamp Unix timestamp (วินาที) — ต้องไม่ต่างจากเวลาเซิร์ฟเวอร์เกิน ±5 นาที
X-Nonce Random string ยาว 16–64 chars — ห้ามซ้ำ ต่อ request
X-Signature HMAC-SHA256 signature คำนวณจาก JSON body (ดู Section 6)

Request Body (ทั้งหมด optional)

Field Type Default Description
search string ค้นหาจากชื่อโครงการ
type string ทุกประเภท content | health_science | room
page integer 1 หน้าที่ต้องการ
per_page integer 20 รายการต่อหน้า (สูงสุด 50)

ถ้าไม่ต้องการ filter ให้ส่ง body เป็น {} หรือ empty body ("")

ตัวอย่าง Request

POST /api/v1/projects
Authorization: Bearer eyJ0eXAiOiJKV1Qi...
Content-Type: application/json
X-Timestamp: 1741852800
X-Nonce: k9xM2pLnQr7vYwZ3
X-Signature: base64encodedHmacSignature==

{
  "type": "content",
  "search": "Python",
  "page": 1,
  "per_page": 20
}

Response 200

{
  "status": "success",
  "fac_code": "3700",
  "data": [
    {
      "type": "content",
      "project_code": "37000001",
      "project_name": "อบรม Python Basics รุ่นที่ 3",
      "amount": 1700.00,
      "faculty": "3700",
      "start_date": "2026-01-01 00:00:00",
      "end_date": "2026-12-31 00:00:00"
    },
    {
      "type": "health_science",
      "project_code": "HS690001",
      "project_name": "บริการตรวจสุขภาพพนักงาน",
      "amount": "1810.00",
      "faculty": "09001",
      "start_date": null,
      "end_date": null
    },
    {
      "type": "room",
      "project_code": "RM690001",
      "project_name": "ห้องพักนิสิต A101",
      "amount": "3500.00",
      "faculty": "0104",
      "start_date": null,
      "end_date": null
    }
  ],
  "pagination": {
    "total": 12,
    "per_page": 20,
    "current_page": 1,
    "last_page": 1
  }
}
Field Description
fac_code รหัสคณะของ Gateway Client (null = ระดับ System Admin)
data[].type ประเภทโครงการ: content, health_science, roomต้องส่งเป็น type ใน POST /api/v1/payment/create
data[].project_code รหัสโครงการ — ต้องส่งคู่กับ type ใน POST /api/v1/payment/create
data[].project_name ชื่อโครงการ
data[].amount จำนวนเงิน (บาท)

4. การสร้าง Payment Session

POST /api/v1/payment/create

Required Headers

Header Description
Authorization Bearer {access_token}
Content-Type application/json
X-Timestamp Unix timestamp (วินาที) — ต้องไม่ต่างจากเวลาเซิร์ฟเวอร์เกิน ±5 นาที
X-Nonce Random string ยาว 16–64 chars — ห้ามซ้ำ ต่อ request
X-Signature HMAC-SHA256 signature (ดูวิธีคำนวณ Section 6)

Request Body

Field Type Required Description
mode string โหมดการทำงาน: test (default) หรือ productionดูหมายเหตุด้านล่าง
project_code string รหัสโครงการ (field project_code จาก POST /api/v1/projects)
type string ประเภทโครงการ (field type จาก POST /api/v1/projects): content | health_science | room
amount number ⚠️ จำนวนเงินรวม (บาท) — required เมื่อ type=content หรือ health_science; optional เมื่อ type=room (คำนวณจาก room.unit_price + data อัตโนมัติ)
payer_name string ⚠️ ชื่อผู้ชำระ — required เมื่อ type=content หรือ health_science; optional เมื่อ type=room
payer_email string ⚠️ อีเมลผู้ชำระ — required เมื่อ type=content หรือ health_science; optional เมื่อ type=room
payer_phone string ⚠️ เบอร์โทรผู้ชำระ — required เมื่อ type=content หรือ health_science; optional เมื่อ type=room
payer_organization string ชื่อองค์กรผู้ชำระ
ref_no string เลขอ้างอิงจาก system ของคุณ (จะส่งกลับใน webhook) — สูงสุด 55 อักษร
callback_url string ⚠️ URL สำหรับรับ webhook — required เมื่อ mode=production, optional เมื่อ mode=test
title string ⚠️ ชื่อหัวข้อรายการในใบเสร็จ — required เมื่อส่ง data (type!=room); optional เมื่อ type=room
data array รายการค่าใช้จ่ายเพิ่มเติม เช่น ค่าน้ำ ค่าไฟ — optional ทุก type (ดูรายละเอียดด้านล่าง)
room object ⚠️ ข้อมูลห้องพัก — required เมื่อ type=room (ดูรายละเอียดด้านล่าง)
receipt_info object ข้อมูลผู้รับใบเสร็จ — optional สำหรับ type=room (ดูรายละเอียดด้านล่าง)

mode — ความแตกต่าง:

mode callback_url การทำงาน
test (default) optional ทดสอบ flow ได้โดยไม่ต้องมี callback server
production required ใช้งานจริง — ระบบ POST webhook ไปที่ callback_url เมื่อชำระเงินสำเร็จ

หมายเหตุ: บน Production server (ereceipt.ofas.chula.ac.th) callback_url บังคับ เสมอ ไม่ว่าจะส่ง mode อะไรมา

room Object (required ทุก field เมื่อ type=room)

เมื่อ type=room ต้องส่ง object room พร้อมข้อมูลครบถ้วน

Field Type Required Description
room.building_id integer รหัสอาคาร (≥ 1)
room.floor integer ชั้น (≥ 1)
room.room_id integer รหัสห้อง (≥ 1)
room.booking_type string ประเภทการจอง: daily (รายวัน) | monthly (รายเดือน) | term (รายเทอม)
room.start_date string วันที่เริ่มต้น รูปแบบ YYYY-MM-DD เช่น 2026-06-01
room.end_date string วันที่สิ้นสุด รูปแบบ YYYY-MM-DD — ต้องไม่น้อยกว่า start_date
room.unit_price number ราคาค่าเช่าหลัก (บาท) — ใช้เป็น main item ในใบเสร็จ

การคำนวณ amount: total = room.unit_price + sum(data[].quantity × data[].unit_price) หากส่ง amount มาด้วย ต้องตรงกับค่าที่คำนวณได้ (tolerance ±0.01 บาท)

receipt_info Object (optional — สำหรับ type=room)

ข้อมูลผู้รับใบเสร็จ สำหรับพิมพ์ในใบเสร็จห้องพัก

Field Type Required Description
receipt_info.is_personal boolean true = บุคคลธรรมดา (default), false = นิติบุคคล
receipt_info.id_card string เลขบัตรประชาชน (กรณี is_personal=true)
receipt_info.tax_number string เลขประจำตัวผู้เสียภาษี (กรณี is_personal=false)
receipt_info.name string ชื่อที่ออกใบเสร็จ
receipt_info.phone string เบอร์โทรในใบเสร็จ

data Array (optional ทุก type)

สำหรับ type=room — ใช้สำหรับ ค่าใช้จ่ายเพิ่มเติม เท่านั้น เช่น ค่าน้ำ ค่าไฟ (ค่าเช่าหลักอยู่ใน room.unit_price)
สำหรับ type=content / type=health_science — ถ้าไม่ส่ง data ระบบจะใช้ชื่อโครงการเป็นรายการเดียวในใบเสร็จ
title ใช้แสดงเป็นหัวข้อหลักเหนือรายการย่อย (required เมื่อส่ง data สำหรับ type!=room)

Field Type Required Description
data[].name string ชื่อรายการ เช่น "ค่าน้ำ", "ค่าไฟ"
data[].quantity integer จำนวน (≥ 1)
data[].unit_price number ราคาต่อหน่วย (บาท)
data[].description string รายละเอียดเพิ่มเติม

type=content/health_science: sum(quantity × unit_price) ของ data ต้องตรงกับ amount (tolerance ±0.01 บาท)

ตัวอย่าง Request — type=content

POST /api/v1/payment/create
Authorization: Bearer eyJ0eXAiOiJKV1Qi...
Content-Type: application/json
X-Timestamp: 1741852800
X-Nonce: k9xM2pLnQr7vYwZ3
X-Signature: base64encodedHmacSignature==

{
  "project_code": "37000001",
  "type": "content",
  "amount": 1700.00,
  "payer_name": "สมชาย ใจดี",
  "payer_email": "somchai@example.com",
  "payer_phone": "0891234567",
  "ref_no": "YOUR-SYS-REF-001",
  "title": "อบรม Python Basics รุ่นที่ 3",
  "data": [
    {
      "name": "ค่าลงทะเบียน",
      "quantity": 1,
      "unit_price": 1500.00,
      "description": "หลักสูตรสำหรับผู้เริ่มต้น"
    },
    {
      "name": "ค่าเอกสารประกอบ",
      "quantity": 2,
      "unit_price": 100.00
    }
  ]
}

ตัวอย่าง Request — type=room

POST /api/v1/payment/create
Authorization: Bearer eyJ0eXAiOiJKV1Qi...
Content-Type: application/json
X-Timestamp: 1741852800
X-Nonce: k9xM2pLnQr7vYwZ3
X-Signature: base64encodedHmacSignature==

{
  "mode": "production",
  "project_code": "0104001",
  "type": "room",
  "ref_no": "YOUR-SYS-REF-002",
  "callback_url": "https://your-system.example.com/callback",
  "room": {
    "building_id": 1,
    "floor": 3,
    "room_id": 42,
    "booking_type": "monthly",
    "start_date": "2026-06-01",
    "end_date": "2026-06-30",
    "unit_price": 3000.00
  },
  "receipt_info": {
    "is_personal": true,
    "id_card": "1100100100001",
    "name": "นายสมชาย ใจดี",
    "phone": "0891234567"
  },
  "title": "ค่าเช่าห้องพัก เดือนมิถุนายน 2569",
  "data": [
    {
      "name": "ค่าน้ำ",
      "quantity": 1,
      "unit_price": 200.00
    },
    {
      "name": "ค่าไฟ",
      "quantity": 1,
      "unit_price": 350.00
    }
  ]
}

หมายเหตุ: ตัวอย่างนี้ไม่ส่ง amount — ระบบคำนวณให้อัตโนมัติ: 3000 + 200 + 350 = 3550 บาท payer_name, payer_email, payer_phone เป็น optional เมื่อ type=room

Response 201:

{
  "status": "success",
  "payment_url": "https://ereceipt.ofas.chula.ac.th/project/payment/aB3xK9mP...",
  "booking_ref": "BK-690001",
  "expires_at": "2026-03-13T16:30:00+07:00"
}
Field Description
mode โหมดที่ใช้งาน (test หรือ production)
payment_url URL สำหรับ redirect user ไปชำระเงิน
booking_ref เลขอ้างอิง booking ของระบบ CU
expires_at Payment URL หมดอายุ (30 นาที)

การใช้งาน: Redirect user ไปที่ payment_url ทันที User จะเห็นหน้าชำระเงินพร้อมข้อมูล pre-filled


5. การรับ Webhook Callback

หลังจาก user ชำระเงินสำเร็จ ระบบจะ POST ไปที่ callback_url ของคุณ

Webhook Payload

POST https://your-system.example.com/payment/callback
Content-Type: application/json
X-Timestamp: 1741852900
X-Signature: a3f8c2d1e4b7...   (hex string — ดู Section 7)

{
  "event": "payment.success",
  "booking_ref": "BK-690001",
  "ref_no": "YOUR-SYS-REF-001",
  "receipt_no": "CU37690001",
  "amount": 1700.00,
  "paid_at": "2026-03-13T15:45:00+07:00",
  "receipt_url": "https://ereceipt.ofas.chula.ac.th/project/payment/success/aB3xK9..."
}
Field Description
event "payment.success" เสมอ
booking_ref เลข booking ของระบบ CU
ref_no เลขอ้างอิงที่คุณส่งมาตอน create session — ใช้ track payment ใน system ของคุณ
receipt_no เลขใบเสร็จ เช่น CU37690001
amount จำนวนเงิน (บาท)
paid_at เวลาชำระ (ISO 8601, Asia/Bangkok)
receipt_url URL สำหรับดูใบเสร็จ (HTML + print)

การ track ระหว่าง 2 ระบบ: ใช้ ref_no ที่คุณส่งมาตอน create session เพื่อ map กับ order ใน system ของคุณ

Response ที่ Server คุณต้องตอบกลับ

HTTP 200 OK

ถ้า server ตอบ non-2xx ระบบจะ retry อัตโนมัติ: 1 นาที → 5 นาที → 30 นาที (รวม 4 ครั้ง)


6. HMAC Signature — Inbound (คำนวณและส่งมาให้เรา)

ทุก request ไปที่ /api/v1/* ต้องมี X-Signature

วิธีคำนวณ

message   = client_id + "\n" + timestamp + "\n" + nonce + "\n" + sha256(body)
signature = base64( HMAC-SHA256(message, client_secret) )

หมายเหตุ:

  • client_id — UUID ที่ได้รับทาง email (lowercase)
  • client_secret — plain text secret ที่ได้รับทาง email (ไม่ใช่ค่า bcrypt)
  • timestamp — ค่าเดียวกับ X-Timestamp header
  • nonce — ค่าเดียวกับ X-Nonce header
  • sha256(body) — SHA-256 hash ของ raw request body (hex string, lowercase)
  • ส่ง body {} (empty JSON object) หรือ "" (empty string) → sha256 ค่าต่างกัน ให้ใช้ค่าจริงที่ส่งไป
  • \n — newline character (LF, 0x0A)

ตัวอย่าง (Python)

import hashlib
import hmac
import base64
import json
import time
import uuid
import requests

client_id     = "550e8400-e29b-41d4-a716-446655440000"
client_secret = "AbCd1234EfGh5678..."  # plain text จาก email

def create_signature(body_str: str, timestamp: int, nonce: str) -> str:
    """body_str = json.dumps(payload) สำหรับ POST หรือ '' สำหรับ GET"""
    body_hash = hashlib.sha256(body_str.encode('utf-8')).hexdigest()
    message = "\n".join([client_id, str(timestamp), nonce, body_hash])
    sig = hmac.new(
        client_secret.encode('utf-8'),
        message.encode('utf-8'),
        hashlib.sha256
    ).digest()
    return base64.b64encode(sig).decode('utf-8')

# ─── Create Payment Session ───────────────────────────────────────────────────
payload = {
    "project_code": "37000001",
    "type": "content",
    "amount": 1700.00,
    "payer_name": "สมชาย ใจดี",
    "payer_email": "somchai@example.com",
    "ref_no": "YOUR-SYS-REF-001",
    "title": "อบรม Python Basics รุ่นที่ 3",
    "data": [
        {"name": "ค่าลงทะเบียน", "quantity": 1, "unit_price": 1500.00},
        {"name": "ค่าเอกสารประกอบ", "quantity": 2, "unit_price": 100.00}
    ]
}

timestamp = int(time.time())
nonce     = uuid.uuid4().hex[:16]
body_str  = json.dumps(payload, separators=(',', ':'), ensure_ascii=False)
signature = create_signature(body_str, timestamp, nonce)

response = requests.post(
    "https://ereceipt.ofas.chula.ac.th/api/v1/payment/create",
    data=body_str.encode('utf-8'),
    headers={
        "Authorization": f"Bearer {access_token}",
        "Content-Type":  "application/json",
        "X-Timestamp":   str(timestamp),
        "X-Nonce":       nonce,
        "X-Signature":   signature,
    }
)
print(response.json())

ตัวอย่าง (PHP)

<?php
$clientId     = '550e8400-e29b-41d4-a716-446655440000';
$clientSecret = 'AbCd1234EfGh5678...'; // plain text จาก email

/**
 * $bodyJson = json_encode($payload) สำหรับ POST หรือ '' สำหรับ GET
 */
function createSignature(string $bodyJson, int $timestamp, string $nonce, string $clientId, string $clientSecret): string
{
    $bodyHash = hash('sha256', $bodyJson);
    $message  = implode("\n", [$clientId, $timestamp, $nonce, $bodyHash]);
    $rawHmac  = hash_hmac('sha256', $message, $clientSecret, true);
    return base64_encode($rawHmac);
}

$payload = [
    'project_code' => '37000001',
    'type'         => 'content',
    'amount'       => 1700.00,
    'payer_name'   => 'สมชาย ใจดี',
    'payer_email'  => 'somchai@example.com',
    'ref_no'       => 'YOUR-SYS-REF-001',
    'title'        => 'อบรม Python Basics รุ่นที่ 3',
    'data'         => [
        ['name' => 'ค่าลงทะเบียน',    'quantity' => 1, 'unit_price' => 1500.00],
        ['name' => 'ค่าเอกสารประกอบ', 'quantity' => 2, 'unit_price' => 100.00],
    ],
];

$timestamp = time();
$nonce     = bin2hex(random_bytes(8)); // 16 hex chars
$bodyJson  = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$signature = createSignature($bodyJson, $timestamp, $nonce, $clientId, $clientSecret);

$ch = curl_init('https://ereceipt.ofas.chula.ac.th/api/v1/payment/create');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => $bodyJson,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $accessToken,
        'Content-Type: application/json',
        'X-Timestamp: ' . $timestamp,
        'X-Nonce: '     . $nonce,
        'X-Signature: ' . $signature,
    ],
]);
$response = json_decode(curl_exec($ch), true);
curl_close($ch);
print_r($response);

ตัวอย่าง (Node.js)

const crypto = require('crypto');
const axios  = require('axios');

const clientId     = '550e8400-e29b-41d4-a716-446655440000';
const clientSecret = 'AbCd1234EfGh5678...'; // plain text จาก email

/**
 * bodyStr = JSON.stringify(payload) สำหรับ POST หรือ '' สำหรับ GET
 */
function createSignature(bodyStr, timestamp, nonce) {
  const bodyHash = crypto.createHash('sha256').update(bodyStr, 'utf8').digest('hex');
  const message  = [clientId, timestamp, nonce, bodyHash].join('\n');
  const hmac     = crypto.createHmac('sha256', clientSecret).update(message, 'utf8').digest();
  return Buffer.from(hmac).toString('base64');
}

const payload = {
  project_code: '37000001',
  type:         'content',
  amount:       1700.00,
  payer_name:   'สมชาย ใจดี',
  payer_email:  'somchai@example.com',
  ref_no:       'YOUR-SYS-REF-001',
  title:        'อบรม Python Basics รุ่นที่ 3',
  data: [
    { name: 'ค่าลงทะเบียน',    quantity: 1, unit_price: 1500.00 },
    { name: 'ค่าเอกสารประกอบ', quantity: 2, unit_price: 100.00 },
  ],
};

const timestamp = Math.floor(Date.now() / 1000);
const nonce     = crypto.randomBytes(8).toString('hex');
const bodyStr   = JSON.stringify(payload);
const signature = createSignature(bodyStr, timestamp, nonce);

const response = await axios.post(
  'https://ereceipt.ofas.chula.ac.th/api/v1/payment/create',
  bodyStr,
  {
    headers: {
      'Authorization': `Bearer ${accessToken}`,
      'Content-Type':  'application/json',
      'X-Timestamp':   String(timestamp),
      'X-Nonce':       nonce,
      'X-Signature':   signature,
    },
  }
);
console.log(response.data);

7. HMAC Signature — Outbound (verify callback ที่รับจากเรา)

เมื่อระบบส่ง Webhook มาที่ callback_url ของคุณ คุณต้อง verify signature เพื่อยืนยันว่า request มาจากระบบจริง

วิธี Verify

signature = hex( HMAC-SHA256(timestamp + "." + body, webhook_secret) )

หมายเหตุ:

  • webhook_secret — รับทาง email ตอนได้รับ credentials
  • timestamp — มาจาก X-Timestamp header ของ webhook request
  • body — raw JSON string ของ webhook payload
  • ผลลัพธ์เป็น hex string (ไม่ใช่ base64)

ตัวอย่าง (Python — Flask)

import hmac
import hashlib
from flask import Flask, request, jsonify

app = Flask(__name__)
WEBHOOK_SECRET = "xK9mP2..."  # จาก email

@app.route('/payment/callback', methods=['POST'])
def payment_callback():
    timestamp       = request.headers.get('X-Timestamp', '')
    received_sig    = request.headers.get('X-Signature', '')
    body            = request.get_data()  # raw bytes

    message         = f"{timestamp}.{body.decode('utf-8')}"
    expected_sig    = hmac.new(
        WEBHOOK_SECRET.encode('utf-8'),
        message.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(expected_sig, received_sig):
        return jsonify({'error': 'Invalid signature'}), 401

    data = request.get_json()
    if data.get('event') == 'payment.success':
        ref_no     = data['ref_no']      # เลขอ้างอิงของคุณ
        receipt_no = data['receipt_no']
        amount     = data['amount']
        # อัปเดต order ใน system ของคุณโดยใช้ ref_no...

    return jsonify({'status': 'ok'}), 200

ตัวอย่าง (PHP)

<?php
$webhookSecret = 'xK9mP2...'; // จาก email

$timestamp    = $_SERVER['HTTP_X_TIMESTAMP'] ?? '';
$receivedSig  = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
$body         = file_get_contents('php://input');

$message      = $timestamp . '.' . $body;
$expectedSig  = hash_hmac('sha256', $message, $webhookSecret);

if (!hash_equals($expectedSig, $receivedSig)) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}

$data = json_decode($body, true);
if ($data['event'] === 'payment.success') {
    $refNo     = $data['ref_no'];      // เลขอ้างอิงของคุณ
    $receiptNo = $data['receipt_no'];
    $amount    = $data['amount'];
    // อัปเดต order ใน system ของคุณโดยใช้ $refNo...
}

http_response_code(200);
echo json_encode(['status' => 'ok']);

ตัวอย่าง (Node.js — Express)

const crypto = require('crypto');
const express = require('express');
const app = express();

const WEBHOOK_SECRET = 'xK9mP2...'; // จาก email

app.post('/payment/callback', express.raw({ type: 'application/json' }), (req, res) => {
  const timestamp    = req.headers['x-timestamp'] || '';
  const receivedSig  = req.headers['x-signature'] || '';
  const body         = req.body.toString('utf-8');

  const message      = `${timestamp}.${body}`;
  const expectedSig  = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(message, 'utf-8')
    .digest('hex');

  if (!crypto.timingSafeEqual(
    Buffer.from(expectedSig, 'utf-8'),
    Buffer.from(receivedSig, 'utf-8')
  )) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const data = JSON.parse(body);
  if (data.event === 'payment.success') {
    const { ref_no, receipt_no, amount } = data;
    // อัปเดต order ใน system ของคุณโดยใช้ ref_no...
  }

  res.status(200).json({ status: 'ok' });
});

8. Error Codes

HTTP Status Codes

Status ความหมาย
200 สำเร็จ
201 สร้างสำเร็จ
401 Unauthorized — token ไม่ถูกต้อง / signature ไม่ตรง / timestamp เกิน ±5 นาที / nonce ซ้ำ
403 Forbidden — Gateway ถูกระงับหรือยังไม่ผ่านการอนุมัติ
404 ไม่พบ resource ที่ขอ
422 ข้อมูลไม่ถูกต้อง — ดูรายละเอียดใน errors field
429 Too Many Requests — Rate limit เกิน (30 req/นาที)
500 เกิดข้อผิดพลาดระบบ

Error Response Format

{
  "status": "error",
  "message": "ข้อผิดพลาดหลัก",
  "errors": {
    "field_name": ["รายละเอียดข้อผิดพลาด"]
  }
}

ข้อผิดพลาดที่พบบ่อย

Message สาเหตุ วิธีแก้
Missing required security headers ขาด X-Timestamp, X-Nonce, หรือ X-Signature ตรวจสอบ headers ครบหรือไม่
Request timestamp is too old เวลาเซิร์ฟเวอร์ต่างกันเกิน 5 นาที sync NTP ให้เวลาตรง
Nonce has already been used ส่ง nonce ซ้ำ ใช้ nonce ใหม่ทุก request
Invalid signature คำนวณ HMAC ผิด ตรวจสอบลำดับ message, client_secret, UUID lowercase
X-Nonce must be 16-64 characters nonce สั้น/ยาวเกิน ใช้ 16–64 chars
กรุณาระบุรหัสโครงการ ไม่ส่ง project_code ส่ง project_code ที่ได้จาก POST /api/v1/projects
กรุณาระบุประเภทโครงการ ไม่ส่ง type ส่ง type ที่ได้จาก POST /api/v1/projects (content, health_science, หรือ room)
กรุณาระบุ callback_url เมื่อใช้ mode=production ส่ง mode=production แต่ไม่มี callback_url ใส่ callback_url ที่เป็น HTTPS endpoint ของคุณ
กรุณาระบุ title เมื่อส่ง data ส่ง data แต่ไม่มี title ใส่ title เป็นชื่อหัวข้อรายการ
ผลรวมรายการ data ไม่ตรงกับ amount sum ของ data ≠ amount ตรวจสอบ quantity × unit_price ทุก item

9. ตัวอย่างโค้ด Full Flow

หลักการ: ขอ Access Token ใหม่ทุกครั้งก่อนเรียก API — ไม่ต้อง cache Flow: ขอ Token → ดึงรายการโครงการ → สร้าง Payment Session → Redirect user


Full Flow — Python

import hashlib
import hmac
import base64
import json
import time
import uuid
import requests

BASE_URL      = "https://ereceipt.ofas.chula.ac.th"
CLIENT_ID     = "550e8400-e29b-41d4-a716-446655440000"
CLIENT_SECRET = "AbCd1234..."  # plain text จาก email


def _get_access_token() -> str:
    resp = requests.post(
        f"{BASE_URL}/oauth/token",
        json={
            "grant_type":    "client_credentials",
            "client_id":     CLIENT_ID,
            "client_secret": CLIENT_SECRET,
            "scope":         "payment:create",
        },
        timeout=10,
    )
    resp.raise_for_status()
    return resp.json()["access_token"]


def _sign(body_str: str, timestamp: int, nonce: str) -> str:
    """body_str = json.dumps(payload) สำหรับ POST หรือ '' สำหรับ GET"""
    body_hash = hashlib.sha256(body_str.encode("utf-8")).hexdigest()
    message   = "\n".join([CLIENT_ID, str(timestamp), nonce, body_hash])
    raw_hmac  = hmac.new(
        CLIENT_SECRET.encode("utf-8"),
        message.encode("utf-8"),
        hashlib.sha256,
    ).digest()
    return base64.b64encode(raw_hmac).decode("utf-8")


def list_projects(access_token: str, search: str = "", type_filter: str = "") -> list:
    """ดึงรายการโครงการ — POST body (filter ถูก sign ด้วย HMAC)"""
    payload = {}
    if search:      payload["search"] = search
    if type_filter: payload["type"]   = type_filter

    timestamp = int(time.time())
    nonce     = uuid.uuid4().hex[:16]
    body_str  = json.dumps(payload, separators=(",", ":"), ensure_ascii=False)
    signature = _sign(body_str, timestamp, nonce)

    resp = requests.post(
        f"{BASE_URL}/api/v1/projects",
        data=body_str.encode("utf-8"),
        headers={
            "Authorization": f"Bearer {access_token}",
            "Content-Type":  "application/json",
            "X-Timestamp":   str(timestamp),
            "X-Nonce":       nonce,
            "X-Signature":   signature,
        },
        timeout=10,
    )
    resp.raise_for_status()
    return resp.json()["data"]


def create_payment_session(
    access_token: str,
    project_code: str,
    project_type: str,
    amount: float,
    ref_no: str,
    payer_name: str = "",
    payer_email: str = "",
    payer_phone: str = "",
    data: dict = None,
) -> dict:
    payload = {"project_code": project_code, "type": project_type, "amount": amount, "ref_no": ref_no}
    if payer_name:  payload["payer_name"]  = payer_name
    if payer_email: payload["payer_email"] = payer_email
    if payer_phone: payload["payer_phone"] = payer_phone
    if data:        payload["data"]        = data

    timestamp = int(time.time())
    nonce     = uuid.uuid4().hex[:16]
    body_str  = json.dumps(payload, separators=(",", ":"), ensure_ascii=False)
    signature = _sign(body_str, timestamp, nonce)

    resp = requests.post(
        f"{BASE_URL}/api/v1/payment/create",
        data=body_str.encode("utf-8"),
        headers={
            "Authorization": f"Bearer {access_token}",
            "Content-Type":  "application/json",
            "X-Timestamp":   str(timestamp),
            "X-Nonce":       nonce,
            "X-Signature":   signature,
        },
        timeout=15,
    )
    resp.raise_for_status()
    return resp.json()


# ─── ตัวอย่างการใช้งาน ─────────────────────────────────────────────────────
access_token = _get_access_token()

# Step 1.5: ดึงรายการโครงการ
projects = list_projects(access_token, type_filter="content")
project  = next(p for p in projects if p["project_name"] == "อบรม Python Basics รุ่นที่ 3")
project_code = project["project_code"]  # เช่น "37000001"
project_type = project["type"]          # เช่น "content"

# Step 2: สร้าง Payment Session
result = create_payment_session(
    access_token = access_token,
    project_code = project_code,
    project_type = project_type,
    amount       = 1700.00,
    ref_no       = "ORDER-001",
    payer_name   = "สมชาย ใจดี",
    payer_email  = "somchai@example.com",
    payer_phone  = "0891234567",
    title        = "อบรม Python Basics รุ่นที่ 3",
    data         = [
        {"name": "ค่าลงทะเบียน",    "quantity": 1, "unit_price": 1500.00},
        {"name": "ค่าเอกสารประกอบ", "quantity": 2, "unit_price": 100.00},
    ],
)

print("Payment URL:", result["payment_url"])
print("Booking Ref:", result["booking_ref"])
# → Redirect user ไปที่ result["payment_url"]

Full Flow — PHP

<?php

const BASE_URL      = 'https://ereceipt.ofas.chula.ac.th';
const CLIENT_ID     = '550e8400-e29b-41d4-a716-446655440000';
const CLIENT_SECRET = 'AbCd1234...'; // plain text จาก email

function getAccessToken(): string
{
    $ch = curl_init(BASE_URL . '/oauth/token');
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => json_encode([
            'grant_type'    => 'client_credentials',
            'client_id'     => CLIENT_ID,
            'client_secret' => CLIENT_SECRET,
            'scope'         => 'payment:create',
        ]),
        CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
        CURLOPT_TIMEOUT    => 10,
    ]);
    $body = curl_exec($ch);
    curl_close($ch);
    $data = json_decode($body, true);
    if (empty($data['access_token'])) {
        throw new \RuntimeException('Failed to get access token');
    }
    return $data['access_token'];
}

/**
 * $bodyJson = json_encode($payload) สำหรับ POST หรือ '' สำหรับ GET
 */
function createSignature(string $bodyJson, int $timestamp, string $nonce): string
{
    $bodyHash = hash('sha256', $bodyJson);
    $message  = implode("\n", [CLIENT_ID, $timestamp, $nonce, $bodyHash]);
    $rawHmac  = hash_hmac('sha256', $message, CLIENT_SECRET, true);
    return base64_encode($rawHmac);
}

function listProjects(string $accessToken, string $typeFilter = ''): array
{
    $payload   = $typeFilter ? ['type' => $typeFilter] : [];
    $bodyJson  = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    $timestamp = time();
    $nonce     = bin2hex(random_bytes(8));
    $signature = createSignature($bodyJson, $timestamp, $nonce);

    $ch = curl_init(BASE_URL . '/api/v1/projects');
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => $bodyJson,
        CURLOPT_HTTPHEADER     => [
            'Authorization: Bearer ' . $accessToken,
            'Content-Type: application/json',
            'X-Timestamp: ' . $timestamp,
            'X-Nonce: '     . $nonce,
            'X-Signature: ' . $signature,
        ],
        CURLOPT_TIMEOUT => 10,
    ]);
    $body = curl_exec($ch);
    curl_close($ch);
    return json_decode($body, true)['data'] ?? [];
}

function createPaymentSession(
    string $accessToken,
    string $projectCode,
    string $projectType,
    float  $amount,
    string $refNo,
    string $payerName  = '',
    string $payerEmail = '',
    string $payerPhone = '',
    ?array $data       = null
): array {
    $payload = ['project_code' => $projectCode, 'type' => $projectType, 'amount' => $amount, 'ref_no' => $refNo];
    if ($payerName)  $payload['payer_name']  = $payerName;
    if ($payerEmail) $payload['payer_email'] = $payerEmail;
    if ($payerPhone) $payload['payer_phone'] = $payerPhone;
    if ($data)       $payload['data']        = $data;

    $timestamp = time();
    $nonce     = bin2hex(random_bytes(8));
    $bodyJson  = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    $signature = createSignature($bodyJson, $timestamp, $nonce);

    $ch = curl_init(BASE_URL . '/api/v1/payment/create');
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => $bodyJson,
        CURLOPT_HTTPHEADER     => [
            'Authorization: Bearer ' . $accessToken,
            'Content-Type: application/json',
            'X-Timestamp: '  . $timestamp,
            'X-Nonce: '      . $nonce,
            'X-Signature: '  . $signature,
        ],
        CURLOPT_TIMEOUT => 15,
    ]);
    $body     = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    $result = json_decode($body, true);
    if ($httpCode !== 201 || ($result['status'] ?? '') !== 'success') {
        throw new \RuntimeException('Payment session failed: ' . $body);
    }
    return $result;
}

// ─── ตัวอย่างการใช้งาน ─────────────────────────────────────────────────────
$accessToken = getAccessToken();

// Step 1.5: ดึงรายการโครงการ
$projects    = listProjects($accessToken, 'content');
$projectCode = $projects[0]['project_code'] ?? '37000001'; // เลือกโครงการที่ต้องการ
$projectType = $projects[0]['type']         ?? 'content';

// Step 2: สร้าง Payment Session
$result = createPaymentSession(
    accessToken: $accessToken,
    projectCode: $projectCode,
    projectType: $projectType,
    amount:      1700.00,
    refNo:       'ORDER-001',
    payerName:   'สมชาย ใจดี',
    payerEmail:  'somchai@example.com',
    payerPhone:  '0891234567',
    title: 'อบรม Python Basics รุ่นที่ 3',
    data: [
        ['name' => 'ค่าลงทะเบียน',    'quantity' => 1, 'unit_price' => 1500.00],
        ['name' => 'ค่าเอกสารประกอบ', 'quantity' => 2, 'unit_price' => 100.00],
    ]
);

echo 'Payment URL: ' . $result['payment_url'] . PHP_EOL;
// header('Location: ' . $result['payment_url']);

Full Flow — Node.js

const crypto  = require('crypto');
const https   = require('https');

const BASE_URL      = 'https://ereceipt.ofas.chula.ac.th';
const CLIENT_ID     = '550e8400-e29b-41d4-a716-446655440000';
const CLIENT_SECRET = 'AbCd1234...'; // plain text จาก email

function postJson(url, body, headers = {}) {
  return new Promise((resolve, reject) => {
    const bodyStr = JSON.stringify(body);
    const parsed  = new URL(url);
    const options = {
      hostname: parsed.hostname,
      port:     parsed.port || 443,
      path:     parsed.pathname,
      method:   'POST',
      headers:  { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(bodyStr), ...headers },
    };
    const req = https.request(options, (res) => {
      let data = '';
      res.on('data', (chunk) => (data += chunk));
      res.on('end', () => resolve({ status: res.statusCode, data: JSON.parse(data) }));
    });
    req.on('error', reject);
    req.setTimeout(15000, () => req.destroy(new Error('timeout')));
    req.write(bodyStr);
    req.end();
  });
}

async function getAccessToken() {
  const { data } = await postJson(`${BASE_URL}/oauth/token`, {
    grant_type: 'client_credentials', client_id: CLIENT_ID,
    client_secret: CLIENT_SECRET, scope: 'payment:create',
  });
  if (!data.access_token) throw new Error('Token failed');
  return data.access_token;
}

function createSignature(bodyStr, timestamp, nonce) {
  const bodyHash = crypto.createHash('sha256').update(bodyStr, 'utf8').digest('hex');
  const message  = [CLIENT_ID, timestamp, nonce, bodyHash].join('\n');
  const rawHmac  = crypto.createHmac('sha256', CLIENT_SECRET).update(message, 'utf8').digest();
  return Buffer.from(rawHmac).toString('base64');
}

async function listProjects(accessToken, typeFilter = '') {
  const payload   = typeFilter ? { type: typeFilter } : {};
  const bodyStr   = JSON.stringify(payload);
  const timestamp = Math.floor(Date.now() / 1000);
  const nonce     = crypto.randomBytes(8).toString('hex');
  const signature = createSignature(bodyStr, timestamp, nonce);

  const { data } = await postJson(`${BASE_URL}/api/v1/projects`, payload, {
    'Authorization': `Bearer ${accessToken}`,
    'X-Timestamp':   String(timestamp),
    'X-Nonce':       nonce,
    'X-Signature':   signature,
  });
  return data.data || [];
}

async function createPaymentSession({ accessToken, projectCode, projectType, amount, refNo, payerName, payerEmail, payerPhone, data }) {
  const payload = { project_code: projectCode, type: projectType, amount, ref_no: refNo };
  if (payerName)  payload.payer_name  = payerName;
  if (payerEmail) payload.payer_email = payerEmail;
  if (payerPhone) payload.payer_phone = payerPhone;
  if (data)       payload.data        = data;

  const timestamp = Math.floor(Date.now() / 1000);
  const nonce     = crypto.randomBytes(8).toString('hex');
  const bodyStr   = JSON.stringify(payload);
  const signature = createSignature(bodyStr, timestamp, nonce);

  const { status, data: result } = await postJson(
    `${BASE_URL}/api/v1/payment/create`, payload,
    { Authorization: `Bearer ${accessToken}`, 'X-Timestamp': String(timestamp), 'X-Nonce': nonce, 'X-Signature': signature }
  );

  if (status !== 201 || result.status !== 'success') {
    throw new Error(`Failed (${status}): ${JSON.stringify(result)}`);
  }
  return result;
}

// ─── ตัวอย่างการใช้งาน ───────────────────────────────────────────────────────
(async () => {
  const accessToken = await getAccessToken();

  // Step 1.5: ดึงรายการโครงการ
  const projects    = await listProjects(accessToken, 'content');
  const projectCode = projects[0]?.project_code ?? '37000001'; // เลือกโครงการที่ต้องการ
  const projectType = projects[0]?.type         ?? 'content';

  // Step 2: สร้าง Payment Session
  const result = await createPaymentSession({
    accessToken,
    projectCode,
    projectType,
    amount:     1700.00,
    refNo:      'ORDER-001',
    payerName:  'สมชาย ใจดี',
    payerEmail: 'somchai@example.com',
    payerPhone: '0891234567',
    title: 'อบรม Python Basics รุ่นที่ 3',
    data: [
      { name: 'ค่าลงทะเบียน',    quantity: 1, unit_price: 1500.00 },
      { name: 'ค่าเอกสารประกอบ', quantity: 2, unit_price: 100.00 },
    ],
  });
  console.log('Payment URL:', result.payment_url);
  // res.redirect(result.payment_url);
})();

10. Environment & Base URL

Environment Base URL
UAT / Staging https://ereceipt.pay.ofas.chula.ac.th
Production https://ereceipt.ofas.chula.ac.th

Endpoints สรุป

Method Path Auth Description
POST /oauth/token Client Credentials ขอ Access Token
POST /api/v1/projects Bearer + HMAC ดึงรายการโครงการทุกประเภท
POST /api/v1/payment/create Bearer + HMAC สร้าง Payment Session

Rate Limits

Endpoint group Limit
/api/v1/* 30 req/นาที

11. FAQ

Q: mode=test กับ mode=production ต่างกันอย่างไร? A: mode=test (default) — ทดสอบ flow ได้ callback_url เป็น optional ไม่ต้องมี callback server; mode=production — ใช้งานจริง callback_url required ระบบจะ POST webhook ไปที่ callback_url เมื่อชำระเงินสำเร็จ

Q: ได้รับ credentials อย่างไร? A: ลงทะเบียนผ่าน Web UI ของ CU E-Receipt → ผู้ดูแลระบบตรวจสอบและอนุมัติ → ระบบส่ง client_id, client_secret, webhook_secret ทาง email โดยอัตโนมัติ

Q: Access Token หมดอายุต้องทำอย่างไร? A: ขอใหม่ด้วย POST /oauth/token ด้วย credentials เดิมได้เลย ไม่มี refresh token

Q: POST /api/v1/projects คืนโครงการของคณะไหน? A: ระบบ filter โครงการ content ตาม fac_code ของ Gateway Client อัตโนมัติ โครงการ health_science และ room คืนทั้งหมด (ไม่ filter ตามคณะ)

Q: project_code ของโครงการแต่ละประเภทมีรูปแบบอย่างไร? A: content = รหัสที่คณะกำหนด (เช่น 37000001); health_science = integer ID ของโครงการ (เช่น 1, 2); room = รหัสห้องที่กำหนด (เช่น RM690001) — ใช้ค่าที่ได้จาก POST /api/v1/projects เสมอ อย่า hardcode

Q: ต้องส่ง type ใน POST /api/v1/payment/create ด้วยไหม? A: ต้องส่งเสมอtype และ project_code ต้องส่งคู่กัน ใช้ค่า type ที่ได้จาก POST /api/v1/projects ตรงๆ (content, health_science, หรือ room)

Q: data จำเป็นต้องส่งหรือไม่? A: ไม่จำเป็น — data เป็น optional ทุก type สำหรับ type=room ให้ใช้ data สำหรับค่าใช้จ่ายเพิ่มเติมเท่านั้น (เช่น ค่าน้ำ ค่าไฟ) ไม่ใส่ค่าเช่าหลักใน data (ค่าเช่าหลักอยู่ใน room.unit_price)

Q: amount สำหรับ type=room ต้องส่งหรือไม่? A: optional — ระบบคำนวณ amount = room.unit_price + sum(data) อัตโนมัติ ถ้าส่ง amount มาต้องตรงกับค่าที่คำนวณได้ (tolerance ±0.01 บาท) ระบบจะตอบ 422 ถ้าไม่ตรง

Q: data sum ต้องตรงกับ amount เสมอไหม? A: เฉพาะ type=content และ health_science — ถ้าส่ง data, sum(quantity × unit_price) ต้องตรงกับ amount (tolerance ±0.01 บาท) ระบบจะตอบ 422 ถ้าไม่ตรง

Q: type=room ต้องส่ง payer_name, payer_email, payer_phone หรือไม่? A: ไม่บังคับ — สำหรับ type=room fields เหล่านี้เป็น optional ผู้ชำระสามารถกรอกข้อมูลในหน้าชำระเงินได้โดยตรง

Q: ref_no มีข้อจำกัดอะไรบ้าง? A: ref_no เป็น string สูงสุด 55 อักษร และจะถูกส่งกลับมาใน webhook payload — ใช้ track payment ระหว่าง 2 ระบบ

Q: ref_no กับ booking_ref ต่างกันอย่างไร? A: ref_no คือเลขอ้างอิงที่ คุณส่งมา (เช่น เลข order ใน system ของคุณ) — ใช้ track การชำระเงินระหว่าง 2 ระบบ; booking_ref คือเลข booking ที่ ระบบ CU ออกให้ (format: BK-690001)

Q: ถ้า Webhook ล้มเหลวจะเกิดอะไรขึ้น? A: ระบบ retry อัตโนมัติ 3 ครั้ง (1 นาที → 5 นาที → 30 นาที)

Q: ผู้ใช้ไม่ชำระเงินภายใน 30 นาที ทำอย่างไร? A: payment_url หมดอายุ user จะเห็นหน้า "Link หมดอายุ" — สร้าง Payment Session ใหม่ได้

Q: client_secret หายต้องทำอย่างไร? A: ติดต่อ thannaphat.j@zenithcomp.co.th เพื่อ regenerate credentials — ไม่สามารถดึงค่าเดิมได้


เอกสารนี้จัดทำโดยทีมพัฒนา CU E-Receipt — จุฬาลงกรณ์มหาวิทยาลัย หากมีข้อสงสัย ติดต่อ: thannaphat.j@zenithcomp.co.th