ข้ามไปยังเนื้อหา

การยืนยันตัวตนและการเซ็น request

ทุก request ต้องเซ็นด้วย HMAC-SHA256 โดยใช้ secret ของคุณ ไม่มีวิธีข้ามการเซ็น เซิร์ฟเวอร์จะตรวจลายเซ็น เวลา และ nonce ทุกครั้ง

ค่าใช้ทำอะไรเก็บที่ไหน
API keyส่งใน header X-Api-Key เพื่อบอกว่าเป็นบัญชีไหนconfig ของเซิร์ฟเวอร์ (ไม่ใช่ความลับ แต่ไม่ควรเผยแพร่)
Secretใช้เซ็น request และตรวจลายเซ็นของ webhooksecret manager หรือ environment variable บนเซิร์ฟเวอร์เท่านั้น
Headerค่า
X-Api-KeyAPI key ของคุณ
X-Timestampเวลาปัจจุบันเป็น Unix time หน่วยวินาที (จำนวนเต็ม) ต้องต่างจากนาฬิกาเซิร์ฟเวอร์ไม่เกิน ±300 วินาที
X-Nonceสตริงสุ่มใหม่ทุก request ยาว 16-128 ตัว ใช้ได้เฉพาะ A-Z a-z 0-9 _ - ใช้ซ้ำภายใน 10 นาทีไม่ได้
X-Signatureลายเซ็น base64 ตามหัวข้อถัดไป
string_to_sign = METHOD + "\n"
+ PATH_AND_QUERY + "\n"
+ X-Timestamp + "\n"
+ X-Nonce + "\n"
+ hex( SHA-256( raw_body ) )
X-Signature = base64( HMAC-SHA256( secret, string_to_sign ) )
  1. METHOD เป็นตัวพิมพ์ใหญ่ เช่น GET, POST, PATCH, DELETE

  2. PATH_AND_QUERY คือ path ที่ส่งจริงรวม /v1 และ query string เช่น /v1/payins?status=pending&limit=10 ไม่รวมโดเมน ห้ามเรียงหรือ encode query ใหม่หลังจากเซ็นแล้ว

  3. raw_body คือไบต์ของ body ที่ส่งจริง ให้แปลง JSON เป็นสตริงครั้งเดียว เซ็นสตริงนั้น แล้วส่งสตริงเดียวกันนั้น hash เป็น hex ตัวเล็ก

  4. request ที่ไม่มี body (GET, DELETE และ endpoint ยกเลิกรายการ) ให้ hash สตริงว่าง ได้ค่า e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

  5. ต่อทั้ง 5 ส่วนด้วยตัวขึ้นบรรทัด \n (ไม่มี \n ต่อท้าย) แล้วคำนวณ HMAC-SHA256 ด้วย secret และเข้ารหัสผลลัพธ์เป็น base64 (ไม่ใช่ hex)

endpoint ที่รับไฟล์ (POST /v1/payins/{id}/slip และการส่งรูปไปที่ POST /v1/slips/verify) ใช้ multipart/form-data กรณีนี้

  • ค่า hash ของ body คือ hex(SHA-256(ไบต์ของไฟล์)) ของ part ชื่อ file ไม่ใช่ ของ body multipart ทั้งก้อน
  • ส่งได้เพียง part file เท่านั้น part อื่นจะถูกปฏิเสธด้วย parameter_unknown เพราะไม่ได้อยู่ในลายเซ็น
  • ไฟล์ต้องเป็น JPG หรือ PNG ขนาดไม่เกิน 5 MB

หลังลายเซ็นถูกต้อง เซิร์ฟเวอร์ยังตรวจว่า

  • บัญชีพาร์ทเนอร์ต้องมีสถานะ active ไม่เช่นนั้นได้ 403 partner_inactive
  • ถ้าบัญชีเปิดใช้ IP allow-list (ดูได้จาก ipAllowListEnabled ใน GET /v1/account) request ต้องมาจาก IP ที่อนุญาต ไม่เช่นนั้นได้ 403 ip_not_allowed ติดต่อซัพพอร์ตเพื่อเพิ่มหรือแก้ IP
  • nonce ต้องไม่เคยใช้มาก่อน ไม่เช่นนั้นได้ 401 nonce_reused

ตัวอย่างอ่าน API key และ secret จาก environment variable PARTNER_API_KEY และ PARTNER_API_SECRET ฟังก์ชันเซ็นรับ timestamp และ nonce แบบกำหนดเองได้ เพื่อใช้ตรวจกับ test vector

