概要
- 每一筆入款通知是一個
POST請求,SPay 以提供方專屬的 API key/secret 驗證 HMAC-SHA256 簽章。 - 只支援入款:內層
type只接受IN,出款通知會被拒絕。 - SPay 先保存通知,再另行配對訂單。HTTP 200 只代表通知已安全接收,不代表訂單已完成;配對結果不會在回應中提供。
- 簽章證明請求來自持有該組憑證的提供方,不代表 SPay 另向銀行核實入款。
- API key/secret 由 SPay Operator 以安全管道另行交付,不會出現在本文件;secret 只能存放在提供方服務端。
環境
| 環境 | Base URL |
|---|---|
| DEV | https://spay4api.ogp99.xyz |
| UAT | https://spay4api.ogp88.xyz |
| PRD | https://spay4api.ogp77.xyz |
下文的 {baseUrl} 代表上表的 Base URL;每個環境使用各自的 API key/secret,不可混用。
端點
POST {baseUrl}/v2/external/house-card-transactions/ingress
Content-Type: application/json
X-Api-Key: {API key}
X-Timestamp: {epoch 秒}
X-Nonce: {新的 UUID v4}
X-Signature: {HMAC-SHA256 十六進位}
- 路徑必須完全相同,不可附帶 query string(連空的
?也不行),否則回 401。 - Body 為 UTF-8 JSON,上限 1,048,576 bytes(1 MiB);超過即回 401。
- 建議送
Content-Type: application/json。
簽章
| Header | 規則 |
|---|---|
X-Api-Key | SPay 交付給該環境的 API key。 |
X-Timestamp | Unix epoch 秒(不是毫秒),1 到 10 位 ASCII 數字;與 SPay 伺服器時間相差須在 ±300 秒以內(含 300 秒)。 |
X-Nonce | 小寫 UUID v4(例如 123e4567-e89b-42d3-a456-426614174000);每個請求都要產生新值,同一組憑證用過的 nonce 一律被拒,即使仍在 300 秒內。 |
X-Signature | HMAC-SHA256 結果的十六進位字串,64 個字元,建議小寫;不是 Base64。 |
簽章資料是下列 UTF-8 字串,後面直接接上實際送出的 body bytes:
POST\n/v2/external/house-card-transactions/ingress\n{X-Timestamp}\n{X-Nonce}\n{body bytes}
\n是單一換行字元(LF);開頭沒有任何協定名稱,body 之後也不補換行。- HMAC 的 key 是 API secret 的 UTF-8 bytes。
- 對「實際送出的 bytes」簽章:簽完後不可再重新排版、重新序列化或改變編碼。
- 簽章、時間或 nonce 任一不符,回 401,請以新的 timestamp、nonce 與簽章重送。
Payload
外層是 JSON object;msg_content 是一個內容為 JSON 的字串,不是巢狀 object。簽章針對整個外層 body 的 bytes,不是內層內容。
{
"app_type": "LINE",
"account_id": "provider-account-001",
"message_id": "LINE-D-0001",
"timestamp": "2026-10-07T10:30:00Z",
"msg_content": "{\"bank\":\"KBANK\",\"type\":\"IN\",\"transferInAccount\":\"0099001122\",\"transferOutAccount\":\"0012345678\",\"transferInAmount\":\"1,000.00\",\"transferInDate\":\"2026-10-07T10:30:00Z\"}"
}
外層欄位
| 欄位 | 必填 | 規則 |
|---|---|---|
message_id | 是 | 字串;這筆通知在提供方的唯一識別,重送時必須相同。去除前後空白後不可為空,最多 160 個字元,不可含控制字元;大小寫視為不同。 |
msg_content | 是 | 字串,內容為下表欄位組成的 JSON object。 |
app_type、account_id、timestamp | 否 | 字串,僅作紀錄參考;不決定入款帳戶、交易時間或任何業務結果。 |
id | 否 | 整數,僅作紀錄參考;不能取代 message_id。 |
msg_content 內層欄位
| 欄位 | 必填 | 規則 |
|---|---|---|
bank | 是 | 字串,銀行識別碼(例如 KBANK),須為雙方已約定的值。 |
type | 是 | 字串,只接受 IN(不分大小寫)。 |
transferInAccount | 是 | 字串,收款帳號,最多 100 個字元;遮罩帳號請照原樣傳送,不要自行補全。 |
transferOutAccount | 否 | 字串或 null,付款帳號;有值時最多 100 個字元,不可含控制字元。未提供付款帳號的通知會保存,但不會自動配對。 |
transferInAmount | 是 | 字串,正數金額,可含千分位逗號(例如 1,000.00);整數最多 20 位、小數最多 10 位,SPay 不會四捨五入。請填實際入帳金額,不是手續費或餘額。 |
transferInDate | 是 | 字串,交易時間,解析後以 UTC 保存,不可晚於現在;秒以下最多 6 位(微秒),超過回 400/40005;格式見下方。外層 timestamp 不能取代此欄。 |
availableBalance | 否 | 字串,有值時須為數字;僅供參考,不影響入款金額。 |
transferOutAmount、transferOutDate、senderName、remark、source、transferBank、bankTextInfo | 否 | 字串,僅作紀錄參考,不決定入款結果。 |
transferInDate 接受的格式:
- ISO 8601 含時區,例如
2026-10-07T10:30:00Z、2026-10-07T17:30:00+07:00(建議使用)。 yyyy-MM-dd HH:mm:ss,例如2026-10-07 17:30:00。- 西元日期
d/M/yyyy H:mm,時間前可加@,例如7/10/2026 17:30、7/10/2026 @17:30。 - 泰國佛曆兩位年
d/M/yy H:mm,例如7/10/69 17:30。 - 泰國佛曆四位年加泰文月份縮寫(
ม.ค.至ธ.ค.),結尾可加น.,例如7 ต.ค. 2569 17:30 น.。
不含時區的格式以 SPay 為該商戶設定的營業時區解讀;可行時請使用含時區的 ISO 8601。
bank 為 GSB 且使用不含時區的格式時,23:00(含)以後的時間會被視為前一日的同一時刻(例如 7/10/2026 23:15 視為 10 月 6 日 23:15);ISO 8601 含時區的格式不做此調整。同一來源請固定使用同一種格式。
JSON 格式錯誤、重複欄位、結尾多餘資料或已知欄位型別錯誤(例如字串欄位送數字)都會被拒絕;未列出的額外欄位會被忽略。
回應
接收成功時回 HTTP 200:
{"code":2000,"message":"","data":{"transactionNo":"00000000-0000-4000-8000-000000000001"}}
transactionNo是 SPay 對這筆通知的追蹤編號,請保存以便對帳;請把它視為不透明字串,不要解析其內容。- 同一個
message_id以相同內容重送,會得到同一個transactionNo,不會重複入款。 - 回應不包含訂單是否配對成功;配對與後續處理由 SPay 進行。
回應碼
請以 HTTP status 與 body 的 code 判斷結果,不要依賴 message 文字。
| HTTP | code | 情境 | 處理方式 |
|---|---|---|---|
| 200 | 2000 | 通知已接收(含相同內容的重送)。 | 保存 transactionNo,不需再送。 |
| 401 | 40000 | API key、簽章、timestamp 或 nonce 不符;nonce 重複使用;帶 query string;body 超過 1 MiB。Body 固定為 {"code":40000,"message":"no authentication"}。 | 修正後以新的 timestamp、nonce 與簽章重送,message_id 與內容不變。 |
| 401/403 | 40000/40001 | 憑證目前不可用於此入口(例如已停用或未綁定)。 | 聯絡 SPay Operator 確認憑證,不要改用其他憑證。 |
| 400 | 40017 | 內容無法解析:外層或內層 JSON 格式錯誤、欄位型別錯誤、缺少必填欄位、type 不是 IN、無法辨識的 bank、金額或交易時間無法解析。 | 本次未被接受;修正資料後再送。 |
| 400 | 40005 | 欄位值超出限制:長度、控制字元、未來的交易時間、交易時間秒以下超過 6 位、金額位數或小數精度。 | 本次未被接受;修正資料後再送。 |
| 409 | 40049 | 同一個 message_id 已以不同內容送過;SPay 保留第一次的內容,不會覆寫。 | 不要以新內容覆蓋或換 message_id 重送;聯絡 SPay Operator 處理。 |
| 503 | 40044 | SPay 暫時無法處理此來源的通知,本次未保存。 | 稍後以新的 timestamp、nonce 與簽章重送。 |
| 500,或連線中斷、逾時 | — | 結果不明:通知可能已經保存。 | 以相同 message_id 與相同內容重送(新的 timestamp、nonce 與簽章);重送是安全的。 |
重送與冪等
message_id是冪等識別:同一筆入款不論送幾次都使用同一個message_id與完全相同的內容。- 每一次 HTTP 請求都要使用新的 timestamp、nonce 與簽章;只有這三者會因重送而改變。
- 不要為了重送而更換
message_id,否則會被視為另一筆通知。 - 收到 409 代表內容與先前不一致,重送不會改變結果。
- 本文件不規定重送次數、間隔與 client timeout;雙方於聯測時約定。
簽章範例
以下是公開的測試向量,secret 只用於驗證實作,不能呼叫任何環境。可執行 examples/sign-line-request.mjs(Node.js 18 以上,只用標準函式庫)以 --self-test 核對。
| secret | line-public-test-only-secret |
|---|---|
| X-Timestamp | 1785168000 |
| X-Nonce | 123e4567-e89b-42d3-a456-426614174000 |
| body(單行,無結尾換行) | {"app_type":"LINE","account_id":"synthetic-provider-account","message_id":"LINE-D-0001","timestamp":"2026-10-07T10:30:00Z","msg_content":"{\"bank\":\"KBANK\",\"type\":\"IN\",\"transferInAccount\":\"0099001122\",\"transferOutAccount\":\"0012345678\",\"transferInAmount\":\"1,000.00\",\"transferInDate\":\"2026-10-07T10:30:00Z\"}"} |
| X-Signature | 44731881b0a95b02e217bbd6748842398f7b58a482b08933634a64d39ae90508 |
Python 3 等價寫法:
import hashlib, hmac
def sign(secret: str, timestamp: str, nonce: str, body: bytes) -> str:
prefix = f"POST\n/v2/external/house-card-transactions/ingress\n{timestamp}\n{nonce}\n".encode()
return hmac.new(secret.encode(), prefix + body, hashlib.sha256).hexdigest()
上線檢查
- API secret 只存放在服務端的安全設定;不要寫進 log、URL、前端程式或文件。
- 服務端時鐘以 NTP 校時,確保與 SPay 相差在 ±300 秒內。
- 每個請求產生新的小寫 UUID v4 nonce。
- 簽章使用實際送出的 body bytes,且
X-Signature為十六進位。 - 同一筆入款的
message_id與內容在重送時保持不變。 - 保存每筆通知的
transactionNo供對帳。 - 先以測試向量通過
--self-test,再於 SPay 指定的測試環境聯測。