PayMeNow

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

หัวข้อ 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เมื่อคำขอมี bodyapplication/json (ใช้ multipart/form-data สำหรับการอัปโหลดสลิป)
X-Content-SHA256เฉพาะอัปโหลดสลิปSHA-256 ของไบต์ไฟล์ที่อัปโหลด เป็นเลขฐานสิบหกตัวพิมพ์เล็ก
X-Sandbox-Scenarioไม่เฉพาะร้านค้า sandbox — บังคับผลลัพธ์จำลองที่ต้องการ

#รูปแบบของคีย์

คีย์รูปแบบของ productionรูปแบบของ sandboxความยาว
API keypk_prod_ + เลขฐานสิบหก 32 ตัวpk_demo_ + เลขฐานสิบหก 32 ตัว40 ตัวอักษร
Secret keysk_prod_ + เลขฐานสิบหก 64 ตัวsk_demo_ + เลขฐานสิบหก 64 ตัว72 ตัวอักษร

secret key จะถูกแสดงให้คุณเห็นเท่านั้น และไม่เคยถูกส่งไปกับคำขอใด ๆ หากพบคีย์ที่มีรูปแบบต่างไปจากนี้ ให้ถือว่าไม่ถูกต้อง

ช่วงเวลาหน่วงตอนเปลี่ยนคีย์ เกตเวย์แคชข้อมูลคีย์ไว้ 5 นาที ต่อหนึ่ง API key หลังจากสร้างคีย์ใหม่ในแดชบอร์ด ให้เผื่อเวลาสูงสุด 5 นาทีกว่าคีย์เดิมจะหยุดทำงาน และคีย์ใหม่จะเริ่มทำงาน

#การสร้างลายเซ็น

ลายเซ็นคำนวณด้วย HMAC-SHA256 โดยใช้ Secret Key ของคุณ:

text
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/transactions
  • TIMESTAMP คือข้อความเดียวกับที่คุณส่งใน X-Timestamp
  • BODY คือไบต์ดิบที่คุณส่งจริง ให้แปลงเป็นข้อความครั้งเดียวแล้วส่งไบต์ชุดเดียวกับที่เซ็น หากแปลงใหม่ (ลำดับคีย์ต่างไป ช่องว่างต่างไป หรือรูปแบบตัวเลขต่างไป) ลายเซ็นจะไม่ตรง
  • เมื่อไม่มี body ให้ ตัด | ตัวท้ายออกทั้งหมด คำขอแบบ GET จึงเซ็นสามส่วน ไม่ใช่สี่ส่วน

ตัวอย่าง (คำขอแบบ POST):

text
SignatureData = "POST|/v1/payments/deposit|1702400000|{\"amount\":100.00,\"currency\":\"THB\",\"reference_id\":\"ORDER-001\"}"

ตัวอย่าง (คำขอแบบ GET):

text
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

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

javascript
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
<?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

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, รหัสข้อผิดพลาด และชื่อคอลัมน์ในไฟล์ส่งออก — คงไว้เป็นภาษาอังกฤษตามที่ระบบใช้จริง