// partner-api.mjs (Node.js 18+, no dependencies)
import crypto from 'node:crypto';
import { readFile } from 'node:fs/promises';
const BASE_URL = 'https://api.example.com';
const API_KEY = process.env.PARTNER_API_KEY;
const API_SECRET = process.env.PARTNER_API_SECRET;
export function signRequest(method, pathAndQuery, body = '', opts = {}) {
const timestamp = opts.timestamp ?? String(Math.floor(Date.now() / 1000));
const nonce = opts.nonce ?? crypto.randomBytes(16).toString('hex');
const bodyHash = crypto.createHash('sha256').update(body).digest('hex');
const stringToSign = [method.toUpperCase(), pathAndQuery, timestamp, nonce, bodyHash].join('\n');
const signature = crypto
.createHmac('sha256', opts.secret ?? API_SECRET)
.update(stringToSign)
.digest('base64');
return {
'X-Api-Key': opts.apiKey ?? API_KEY,
'X-Timestamp': timestamp,
'X-Nonce': nonce,
'X-Signature': signature,
};
}
// JSON requests: serialise once, sign that string, send that string.
export async function api(method, pathAndQuery, payload) {
const body = payload === undefined ? '' : JSON.stringify(payload);
const headers = signRequest(method, pathAndQuery, body);
if (body) headers['Content-Type'] = 'application/json';
const res = await fetch(BASE_URL + pathAndQuery, { method, headers, body: body || undefined });
const json = await res.json();
if (!res.ok) {
const err = new Error(`${json.error.code}: ${json.error.message}`);
Object.assign(err, { status: res.status, ...json.error, requestId: json.requestId });
throw err;
}
return json;
}
// Multipart: sign the SHA-256 of the file bytes, send only the `file` part.
export async function uploadSlip(payinId, filePath, mimeType = 'image/jpeg') {
const path = `/v1/payins/${payinId}/slip`;
const bytes = await readFile(filePath);
const form = new FormData();
form.append('file', new Blob([bytes], { type: mimeType }), 'slip.jpg');
const res = await fetch(BASE_URL + path, {
method: 'POST',
headers: signRequest('POST', path, bytes), // no Content-Type: fetch sets the boundary
body: form,
});
return res.json();
}
// const { data: account } = await api('GET', '/v1/account');

ใช้ค่าด้านล่างตรวจว่าฟังก์ชันเซ็นของคุณถูกต้อง ก่อนเรียก API จริง

  • secret: 3f9c1a7e5b2d4c6e8f0a1b3c5d7e9f11
  • X-Timestamp: 1767225600
RequestX-NonceBodyX-Signature ที่ถูกต้อง
POST /v1/payinsn0nce-0123456789abcdefดูด้านล่างRbN+tURaQJG//uGx4R6FI4eBSwkHuPf1Ou/Vwy5MNKU=
GET /v1/payins?status=pending&limit=10abcdefghijklmnop(ว่าง)dK6H3MXkxAZurxacWPiWtFWkvEfPpGv9pT2HtwZpw5Y=

body ของ request แรก (ไม่มีช่องว่างหรือขึ้นบรรทัด):

{"payerId":"payer_7c0a4b8e2f3d4a1b9c8d7e6f5a4b3c2d","amount":"150.00","currency":"THB"}

SHA-256 ของ body นี้คือ 6fc1ff4ee67aa8ec233a428b3f48ac43436dfcd59d252a3cff673161d828d5e4

import { signRequest } from './partner-api.mjs';
const body = '{"payerId":"payer_7c0a4b8e2f3d4a1b9c8d7e6f5a4b3c2d","amount":"150.00","currency":"THB"}';
const h = signRequest('POST', '/v1/payins', body, {
secret: '3f9c1a7e5b2d4c6e8f0a1b3c5d7e9f11',
timestamp: '1767225600',
nonce: 'n0nce-0123456789abcdef',
});
console.log(h['X-Signature']); // RbN+tURaQJG//uGx4R6FI4eBSwkHuPf1Ou/Vwy5MNKU=
อาการสาเหตุและวิธีแก้
invalid_signature ทุก request ที่มี bodyเซ็นสตริงหนึ่งแต่ส่งอีกสตริง เช่น ไลบรารี HTTP แปลง JSON ให้ใหม่ (axios ที่ส่ง object, requests ที่ใช้ json=) ให้แปลง JSON เองครั้งเดียว แล้วส่งสตริงที่เซ็น
invalid_signature เฉพาะ request ที่มี queryเซ็น path โดยไม่มี query หรือไลบรารี encode query ใหม่ เช่น , กลายเป็น %2C ให้สร้าง URL เต็มก่อน แล้วเซ็น path และ query ตามที่ส่งจริงทุกตัวอักษร
invalid_signature ทุก requestลืม /v1 ใน path, ใส่โดเมนใน path, METHOD เป็นตัวเล็ก, hash body เป็น base64 แทน hex, ลายเซ็นเป็น hex แทน base64, มี \n ต่อท้าย หรือใช้ secret เก่าหลังสร้าง secret ใหม่
invalid_signature ตอนอัปโหลดสลิปhash body multipart ทั้งก้อน ต้อง hash เฉพาะไบต์ของไฟล์
invalid_timestampส่งเป็นมิลลิวินาที (Date.now()) หรือนาฬิกาเซิร์ฟเวอร์คลาดเกิน 5 นาที ให้ใช้วินาทีและเปิด NTP
invalid_noncenonce สั้นกว่า 16 ตัว หรือมีตัวอักษรนอก A-Z a-z 0-9 _ -
nonce_reusedลองส่งซ้ำ (retry) ด้วย header ชุดเดิม ทุกครั้งที่ส่งต้องสร้าง timestamp, nonce และลายเซ็นใหม่
invalid_api_keyคีย์พิมพ์ผิดหรือมีช่องว่าง หรือใช้ test key ตอนที่บัญชีไม่ได้อยู่ในโหมดทดสอบ
parameter_unknown ตอนอัปโหลดส่ง part อื่นนอกจาก file ใน multipart
ip_not_allowedIP ขาออกของเซิร์ฟเวอร์ไม่อยู่ใน allow-list (ระวัง NAT หรือ IP ที่เปลี่ยนตาม deploy)