การยืนยันตัวตนและลายเซ็น
หัวข้อ header ที่ต้องส่ง รูปแบบกุญแจ วิธีสร้างลายเซ็น HMAC-SHA256 กรอบเวลาของ timestamp, IP whitelist และตัวอย่างโค้ด 4 ภาษา
สร้างจาก public-api.md โดยอัตโนมัติเวอร์ชัน 1.2.0ปรับปรุงล่าสุด 2026-09-20
#การยืนยันตัวตน (Authentication)
ทุกคำขอต้องแนบ header ต่อไปนี้:
| Header | จำเป็น | คำอธิบาย |
|---|---|---|
X-API-Key | ใช่ | API key ของร้านค้าคุณ |
X-Signature | ใช่ | ลายเซ็น HMAC-SHA256 ของคำขอ เป็นเลขฐานสิบหกตัวพิมพ์เล็ก |
X-Timestamp | ใช่ | เวลา Unix หน่วย วินาที นับจาก epoch |
Content-Type | เมื่อคำขอมี body | application/json (ใช้ multipart/form-data สำหรับการอัปโหลดสลิป) |
X-Content-SHA256 | เฉพาะอัปโหลดสลิป | SHA-256 ของไบต์ไฟล์ที่อัปโหลด เป็นเลขฐานสิบหกตัวพิมพ์เล็ก |
X-Sandbox-Scenario | ไม่ | เฉพาะร้านค้า sandbox — บังคับผลลัพธ์จำลองที่ต้องการ |
#รูปแบบของคีย์
| คีย์ | รูปแบบของ production | รูปแบบของ sandbox | ความยาว |
|---|---|---|---|
| API key | pk_prod_ + เลขฐานสิบหก 32 ตัว | pk_demo_ + เลขฐานสิบหก 32 ตัว | 40 ตัวอักษร |
| Secret key | sk_prod_ + เลขฐานสิบหก 64 ตัว | sk_demo_ + เลขฐานสิบหก 64 ตัว | 72 ตัวอักษร |
secret key จะถูกแสดงให้คุณเห็นเท่านั้น และไม่เคยถูกส่งไปกับคำขอใด ๆ หากพบคีย์ที่มีรูปแบบต่างไปจากนี้ ให้ถือว่าไม่ถูกต้อง
ช่วงเวลาหน่วงตอนเปลี่ยนคีย์ เกตเวย์แคชข้อมูลคีย์ไว้ 5 นาที ต่อหนึ่ง API key หลังจากสร้างคีย์ใหม่ในแดชบอร์ด ให้เผื่อเวลาสูงสุด 5 นาทีกว่าคีย์เดิมจะหยุดทำงาน และคีย์ใหม่จะเริ่มทำงาน
#การสร้างลายเซ็น
ลายเซ็นคำนวณด้วย HMAC-SHA256 โดยใช้ Secret Key ของคุณ:
Signature = lowercase_hex(HMAC-SHA256(SecretKey, SignatureData))
SignatureData = METHOD|PATH|TIMESTAMP (no request body)
SignatureData = METHOD|PATH|TIMESTAMP|BODY (with a request body)กฎที่ระบบบังคับใช้ตรงตามนี้ทุกตัวอักษร:
METHODคือ HTTP method ตัวพิมพ์ใหญ่ (GET,POST)PATHคือเฉพาะส่วน path ของ URL โดย รวม/v1ไว้ด้วย และ ไม่รวม query string กับชื่อโฮสต์ เช่นสำหรับGET /v1/merchant-reports/transactions?from=2026-09-01ค่าPATHคือ/v1/merchant-reports/transactionsTIMESTAMPคือข้อความเดียวกับที่คุณส่งในX-TimestampBODYคือไบต์ดิบที่คุณส่งจริง ให้แปลงเป็นข้อความครั้งเดียวแล้วส่งไบต์ชุดเดียวกับที่เซ็น หากแปลงใหม่ (ลำดับคีย์ต่างไป ช่องว่างต่างไป หรือรูปแบบตัวเลขต่างไป) ลายเซ็นจะไม่ตรง- เมื่อไม่มี body ให้ ตัด
|ตัวท้ายออกทั้งหมด คำขอแบบGETจึงเซ็นสามส่วน ไม่ใช่สี่ส่วน
ตัวอย่าง (คำขอแบบ POST):
SignatureData = "POST|/v1/payments/deposit|1702400000|{\"amount\":100.00,\"currency\":\"THB\",\"reference_id\":\"ORDER-001\"}"ตัวอย่าง (คำขอแบบ GET):
SignatureData = "GET|/v1/payments/550e8400-e29b-41d4-a716-446655440000|1702400000"ลายเซ็นไม่ครอบคลุม query string ระบบเซ็นเฉพาะส่วน path เท่านั้น สำหรับ endpoint ของรายงานซึ่งรับตัวกรองมาทาง query string ลายเซ็นจึงคุ้มครองตัว endpoint แต่ไม่คุ้มครองค่าพารามิเตอร์
#ช่วงเวลาที่ timestamp ใช้ได้
X-Timestamp ต้องต่างจากเวลาของเซิร์ฟเวอร์ไม่เกิน ±5 นาที (ปรับได้รายการติดตั้งผ่าน
HMAC_TIMESTAMP_WINDOW ค่าที่ใช้จริงคือ 5 นาที) หากอยู่นอกช่วงนี้ คำขอจะถูกปฏิเสธด้วย
401 TIMESTAMP_EXPIRED ควรตั้งเซิร์ฟเวอร์ของคุณให้ซิงก์เวลากับ NTP
#รายการ IP ที่อนุญาต
หากบัญชีร้านค้าของคุณตั้งรายการ IP ที่อนุญาตไว้ ทุกคำขอต้องมาจากที่อยู่ในรายการนั้น
โดยระบุเป็น IP เดี่ยวหรือช่วงแบบ CIDR ก็ได้ คำขอจากที่อยู่อื่นจะถูกปฏิเสธด้วย
403 IP_NOT_WHITELISTED แม้ลายเซ็นจะถูกต้องก็ตาม และหากไม่ได้ตั้งรายการไว้เลย
ระบบจะไม่ตรวจ IP
ระบบอ่าน IP ของผู้เรียกจาก CF-Connecting-IP (ตั้งโดยเกตเวย์ปลายทางของเรา และปลอมผ่านทางนั้นไม่ได้)
หากไม่มีจะใช้ค่าซ้ายสุดของ X-Forwarded-For และสุดท้ายจึงใช้ที่อยู่ของ socket
#ตัวอย่างโค้ด
#Python
import hmac
import hashlib
import time
import json
import requests
api_key = "your_api_key"
secret_key = "your_secret_key"
base_url = "https://api.paymenow.asia" # sandbox: https://sandbox-api.paymenow.asia
def create_signature(method, path, timestamp, body=""):
# path is the full URL path including /v1, WITHOUT the query string
data = f"{method}|{path}|{timestamp}"
if body:
data += f"|{body}"
return hmac.new(
secret_key.encode(),
data.encode(),
hashlib.sha256
).hexdigest()
def make_request(method, path, body=None):
timestamp = str(int(time.time()))
# Serialise ONCE and send exactly the bytes that were signed.
body_str = json.dumps(body) if body else ""
signature = create_signature(method, path, timestamp, body_str)
headers = {
"X-API-Key": api_key,
"X-Signature": signature,
"X-Timestamp": timestamp,
"Content-Type": "application/json"
}
url = base_url + path
if method == "POST":
return requests.post(url, headers=headers, data=body_str)
else:
return requests.get(url, headers=headers)
# Create deposit - note: use full path including /v1
response = make_request("POST", "/v1/payments/deposit", {
"amount": 1000.00,
"currency": "THB",
"reference_id": "ORD888888888",
"bank_code": "004",
"bank_account_no": "1234567890",
"bank_account_name": "test test",
"webhook_url": "https://example.com/path/to/your/webhook"
})
print(response.json())สังเกตว่าโค้ดด้านบนใช้
data=body_strไม่ใช่json=bodyเพราะrequestsจะแปลง dict เป็นข้อความใหม่เอง และอาจได้ไบต์ต่างจากชุดที่เซ็นไว้
#Node.js
const crypto = require("crypto");
const axios = require("axios");
const apiKey = "your_api_key";
const secretKey = "your_secret_key";
const baseUrl = "https://api.paymenow.asia"; // sandbox: https://sandbox-api.paymenow.asia
function createSignature(method, path, timestamp, body = "") {
// path is the full URL path including /v1, WITHOUT the query string
let data = `${method}|${path}|${timestamp}`;
if (body) {
data += `|${body}`;
}
return crypto.createHmac("sha256", secretKey).update(data).digest("hex");
}
async function makeRequest(method, path, body = null) {
const timestamp = Math.floor(Date.now() / 1000).toString();
// Serialise ONCE and send exactly the bytes that were signed.
const bodyStr = body ? JSON.stringify(body) : "";
const signature = createSignature(method, path, timestamp, bodyStr);
const headers = {
"X-API-Key": apiKey,
"X-Signature": signature,
"X-Timestamp": timestamp,
"Content-Type": "application/json",
};
const url = baseUrl + path;
if (method === "POST") {
return axios.post(url, bodyStr, { headers });
} else {
return axios.get(url, { headers });
}
}
// Create deposit - note: use full path including /v1
makeRequest("POST", "/v1/payments/deposit", {
amount: 1000.0,
currency: "THB",
reference_id: "ORD888888888",
bank_code: "004",
bank_account_no: "1234567890",
bank_account_name: "test test",
webhook_url: "https://example.com/path/to/your/webhook",
}).then((res) => console.log(res.data));#PHP
<?php
$apiKey = 'your_api_key';
$secretKey = 'your_secret_key';
$baseUrl = 'https://api.paymenow.asia'; // sandbox: https://sandbox-api.paymenow.asia
function createSignature($method, $path, $timestamp, $body = '') {
global $secretKey;
// path is the full URL path including /v1, WITHOUT the query string
$data = "$method|$path|$timestamp";
if ($body) {
$data .= "|$body";
}
return hash_hmac('sha256', $data, $secretKey);
}
function makeRequest($method, $path, $body = null) {
global $apiKey, $baseUrl;
$timestamp = time();
// Serialise ONCE and send exactly the bytes that were signed.
$bodyStr = $body ? json_encode($body) : '';
$signature = createSignature($method, $path, $timestamp, $bodyStr);
$headers = [
'X-API-Key: ' . $apiKey,
'X-Signature: ' . $signature,
'X-Timestamp: ' . $timestamp,
'Content-Type: application/json'
];
$ch = curl_init($baseUrl . $path);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
if ($method === 'POST') {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $bodyStr);
}
$response = curl_exec($ch);
curl_close($ch);
return json_decode($response, true);
}
// Create deposit - note: use full path including /v1
$response = makeRequest('POST', '/v1/payments/deposit', [
'amount' => 1000.00,
'currency' => 'THB',
'reference_id' => 'ORD888888888',
'bank_code' => '004',
'bank_account_no' => '1234567890',
'bank_account_name' => 'test test',
'webhook_url' => 'https://example.com/path/to/your/webhook'
]);
print_r($response);#Go
package main
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"strconv"
"time"
)
const (
apiKey = "your_api_key"
secretKey = "your_secret_key"
baseURL = "https://api.paymenow.asia" // sandbox: https://sandbox-api.paymenow.asia
)
func createSignature(method, path, timestamp, body string) string {
// path is the full URL path including /v1, WITHOUT the query string
data := fmt.Sprintf("%s|%s|%s", method, path, timestamp)
if body != "" {
data += "|" + body
}
h := hmac.New(sha256.New, []byte(secretKey))
h.Write([]byte(data))
return hex.EncodeToString(h.Sum(nil))
}
func makeRequest(method, path string, body interface{}) (map[string]interface{}, error) {
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
var bodyStr string
var reqBody io.Reader
if body != nil {
// Marshal ONCE and send exactly the bytes that were signed.
bodyBytes, _ := json.Marshal(body)
bodyStr = string(bodyBytes)
reqBody = bytes.NewBuffer(bodyBytes)
}
signature := createSignature(method, path, timestamp, bodyStr)
req, _ := http.NewRequest(method, baseURL+path, reqBody)
req.Header.Set("X-API-Key", apiKey)
req.Header.Set("X-Signature", signature)
req.Header.Set("X-Timestamp", timestamp)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
return result, nil
}
func main() {
// Create deposit - note: use full path including /v1
result, _ := makeRequest("POST", "/v1/payments/deposit", map[string]interface{}{
"amount": 1000.00,
"currency": "THB",
"reference_id": "ORD888888888",
"bank_code": "004",
"bank_account_no": "1234567890",
"bank_account_name": "test test",
"webhook_url": "https://example.com/path/to/your/webhook",
})
fmt.Printf("%+v\n", result)
}หน้านี้แปลจากเอกสารต้นฉบับภาษาอังกฤษ ส่วนที่เป็นค่าทางเทคนิค — path, ชื่อฟิลด์, header, รหัสข้อผิดพลาด และชื่อคอลัมน์ในไฟล์ส่งออก — คงไว้เป็นภาษาอังกฤษตามที่ระบบใช้จริง

