SPay LINE 入款通知對接文件

給 LINE 入款通知提供方:如何把 HOUSE 卡入款通知送到 SPay,以及如何簽章、判讀回應與安全重送。

概要

環境

環境Base URL
DEVhttps://spay4api.ogp99.xyz
UAThttps://spay4api.ogp88.xyz
PRDhttps://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 十六進位}

簽章

Header規則
X-Api-KeySPay 交付給該環境的 API key。
X-TimestampUnix epoch 秒(不是毫秒),1 到 10 位 ASCII 數字;與 SPay 伺服器時間相差須在 ±300 秒以內(含 300 秒)。
X-Nonce小寫 UUID v4(例如 123e4567-e89b-42d3-a456-426614174000);每個請求都要產生新值,同一組憑證用過的 nonce 一律被拒,即使仍在 300 秒內。
X-SignatureHMAC-SHA256 結果的十六進位字串,64 個字元,建議小寫;不是 Base64。

簽章資料是下列 UTF-8 字串,後面直接接上實際送出的 body bytes:

POST\n/v2/external/house-card-transactions/ingress\n{X-Timestamp}\n{X-Nonce}\n{body bytes}

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 接受的格式:

不含時區的格式以 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"}}

回應碼

請以 HTTP status 與 body 的 code 判斷結果,不要依賴 message 文字。

HTTPcode情境處理方式
2002000通知已接收(含相同內容的重送)。保存 transactionNo,不需再送。
40140000API key、簽章、timestamp 或 nonce 不符;nonce 重複使用;帶 query string;body 超過 1 MiB。Body 固定為 {"code":40000,"message":"no authentication"}。修正後以新的 timestamp、nonce 與簽章重送,message_id 與內容不變。
401/40340000/40001憑證目前不可用於此入口(例如已停用或未綁定)。聯絡 SPay Operator 確認憑證,不要改用其他憑證。
40040017內容無法解析:外層或內層 JSON 格式錯誤、欄位型別錯誤、缺少必填欄位、type 不是 IN、無法辨識的 bank、金額或交易時間無法解析。本次未被接受;修正資料後再送。
40040005欄位值超出限制:長度、控制字元、未來的交易時間、交易時間秒以下超過 6 位、金額位數或小數精度。本次未被接受;修正資料後再送。
40940049同一個 message_id 已以不同內容送過;SPay 保留第一次的內容,不會覆寫。不要以新內容覆蓋或換 message_id 重送;聯絡 SPay Operator 處理。
50340044SPay 暫時無法處理此來源的通知,本次未保存。稍後以新的 timestamp、nonce 與簽章重送。
500,或連線中斷、逾時—結果不明:通知可能已經保存。以相同 message_id 與相同內容重送(新的 timestamp、nonce 與簽章);重送是安全的。

重送與冪等

簽章範例

以下是公開的測試向量,secret 只用於驗證實作,不能呼叫任何環境。可執行 examples/sign-line-request.mjs(Node.js 18 以上,只用標準函式庫)以 --self-test 核對。

secretline-public-test-only-secret
X-Timestamp1785168000
X-Nonce123e4567-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-Signature44731881b0a95b02e217bbd6748842398f7b58a482b08933634a64d39ae90508

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()

上線檢查