This is the full developer documentation for 應援金流(OEN Payment) # 應援金流文件 > 不管你是要開通收款的商家,還是負責串接的工程師,都從這裡開始。 ## 我想要… [Section titled “我想要…”](#我想要) 輸入關鍵字,或點「我是商家」「我是工程師」篩選。每一項都同時列出商家在後台怎麼做、工程師怎麼串接。 搜尋你想做的事輸入關鍵字,例如:退款、定期、403、發票 全部我是商家我是工程師 * 開始收款、開通信用卡或其他付款方式[商家怎麼做](/merchant/activate/)[工程師怎麼做](/developers/payment-methods/) * 讓消費者付一筆款(購物、捐款、報名)[商家怎麼做](/merchant/payment-experience/)[工程師怎麼做](/developers/one-time/)[API](/api/checkout/) * 讓消費者用超商代碼繳費[商家怎麼做](/merchant/payment-experience/)[工程師怎麼做](/developers/payment-methods/)[API](/api/checkout/) * 每個月自動扣款(定期定額、會員月費)[商家怎麼做](/merchant/subscriptions/)[工程師怎麼做](/developers/subscriptions/)[API](/api/checkout-subscription/) * 從指定日期才開始扣款[商家怎麼做](/merchant/subscriptions/)[工程師怎麼做](/developers/subscriptions/)[API](/api/checkout-schedule/) * 記住消費者的卡片,之後由我決定何時扣款[工程師怎麼做](/developers/saved-cards/)[API](/api/checkout-token/) * 查一筆付款成功了沒[商家怎麼做](/merchant/transactions/)[工程師怎麼做](/developers/querying/)[API](/api/get-transaction/) * 列出一段時間的交易來對帳[商家怎麼做](/merchant/transactions/)[工程師怎麼做](/developers/querying/)[API](/api/list-transactions/) * 退款給消費者[商家怎麼做](/merchant/refunds/)[工程師怎麼做](/developers/refunds/)[API](/api/refund/) * 取消定期定額[商家怎麼做](/merchant/subscriptions/)[工程師怎麼做](/developers/subscriptions/)[API](/api/cancel-subscription/) * 付款完成後自動通知我的系統[工程師怎麼做](/developers/webhooks/)[API](/api/objects/webhook/) * 拿到串接用的 API token[商家怎麼做](/merchant/api-access/)[工程師怎麼做](/developers/authentication/) * 正式環境呼叫 API 被擋(403)、申請固定 IP[商家怎麼做](/merchant/api-access/)[工程師怎麼做](/developers/ip-allowlist/) * 在測試環境試刷[工程師怎麼做](/developers/environments/) * 看懂 API 回的錯誤碼[工程師怎麼做](/developers/errors/)[API](/api/error-codes/) * 付款結果不確定,不想重複扣款[工程師怎麼做](/developers/errors/) * 開立或作廢電子發票、退款折讓[商家怎麼做](/merchant/invoices/)[工程師怎麼做](/developers/one-time/) * 我的網站是 WordPress/WooCommerce[商家怎麼做](/merchant/woocommerce/) * 付款欄位要嵌在自己的頁面[工程師怎麼做](/products/embed/) * 請 AI 助手幫我寫串接程式[工程師怎麼做](/ai/) * 上線前最後確認[商家怎麼做](/merchant/api-access/)[工程師怎麼做](/developers/go-live/) 找不到符合的項目。試試右上角的全站搜尋,或看[常見問題](/start/faq/)。 ## 依身分開始 [Section titled “依身分開始”](#依身分開始) 商家、營運、客服 不用寫程式。開通付款方式、看懂消費者的付款流程、查交易、退款、管理定期定額與發票,都在 CRM 後台完成。 * [商家指南總覽](/merchant/) * [開通金流與付款方式](/merchant/activate/) * [交給工程師串接前的準備](/merchant/api-access/) 工程師 用 Payment API 建立結帳頁、定期定額與存卡扣款,接收付款通知並處理退款。範例有 cURL、Node.js、PHP、Python。 * [快速開始](/developers/quickstart/) * [API 參考](/api/) * [上線檢查清單](/developers/go-live/) ## 還不確定要用哪一種? [Section titled “還不確定要用哪一種?”](#還不確定要用哪一種) [我該用哪一種收款方式?](/start/choose/)回答兩三個問題,找到適合你的串接方式。 ## 產品一覽 [Section titled “產品一覽”](#產品一覽) | 產品 | 適合 | 狀態 | | ----------------------------------------------- | ------------------------------- | ----- | | [Payment API](/developers/quickstart/) | 自己的網站或 App:單次付款、定期定額、存卡扣款、查詢與退款 | 可直接使用 | | [WooCommerce 外掛](/merchant/woocommerce/) | WordPress 商家,不用寫程式 | 需開通 | | [Embed 嵌入式付款](/products/embed/) | 信用卡表單嵌在自己的頁面 | 需開通 | | [Subscription API](/products/subscription-api/) | 有試用期、方案變更等完整訂閱管理 | 需開通 | | [Payment Skill 與 Payment MCP](/ai/) | 讓 AI 助手幫你寫串接程式 | 見各頁說明 | ## 給 AI 讀 [Section titled “給 AI 讀”](#給-ai-讀) 整站內容也提供純文字版,貼給 AI 助手就能根據最新文件回答:[`/llms.txt`](/llms.txt)、[`/llms-full.txt`](/llms-full.txt)。詳見[用 AI 助手串接](/ai/)。 # 我該用哪一種收款方式? > 回答兩三個問題,找到適合你的串接方式;下方的比較表列出各方式的差異。 先用問答找到方向,再用下方的比較表確認細節。 1\. 你打算怎麼收款? 我的網站是 WordPress + WooCommerce裝外掛就能收款,不用寫程式我有自己的網站或 App,有工程師可以串接自己的團隊或外包我還沒有工程師,想先了解流程先看商家指南,準備好再交給工程師 2\. 要收哪一種款項? 一次付清購物、捐款、活動報名固定金額、固定週期自動扣款會員月費、定期捐款先存卡,之後由我決定何時扣、扣多少儲值、隨用隨扣、快速結帳 3\. 付款畫面要放在哪裡? 把消費者導到應援的結帳頁最快上線,可收信用卡、超商代碼等嵌在我自己的頁面,消費者不離開網站目前只支援信用卡 建議你用 Payment API:單次付款結帳頁建議 你的伺服器呼叫 `POST /checkout` 取得結帳頁,把消費者導過去付款。付款結果以付款通知與查詢 API 確認。 [快速開始 →](/developers/quickstart/)[單次付款指南 →](/developers/one-time/)[API 參考 →](/api/checkout/) 建議你用 Payment API:定期定額建議 消費者在結帳頁綁卡並同意扣款,之後由應援依週期自動扣款。要從指定日期才開始扣,用預約定期定額。 [定期定額指南 →](/developers/subscriptions/)[建立定期定額 →](/api/checkout-subscription/)[建立預約定期定額 →](/api/checkout-schedule/) 建議你用 Payment API:存卡與後續扣款 消費者在綁卡頁完成 3D 驗證後,你會拿到一組 token,之後由你的伺服器用 token 扣款,消費者不必在場。 [存卡與後續扣款指南 →](/developers/saved-cards/)[建立綁卡頁 →](/api/checkout-token/)[用 token 扣款 →](/api/token-transactions/) 建議你用 Embed 嵌入式付款需開通 信用卡表單嵌在你自己的頁面。要先請應援開通,目前只支援信用卡。 [Embed 說明 →](/products/embed/) 建議你用 WooCommerce 外掛需開通 在 WordPress 後台安裝外掛、填入金鑰就能收款,不用寫程式。 [安裝與設定 →](/merchant/woocommerce/) 建議你用 先看商家指南 了解開通流程、消費者付款畫面、退款與對帳,再把「交給工程師串接前的準備」這頁轉給你的工程師。 [商家指南總覽 →](/merchant/)[交給工程師串接前的準備 →](/merchant/api-access/) 重新選擇 ## Payment API 的三種收款流程 [Section titled “Payment API 的三種收款流程”](#payment-api-的三種收款流程) 大多數商家用 Payment API 就夠了。它有三種流程,差別在「誰決定什麼時候扣款」: | | 單次付款 | 定期定額 | 存卡與後續扣款 | | ------- | ----------------------------- | ---------------------------------- | ----------------------------------- | | 適合 | 購物、捐款、活動報名 | 會員月費、定期捐款 | 儲值、隨用隨扣、快速結帳 | | 消費者要做的事 | 在結帳頁付款 | 在結帳頁綁卡並同意扣款 | 在綁卡頁完成 3D 驗證 | | 之後誰扣款 | 不再扣款 | 應援依週期自動扣款 | 你的伺服器呼叫 API 扣款 | | 金額 | 每次建立時決定 | 固定 | 每次扣款時決定 | | 開始 | [單次付款](/developers/one-time/) | [定期定額](/developers/subscriptions/) | [存卡與後續扣款](/developers/saved-cards/) | ## 各種方式比較 [Section titled “各種方式比較”](#各種方式比較) 表格可以左右滑動。 | | Payment API | WooCommerce 外掛 | Embed 嵌入式付款 | Subscription API | | ----- | ------------------------------- | ------------------------------- | ------------------------- | ----------------------------------------------- | | 狀態 | 可直接使用 | 需開通 | 需開通 | 需開通 | | 適合 | 自己的網站或 App | WordPress 商家 | 付款欄位嵌在自己頁面 | 需要試用期、方案變更等訂閱管理 | | 要寫程式嗎 | 要 | 不用 | 要(前端與後端) | 要 | | 付款畫面 | 應援結帳頁 | 應援結帳頁 | 你的頁面 | 見產品頁 | | 驗證 | CRM 產生的 token | 外掛設定頁填入金鑰 | `pk_` 與 `sk_` 金鑰 | `sub_sk_` 金鑰 | | 付款通知 | 沒有簽章,收到後回查 | 外掛自動處理 | 有簽章 | 有簽章 | | 說明 | [快速開始](/developers/quickstart/) | [安裝與設定](/merchant/woocommerce/) | [Embed](/products/embed/) | [Subscription API](/products/subscription-api/) | ## 已經在用 Payment API? [Section titled “已經在用 Payment API?”](#已經在用-payment-api) Payment API 是應援主要支援的串接方式,會持續維護並加入新功能。這一版文件依正式環境 v10.3.1.1 更新,差異見[更新紀錄](/changelog/)。 # 常見問題 > 商家與工程師最常問的問題:開通、token、固定 IP、查交易、退款、定期扣款、發票、撥款、付款通知與錯誤。 找不到答案時,可以用右上角的搜尋,或[聯絡我們](#%E8%81%AF%E7%B5%A1%E6%88%91%E5%80%91)。 ## 開通 [Section titled “開通”](#開通) **申請送出後要等多久?現在卡在哪一關?** 到 CRM「金流開通狀態」頁查看申請紀錄的狀態。各狀態的意思見[開通金流](/merchant/activate/#%E5%AF%A9%E6%A0%B8%E7%8B%80%E6%85%8B)。需要的時間依個案而定。 **顯示「文件未通過」怎麼辦?** 依通知補件後重新送出。有疑問請聯絡客服。 **申請表為什麼沒有超商代碼、ATM、LINE Pay 可以勾?** 這三種要請應援開通,申請表只有信用卡、Apple Pay 與應碰收。見[各付款方式怎麼開通](/merchant/activate/#%E5%90%84%E4%BB%98%E6%AC%BE%E6%96%B9%E5%BC%8F%E6%80%8E%E9%BA%BC%E9%96%8B%E9%80%9A)。 **「帳單顯示名稱」填錯了能改嗎?** 設定後無法更改,請在送出前確認。 **費率表顯示「未開通請聯絡應援」是什麼意思?** 這種付款方式還沒開通,請聯絡業務人員。 ## API 串接 [Section titled “API 串接”](#api-串接) **後台找不到「金流API」選單或「開發者」分頁?** 網域的 API 串接還沒開啟,或你的帳號沒有權限。見[交給工程師串接前的準備](/merchant/api-access/)。 **「產生 Token」按鈕是灰的?** 帳號權限不足,請管理員指派「金流串接管理員」角色。 **忘了複製 token,還看得到嗎?** 看不到,只能重新產生。重新產生會讓舊的立刻失效。 **測試環境和正式環境的 token 可以共用嗎?** 不行,要分別在各自的 CRM 產生。 **呼叫 API 一直回 401?** token 被重新產生過、用錯環境,或 header 格式錯誤。見[驗證與 token](/developers/authentication/#%E6%94%B6%E5%88%B0-401-a0001)。 **測試環境都正常,上線就回 403?** 正式環境的 IP 還沒綁定。見[固定 IP 白名單](/developers/ip-allowlist/)。 **付款通知網址可以在 API 請求裡帶嗎?** 不行,只能在 CRM 設定。請求裡多帶的欄位會被忽略。 **有沒有 Postman 可以用?** 可以匯入本站的 [OpenAPI 定義檔](/openapi/payment-api.json)。舊的 Postman 文件已不再更新。 ## 付款與結帳頁 [Section titled “付款與結帳頁”](#付款與結帳頁) **可以只開超商代碼、不要信用卡嗎?** 不行,信用卡一定會出現。見[付款方式](/developers/payment-methods/)。 **結帳連結可以先產生好,用 email 寄給消費者嗎?** 不行,結帳頁從建立起 5 分鐘內有效。請在消費者按下付款時才建立。 **消費者付款成功了,卻沒有回到我的網站?** 消費者可能付完就關掉視窗。請以付款通知與查詢結果為準,不要只靠導回。 **支援 Google Pay 或信用卡分期嗎?** 目前不支援。 **可以收港幣或其他外幣嗎?** Payment API 目前只支援新台幣。 ## 查交易 [Section titled “查交易”](#查交易) **API 交易在 CRM 哪裡找?** 「金流管理」→「金流列表」,類型是「現金購買」。可以用金流編號、訂單編號、姓名、Email、電話搜尋。 **為什麼只看到最近三個月?** 這是預設的日期範圍,可以自己調整。 **匯出的檔案在哪裡?** 到「下載管理」,完成後 1 小時內可以下載。 **「尚未付款」「已入帳」「撥款後退款」「已失效」是什麼意思?** 見[交易狀態](/merchant/transactions/#%E4%BA%A4%E6%98%93%E7%8B%80%E6%85%8B)。 ## 退款 [Section titled “退款”](#退款) **為什麼這筆交易沒有「退款」按鈕?** 交易不是「付款成功」或「已入帳」、已經退過一次,或你沒有退款權限。 **可以分兩次退嗎?** 不行,每筆交易只能退一次。 **超商付款的退款為什麼要填銀行帳戶?多久會退?** 超商代碼、ATM 的款項要用匯款退回。退款作業需要 3 到 7 個工作天。 **已經撥款的交易退款,錢從哪裡扣?** 從下次撥款扣除退款金額與退款手續費。 **出現「此筆金流缺少開立折讓所需的商品明細」?** 這筆交易有開發票但缺少商品明細,無法在 CRM 退款,請聯絡客服。 **用 API 退款收到 500 `Current charged amount too low`?** 你的網域尚未撥款的已收款金額低於 200 元,暫時無法退款,請聯絡應援。 ## 定期扣款 [Section titled “定期扣款”](#定期扣款) **怎麼取消某位消費者的定期扣款?取消後能恢復嗎?** 在 CRM「定期購買」按「終止定期購買」,或用 [API 取消](/api/cancel-subscription/)。取消後不能恢復。 **扣款失敗會自動重扣嗎?** 預設不會,而且一期失敗整個定期定額就停止。請在 CRM 開啟「定期交易重試機制」。見[扣款失敗](/developers/subscriptions/#%E6%89%A3%E6%AC%BE%E5%A4%B1%E6%95%97)。 **消費者要換信用卡,有頁面可以給他嗎?** Payment API 的定期購買沒有換卡頁面。常見做法是取消後請消費者重新訂閱。 **金流列表出現 1 元的「綁卡試刷」是什麼?** 綁卡時用來驗證卡片的 1 元授權,會立即退回。 ## 發票 [Section titled “發票”](#發票) **API 交易會自動開發票嗎?** 開通「應援代開電子發票」後,付款成功就會自動開立。見[電子發票](/merchant/invoices/)。 **發票開錯了怎麼作廢?** 後台沒有作廢功能,請聯絡客服。 **發票品名後面的「(受託代銷)」是什麼?** 由應援代開發票時會加上這幾個字。想用自己公司名義開立,請聯絡應援。 ## 撥款 [Section titled “撥款”](#撥款) **撥款頻率怎麼改?什麼時候生效?** 「總設定」→「金流設定」→「撥款方式設定」,從下一次撥款生效。 **為什麼 LINE Pay、應碰收 TWQR、藍新的錢不在撥款裡?** 這些款項由該機構直接撥給你。 **撥款帳戶要改?** 寄信到 。 ## 付款通知 [Section titled “付款通知”](#付款通知) **付款通知沒收到?** 應援最多送 3 次,都失敗就不會再送,也不能手動重送。請用訂單編號查詢補救。見[沒收到通知時](/developers/webhooks/#%E6%B2%92%E6%94%B6%E5%88%B0%E9%80%9A%E7%9F%A5%E6%99%82)。 **付款通知有簽章嗎?** 沒有。收到後一定要用查詢 API 回查。 **退款、取消定期扣款有通知嗎?** 要請應援開啟「Webhook 新版事件」才會送。 ## WooCommerce [Section titled “WooCommerce”](#woocommerce) **Secret Key 要填哪一個?** 「OenPay Embed」分頁的 Secret Key,不是「開發者」分頁的存取 Token。見 [WooCommerce 外掛](/merchant/woocommerce/)。 **Webhook Secret 去哪裡拿?** 留空,存檔時外掛會自動取得。 ## 聯絡我們 [Section titled “聯絡我們”](#聯絡我們) * 常見問題:[www.oen.tw/faq](https://www.oen.tw/faq) * 商家客服(LINE):[lin.ee/zgKzwaZ](https://lin.ee/zgKzwaZ),也可以從 CRM 右上角選單的「聯絡客服」進入 * 撥款與帳務: 聯絡時請提供網域名稱,以及交易編號或訂單編號。**請不要提供 token 或 Secret Key。** # 名詞對照 > 用白話解釋文件裡常出現的名詞:結帳頁、付款通知、token、3D 驗證、定期定額、交易編號、撥款、折讓等。 依主題排列。每個名詞後面附上對應的說明頁。 ## 帳號與串接 [Section titled “帳號與串接”](#帳號與串接) | 名詞 | 白話解釋 | | ------------------- | ------------------------------------------------------------------------------ | | CRM | 應援的商家管理後台,網址是 `https://{網域名稱}.oen.tw/crm`。 | | 網域名稱 | 你的應援頁子網域,例如 `ming.oen.tw` 的 `ming`。API 裡叫 `merchantId`,WooCommerce 外掛裡叫「商店代碼」。 | | API 串接(開發者模式) | 讓你的系統可以呼叫 Payment API 的開關,由應援開啟。 | | API token(存取 Token) | 你的系統呼叫 API 時用來證明身分的一串密碼,在 CRM 產生。見[驗證與 token](/developers/authentication/)。 | | 測試環境、正式環境 | 測試環境不會真的扣款,用來開發與測試;正式環境才是真的收款。兩邊的帳號與 token 分開。 | | 固定 IP 白名單 | 正式環境只接受事先登記過的伺服器 IP 呼叫。見[固定 IP 白名單](/developers/ip-allowlist/)。 | ## 付款 [Section titled “付款”](#付款) | 名詞 | 白話解釋 | | ------------- | ---------------------------------------------------- | | 結帳頁 | 應援提供的付款頁面,消費者在這裡輸入信用卡或選擇付款方式。 | | 3D 驗證 | 刷卡時跳到發卡銀行頁面,用簡訊驗證碼等方式確認是持卡人本人。 | | 付款通知(webhook) | 付款結果確定時,應援自動送到你系統的訊息。見[付款通知](/developers/webhooks/)。 | | 回查 | 收到付款通知後,再呼叫查詢 API 確認結果。因為付款通知沒有簽章,一定要回查。 | | 超商代碼 | 消費者拿代碼到超商繳費的付款方式。 | | ATM 虛擬帳號 | 每筆交易產生一個專屬帳號,消費者轉帳到這個帳號完成付款。 | | 授權碼 | 刷卡成功時發卡銀行給的號碼,對帳時會用到。 | ## 交易與編號 [Section titled “交易與編號”](#交易與編號) | 名詞 | 白話解釋 | | ------------- | ------------------------------------------------------------ | | 交易編號 | `P` 開頭、17 字元的編號,例如 `P20260928AB12CD34`。CRM 的「金流編號」就是它,退款也用它。 | | 交易 id | 27 字元的英數字串。用來組結帳頁網址與查詢交易。 | | 訂單編號(orderId) | 你自己系統的訂單編號,建立交易時帶給應援,之後可以用它查詢。 | | 定期定額編號 | `S` 開頭、17 字元的編號,查詢與取消定期定額用。 | | 現金購買 | API 交易在 CRM 金流列表顯示的類型名稱。 | ## 定期扣款與存卡 [Section titled “定期扣款與存卡”](#定期扣款與存卡) | 名詞 | 白話解釋 | | ---------- | ------------------------------------------------------------------------- | | 定期定額(定期購買) | 消費者同意一次,之後由應援依週期自動扣固定金額。CRM 裡叫「定期購買」。見[定期定額](/developers/subscriptions/)。 | | 預約定期定額 | 可以指定從哪一天開始扣、每幾個月扣一次的定期定額。 | | 重扣 | 定期扣款失敗後,隔天再試一次。預設關閉,要在 CRM 開啟。 | | 存卡、綁卡 | 消費者先登記信用卡,之後由商家決定何時扣款。 | | token(卡片) | 存卡後得到的代號,用來代替卡號扣款。跟「API token」是不同的東西。 | ## 退款與撥款 [Section titled “退款與撥款”](#退款與撥款) | 名詞 | 白話解釋 | | --------------- | --------------------------------- | | 部分退款 | 只退一部分金額。每筆交易只能退一次,所以部分退款後就不能再退。 | | 退款帳戶(remitInfo) | 超商代碼、ATM 付款的交易退款時,要退到消費者的哪個銀行帳戶。 | | 折讓 | 已開發票的交易退款時,開立的電子發票折讓單,系統會自動處理。 | | 撥款 | 應援把代收的款項匯給商家。頻率可以在 CRM 設定。 | | 撥款後退款 | 款項已經撥給商家後才退款,退款金額會從下次撥款扣除。 | | 帳單顯示名稱 | 出現在消費者信用卡帳單上的商家名稱,申請金流時設定,之後不能更改。 | # 快速開始 > 在測試環境建立第一筆結帳、把消費者導到應援結帳頁、收到付款通知並回查結果。 這一頁帶你在測試環境走完一筆單次付款:建立結帳 → 消費者付款 → 收到付款通知 → 回查確認。 ## 開始前 [Section titled “開始前”](#開始前) * **網域名稱**:例如應援頁是 `ming.oen.tw`,網域名稱就是 `ming`。以下範例都用 `ming`,請換成你的。 * **測試環境的 token**:在測試環境 CRM(`https://{網域名稱}.testing.oen.tw/crm`)的「總設定」→「開發者」→「應援金流設定」產生。還沒有測試環境帳號請請商家聯絡應援。詳見[交給工程師串接前的準備](/merchant/api-access/)。 * **能接收 HTTPS 的網址**:付款通知會送到這裡。本機開發可以用 tunnel 工具取得公開的 https 網址,再填到 CRM 的「交易資料回傳網址位置」。 注意 API 只能從你的伺服器呼叫。token 就像密碼,請放在伺服器的環境變數,不要放進前端程式或版本控制。 ## 1. 設定 token [Section titled “1. 設定 token”](#1-設定-token) ```bash export OEN_API_TOKEN="貼上測試環境的 token" export OEN_MERCHANT_ID="ming" ``` ## 2. 建立結帳 [Section titled “2. 建立結帳”](#2-建立結帳) 在消費者按下「付款」時,由你的伺服器呼叫 `POST /checkout`。三個重點: * `productDetails` **必填**,各品項「數量 × 單價」的合計必須等於 `amount`。 * 回應不會給結帳頁網址,要用 `data.id` 自己組:`https://{網域名稱}.testing.oen.tw/checkout/{id}`(正式環境沒有 `.testing`)。 * 結帳頁從建立起 **5 分鐘內**有效,請建立後立刻把消費者導過去。 - cURL ```bash curl -X POST "https://payment-api.testing.oen.tw/checkout" \ -H "Authorization: Bearer $OEN_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "merchantId": "ming", "amount": 1200, "orderId": "A20260928001", "successUrl": "https://shop.example.com/payment/success?order=A20260928001", "failureUrl": "https://shop.example.com/payment/failure/A20260928001", "productDetails": [ { "productionCode": "SKU-001", "description": "手沖咖啡豆 200g", "quantity": 2, "unit": "包", "unitPrice": 600 } ] }' ``` - Node.js ```js const API = "https://payment-api.testing.oen.tw"; const CHECKOUT_HOST = `https://${process.env.OEN_MERCHANT_ID}.testing.oen.tw`; export async function createCheckout(order) { const res = await fetch(`${API}/checkout`, { method: "POST", headers: { Authorization: `Bearer ${process.env.OEN_API_TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify({ merchantId: process.env.OEN_MERCHANT_ID, amount: order.total, // 必須等於下面品項的合計 orderId: order.id, successUrl: `https://shop.example.com/payment/success?order=${order.id}`, failureUrl: `https://shop.example.com/payment/failure/${order.id}`, productDetails: order.items.map((item) => ({ productionCode: item.sku, description: item.name, quantity: item.quantity, unit: item.unit, unitPrice: item.price, })), }), }); const result = await res.json(); if (result.code !== "S0000") { throw new Error(`${result.code} ${result.message}`); } // 把 id 與 transactionHid 存進你的訂單,之後回查與退款會用到 await saveOenTransaction(order.id, result.data.id, result.data.transactionHid); return `${CHECKOUT_HOST}/checkout/${result.data.id}`; } ``` - PHP ```php true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('OEN_API_TOKEN'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'merchantId' => $merchantId, 'amount' => $order['total'], // 必須等於品項合計 'orderId' => $order['id'], 'successUrl' => 'https://shop.example.com/payment/success?order=' . $order['id'], 'failureUrl' => 'https://shop.example.com/payment/failure/' . $order['id'], 'productDetails' => array_map(fn ($item) => [ 'productionCode' => $item['sku'], 'description' => $item['name'], 'quantity' => $item['quantity'], 'unit' => $item['unit'], 'unitPrice' => $item['price'], ], $order['items']), ]), ]); $result = json_decode(curl_exec($ch), true); if (($result['code'] ?? '') !== 'S0000') { throw new RuntimeException(($result['code'] ?? 'HTTP') . ' ' . ($result['message'] ?? '')); } // 把 id 與 transactionHid 存進你的訂單 saveOenTransaction($order['id'], $result['data']['id'], $result['data']['transactionHid']); return "https://{$merchantId}.testing.oen.tw/checkout/{$result['data']['id']}"; } header('Location: ' . createCheckout($order)); ``` - Python ```python import os import requests API = "https://payment-api.testing.oen.tw" MERCHANT_ID = os.environ["OEN_MERCHANT_ID"] def create_checkout(order: dict) -> str: res = requests.post( f"{API}/checkout", headers={"Authorization": f"Bearer {os.environ['OEN_API_TOKEN']}"}, json={ "merchantId": MERCHANT_ID, "amount": order["total"], # 必須等於品項合計 "orderId": order["id"], "successUrl": f"https://shop.example.com/payment/success?order={order['id']}", "failureUrl": f"https://shop.example.com/payment/failure/{order['id']}", "productDetails": [ { "productionCode": item["sku"], "description": item["name"], "quantity": item["quantity"], "unit": item["unit"], "unitPrice": item["price"], } for item in order["items"] ], }, timeout=30, ) result = res.json() if result.get("code") != "S0000": raise RuntimeError(f"{result.get('code')} {result.get('message')}") # 把 id 與 transactionHid 存進你的訂單 save_oen_transaction(order["id"], result["data"]["id"], result["data"]["transactionHid"]) return f"https://{MERCHANT_ID}.testing.oen.tw/checkout/{result['data']['id']}" ``` 回應: ```json { "code": "S0000", "data": { "id": "2HhndgEquCbDzC5OyVxWSGZmd2l", "transactionHid": "P20260928AB12CD34" }, "message": "" } ``` * `id`:組結帳頁網址,也是之後付款通知與回查用的 id。 * `transactionHid`:`P` 開頭的交易編號,退款與 CRM 搜尋用這個。 ## 3. 把消費者導到結帳頁 [Section titled “3. 把消費者導到結帳頁”](#3-把消費者導到結帳頁) 把消費者導到 `https://ming.testing.oen.tw/checkout/2HhndgEquCbDzC5OyVxWSGZmd2l`。消費者付款後: * 成功時回到 `successUrl`,**網址不會帶任何參數**,所以範例把訂單編號放在自己的網址裡。 * 失敗時回到 `failureUrl`,網址加上 `payment_error`,例如 `?payment_error=T0004`。 不要只靠導回來判斷付款成功 消費者可能付完款就關掉視窗,永遠不會回到你的頁面;`successUrl` 也可以被任何人直接打開。**出貨一律以付款通知與回查結果為準。** ## 4. 接收付款通知並回查 [Section titled “4. 接收付款通知並回查”](#4-接收付款通知並回查) 付款完成後,應援會把結果 `POST` 到你在 CRM 設定的網址。**付款通知沒有簽章**,任何人都能偽造,所以收到後一定要用 `id` 呼叫 `GET /transactions/{id}` 回查,以查到的結果為準。 * cURL ```bash # 用付款通知裡的 id 回查 curl "https://payment-api.testing.oen.tw/transactions/2HhndgEquCbDzC5OyVxWSGZmd2l" \ -H "Authorization: Bearer $OEN_API_TOKEN" ``` * Node.js ```js import express from "express"; const app = express(); const PAID = new Set(["charged", "claimed"]); app.post("/webhooks/oen", express.json(), (req, res) => { // 先回 200:應援每次最多等 10 秒,逾時會重送 res.sendStatus(200); const event = req.body; if (event.purpose !== "charge") return; // token 等其他通知另外處理 handleCharge(event.id).catch((err) => console.error("OEN webhook", err)); }); async function handleCharge(id) { // 不相信通知內容,一律回查 const res = await fetch(`https://payment-api.testing.oen.tw/transactions/${encodeURIComponent(id)}`, { headers: { Authorization: `Bearer ${process.env.OEN_API_TOKEN}` }, }); const result = await res.json(); if (result.code !== "S0000") throw new Error(`${result.code} ${result.message}`); const txn = result.data; // 確認是你建立的那一筆:比對訂單編號與金額 const order = await findOrderByOenId(txn.transactionId); if (!order || order.total !== txn.amount) return; if (PAID.has(txn.status)) await markOrderPaid(order.id, txn); // 同一筆重複呼叫也要安全 else if (txn.status === "failed") await markOrderFailed(order.id, txn); // charging:超商、ATM 等待繳費,等下一則通知 } app.listen(3000); ``` * PHP ```php true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('OEN_API_TOKEN')], ]); $result = json_decode(curl_exec($ch), true); if (($result['code'] ?? '') !== 'S0000') exit; $txn = $result['data']; $order = findOrderByOenId($txn['transactionId']); if (!$order || $order['total'] !== $txn['amount']) exit; if (in_array($txn['status'], ['charged', 'claimed'], true)) { markOrderPaid($order['id'], $txn); // 同一筆重複呼叫也要安全 } elseif ($txn['status'] === 'failed') { markOrderFailed($order['id'], $txn); } ``` * Python ```python import os import threading import requests from flask import Flask, request app = Flask(__name__) PAID = {"charged", "claimed"} def handle_charge(transaction_id: str) -> None: # 不相信通知內容,一律回查 res = requests.get( f"https://payment-api.testing.oen.tw/transactions/{transaction_id}", headers={"Authorization": f"Bearer {os.environ['OEN_API_TOKEN']}"}, timeout=30, ) result = res.json() if result.get("code") != "S0000": return txn = result["data"] order = find_order_by_oen_id(txn["transactionId"]) if not order or order["total"] != txn["amount"]: return if txn["status"] in PAID: mark_order_paid(order["id"], txn) # 同一筆重複呼叫也要安全 elif txn["status"] == "failed": mark_order_failed(order["id"], txn) @app.post("/webhooks/oen") def oen_webhook(): event = request.get_json(silent=True) or {} if event.get("purpose") == "charge": # 放到背景處理,先回 200:應援每次最多等 10 秒 threading.Thread(target=handle_charge, args=(event["id"],)).start() return "", 200 ``` ## 5. 付款測試 [Section titled “5. 付款測試”](#5-付款測試) 打開結帳頁,用測試卡付款,確認: * 你的伺服器收到付款通知,回查的 `status` 是 `charged`。 * 訂單狀態有更新。 * 在 CRM「金流管理」→「金流列表」看得到這筆交易(類型是「現金購買」)。 測試卡與測試環境的注意事項見[環境與測試](/developers/environments/)。 ## 接下來 [Section titled “接下來”](#接下來) * [單次付款](/developers/one-time/):付款方式、發票、結帳頁逾時與重新付款。 * [付款通知](/developers/webhooks/):重送規則、同一筆的多則通知、沒收到通知時怎麼補。 * [錯誤處理](/developers/errors/):哪些錯誤可以重試,付款結果不明時怎麼辦。 * 上線前走一遍[上線檢查清單](/developers/go-live/),並申請[固定 IP 白名單](/developers/ip-allowlist/)。 # API 參考 > Payment API 的共通規格:網址、驗證、回應格式、錯誤格式、分頁、時間與 ID,以及所有端點一覽。 這裡是 Payment API 每一支端點的完整規格。第一次串接請先看[快速開始](/developers/quickstart/)。 所有欄位、限制與錯誤碼都依正式環境 v10.3.1.1 的程式核對。也可以下載 [OpenAPI 定義檔](/openapi/payment-api.json),匯入 Postman 等工具。 ## 網址 [Section titled “網址”](#網址) | 環境 | API | 結帳頁 | | -- | ------------------------------------ | ------------------------------------- | | 正式 | `https://payment-api.oen.tw` | `https://{merchantId}.oen.tw` | | 測試 | `https://payment-api.testing.oen.tw` | `https://{merchantId}.testing.oen.tw` | 正式環境要先綁定固定 IP 正式環境只接受已綁定的 IP,其他來源一律回 HTTP 403。測試環境沒有這個限制。見[固定 IP 白名單](/developers/ip-allowlist/)。 ## 驗證 [Section titled “驗證”](#驗證) 每個請求都帶: ```http Authorization: Bearer Content-Type: application/json ``` * token 在 CRM 產生,測試與正式環境各一把,不能互用。見[驗證與 token](/developers/authentication/)。 * `Bearer` 要照這個大小寫,中間只有一個空白。 * 有 body 的請求要帶 `merchantId`,而且必須是 token 所屬的網域。 * **只能從伺服器呼叫**。API 不支援瀏覽器跨網域請求,token 也不能放在前端。 ## 回應格式 [Section titled “回應格式”](#回應格式) 成功時 HTTP 200: ```json { "code": "S0000", "data": { }, "message": "" } ``` 失敗時: ```json { "code": "V0001", "data": {}, "message": "must have required property 'orderId'" } ``` * 程式判斷請看 `code`,`message` 的文字可能調整。 * 付款結果不明時是 HTTP 409,另外帶 `"retryable": false`,見 [C026](/api/error-codes/)。 * 個人身分商家被風控拒絕時(`K0001`),另外帶 `detail`。 * 沒有值的欄位不會出現在回應裡。 * 找不到路徑時回 404、不支援的 HTTP method 回 405,body 是純文字,不是上面的 JSON 格式。 * 被正式環境的 IP 白名單擋下時回 403,body 只有 `message` 欄位(通常是 `Forbidden`),沒有 `code`。 ## 欄位驗證 [Section titled “欄位驗證”](#欄位驗證) * body 必須是合法的 JSON,否則回 500 `F0001`。 * 規格裡沒有的欄位會被忽略,不會報錯。所以欄位名稱拼錯時不會有任何提示,請對照各端點的欄位表。例如 Payment API 沒有 `webhookUrl`、`cancelUrl` 這類欄位,付款通知網址在 CRM 設定。 * 驗證失敗回 400 `V0001`,`message` 會列出所有不符合的規則,但不一定有欄位名稱。 ## 分頁 [Section titled “分頁”](#分頁) [查詢交易列表](/api/list-transactions/)與[查詢商店定期購列表](/api/list-subscriptions/)每頁 50 筆。回應的 `page` 是下一頁的分頁標記,原封不動放進下一次請求的 `?page=`;沒有下一頁時是 `null`。標記無效時回 500 `F0001`。 ## 時間 [Section titled “時間”](#時間) * 回應的時間都是 UTC 的 ISO 8601 字串,例如 `2026-09-28T02:40:25.502Z`。 * 你送出的日期(`startDate`、`expectedPayoutDate`)格式是 `yyyy/MM/dd`,以台北日期解讀。 * 定期定額每天台北時間上午 9 點扣款。 ## ID [Section titled “ID”](#id) | 名稱 | 格式 | 用在 | | ----------------------- | --------------------------------------------- | ---------------------------------------------------------------- | | 交易 `id`/`transactionId` | 27 字元英數 | 組結帳頁網址、付款通知、[查詢交易明細](/api/get-transaction/) | | 交易編號 `transactionHid` | `P` + 日期 + 8 碼,共 17 字元,例如 `P20260928AB12CD34` | [退款](/api/refund/)、對帳、CRM 搜尋 | | 定期定額編號 | `S` + 日期 + 8 碼,共 17 字元 | [查詢](/api/get-subscription/)與[取消定期定額](/api/cancel-subscription/) | | `orderId` | 你自己的訂單編號 | [用訂單編號查詢](/api/list-order-transactions/) | ## 冪等性與頻率限制 [Section titled “冪等性與頻率限制”](#冪等性與頻率限制) * Payment API **沒有** `Idempotency-Key`。同樣的請求送兩次,就會建立兩筆交易。逾時或收到 5xx 時,先查詢再決定要不要重送,見[錯誤處理](/developers/errors/)。 * 目前沒有對個別商家限制請求頻率,但請不要高頻率輪詢;日後可能開始限制。 ## 端點一覽 [Section titled “端點一覽”](#端點一覽) | Method | Path | 說明 | | ------ | ---------------------------------------------------------------- | -------------- | | POST | [`/checkout`](/api/checkout/) | 建立單次付款 | | POST | [`/checkout-subscription`](/api/checkout-subscription/) | 建立定期定額 | | POST | [`/checkout-schedule`](/api/checkout-schedule/) | 建立預約定期定額 | | POST | [`/checkout-token`](/api/checkout-token/) | 建立綁卡頁 | | POST | [`/token/transactions`](/api/token-transactions/) | 用 token 扣款 | | POST | [`/token/subscriptions`](/api/token-subscriptions/) | 用 token 建立定期定額 | | GET | [`/transactions/{id}`](/api/get-transaction/) | 查詢交易明細 | | GET | [`/transactions`](/api/list-transactions/) | 查詢交易列表 | | GET | [`/order/{orderId}/transactions`](/api/list-order-transactions/) | 用訂單編號查詢交易 | | POST | [`/refunds/{transactionHid}`](/api/refund/) | 退款 | | GET | [`/subscriptions/{id}`](/api/get-subscription/) | 查詢定期定額明細 | | PUT | [`/subscriptions/{subscriptionHid}`](/api/cancel-subscription/) | 取消定期定額 | | GET | [`/subscriptions`](/api/list-subscriptions/) | 查詢商店定期購訂單列表 | 需要傳送完整卡號的 API 必須符合 PCI DSS 規範,不在本站公開。若你的情境確實需要,請聯絡業務人員個別討論。 # 取消定期定額 > 取消一筆定期定額,之後不再扣款。 PUT `/subscriptions/:subscriptionHid` * 正式環境:`https://payment-api.oen.tw/subscriptions/:subscriptionHid` * 測試環境:`https://payment-api.testing.oen.tw/subscriptions/:subscriptionHid` 取消一筆 Payment API 定期定額,之後不會再扣款。只能取消進行中、已排程、扣款失敗待重扣的定期定額。**請一定要送 body**,至少帶 `merchantId`。 Header 帶 `Authorization: Bearer ` 與 `Content-Type: application/json` 。token 的取得方式見[驗證與 token](/developers/authentication/)。 ## 請求 [Section titled “請求”](#請求) ### 路徑參數 [Section titled “路徑參數”](#路徑參數) | 欄位 | 型別 | 必填 | 說明 | | ----------------- | -------- | -- | ------------------------------ | | `subscriptionHid` | `string` | 必填 | `S` 開頭的定期定額編號(17 字元)。不接受內部 id。 | ### Body 欄位 [Section titled “Body 欄位”](#body-欄位) | 欄位 | 型別 | 必填 | 說明 | | ------------ | -------- | -- | ------------------------------------------------------------------------------------------ | | `merchantId` | `string` | 必填 | 你的網域名稱。例如應援頁是 `https://ming.oen.tw`,就填 `ming`。必須與 token 所屬的網域相同,不同會回 400 `V0001`。範例:`ming` | | `reason` | `string` | 選填 | 取消原因。 | ### 請求範例 [Section titled “請求範例”](#請求範例) 範例一律打測試環境。把 `OEN_API_TOKEN` 設成測試環境 CRM 產生的 token。 * cURL ```bash curl -X PUT "https://payment-api.testing.oen.tw/subscriptions/S20260928EF56GH78" \ -H "Authorization: Bearer $OEN_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "merchantId": "ming", "reason": "會員申請取消" }' ``` * Node.js ```js const res = await fetch("https://payment-api.testing.oen.tw/subscriptions/S20260928EF56GH78", { method: "PUT", headers: { Authorization: `Bearer ${process.env.OEN_API_TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify({ "merchantId": "ming", "reason": "會員申請取消" }), }); const result = await res.json(); if (result.code !== "S0000") { throw new Error(`${result.code} ${result.message}`); } ``` * PHP ```php 'PUT', CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('OEN_API_TOKEN'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'merchantId' => 'ming', 'reason' => '會員申請取消', ]), ]); $result = json_decode(curl_exec($ch), true); if ($result['code'] !== 'S0000') { throw new Exception($result['code'] . ' ' . $result['message']); } ``` * Python ```python import os import requests res = requests.request( "PUT", "https://payment-api.testing.oen.tw/subscriptions/S20260928EF56GH78", headers={"Authorization": f"Bearer {os.environ['OEN_API_TOKEN']}"}, json={ "merchantId": "ming", "reason": "會員申請取消", }, timeout=30, ) result = res.json() if result["code"] != "S0000": raise RuntimeError(f"{result['code']} {result['message']}") ``` ## 回應 [Section titled “回應”](#回應) | HTTP | 說明 | | ----- | --------------- | | `200` | 取消成功,回傳更新後的定期定額 | | `400` | 找不到,或目前狀態不能取消 | | `401` | token 錯誤 | ### 回應範例 [Section titled “回應範例”](#回應範例) ```json { "code": "S0000", "data": { "id": "S20260928EF56GH78", "status": "cancelled", "period": 1, "paymentInterval": 1, "startedAt": "2026-09-28T02:31:46.640Z", "cancelledAt": "2026-09-28T02:35:00.000Z", "reason": "會員申請取消", "createdAt": "2026-09-28T02:31:46.640Z" }, "message": "" } ``` ## 可能的錯誤 [Section titled “可能的錯誤”](#可能的錯誤) | 錯誤碼 | HTTP | 什麼時候會發生 | | ------- | ---- | -------------------------------------------- | | `V0001` | 400 | 找不到定期定額、不屬於你的網域,或用了內部 id | | `V0002` | 400 | 目前狀態不能取消(例如消費者還沒完成結帳、已取消、已完成、已停止),或取消當下剛好在扣款 | | `A0001` | 401 | token 錯誤 | 完整清單與處理方式見[錯誤碼一覽](/api/error-codes/)。 ## 相關說明 [Section titled “相關說明”](#相關說明) 消費者提出取消時就應該取消,見[定期定額](/developers/subscriptions/)。 # 建立單次付款 > 建立單次付款的結帳頁,把消費者導到應援結帳頁付款。 POST `/checkout` * 正式環境:`https://payment-api.oen.tw/checkout` * 測試環境:`https://payment-api.testing.oen.tw/checkout` 建立一筆單次付款,回傳交易的 `id`。把消費者導到 `https://{merchantId}.oen.tw/checkout/{id}`(測試環境是 `{merchantId}.testing.oen.tw`)付款。**結帳頁從呼叫這支 API 起 5 分鐘內有效**,請在消費者按下付款時才建立。 Header 帶 `Authorization: Bearer ` 與 `Content-Type: application/json` 。token 的取得方式見[驗證與 token](/developers/authentication/)。 ## 請求 [Section titled “請求”](#請求) ### Body 欄位 [Section titled “Body 欄位”](#body-欄位) | 欄位 | 型別 | 必填 | 說明 | | ------------------------------------------------ | ---------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `merchantId` | `string` | 必填 | 你的網域名稱。例如應援頁是 `https://ming.oen.tw`,就填 `ming`。必須與 token 所屬的網域相同,不同會回 400 `V0001`。範例:`ming` | | `amount` | `integer` | 必填 | 金額(新台幣元)。必須等於 `productDetails` 各品項「數量 × 單價」的合計。限制:1 以上的整數範例:`1200` | | `currency` | `string` | 選填 | 幣別。目前只支援新台幣。可用值:`TWD`預設:`TWD` | | `orderId` | `string` | 必填 | 你的訂單編號。之後可以用[用訂單編號查詢](/api/list-order-transactions/)找到這筆訂單的所有交易。限制:不檢查重複。同一個 `orderId` 可以建立多筆交易範例:`A20260928001` | | `successUrl` | `string (uri) \| null` | 必填 | 付款成功後把消費者導回的網址。**導回時不帶任何參數**,請在網址裡放自己的訂單編號,例如 `https://shop.example.com/thanks?order=A001`。導回不代表付款成功,請以付款通知或查詢結果為準。限制:要寫完整網址(含 `https://`);可以是 `null`,但欄位一定要有 | | `failureUrl` | `string (uri) \| null` | 必填 | 付款失敗或逾時後導回的網址,會加上 `payment_error` 參數,可能的值見[錯誤碼一覽](/api/error-codes/#結帳頁導回的-payment_error)。限制:同 `successUrl`。建議不要自帶 `?` 參數與 `#` 片段 | | `productDetails` | `array` | 必填 | 商品明細,用來開立電子發票與顯示在 CRM。**所有品項「數量 × 單價」的合計必須等於 `amount`**,否則回 400 `V0001`(`PRODUCT_AMOUNT_NOT_MATCH`)。 | | `productionCode`productDetails\[].productionCode | `string` | 必填 | 商品代碼 | | `description`productDetails\[].description | `string` | 必填 | 商品名稱。第一個品項的名稱也會當成交易的商品描述 | | `quantity`productDetails\[].quantity | `integer` | 必填 | 數量限制:1 以上的整數 | | `unit`productDetails\[].unit | `string` | 必填 | 單位,例如「個」「份」 | | `unitPrice`productDetails\[].unitPrice | `integer` | 必填 | 單價(新台幣元) | | `allowedPaymentMethods` | `array` | 選填 | 結帳頁可以選的付款方式。**信用卡一定會出現**,這裡是「加開」其他方式:`['cvs']` 會顯示信用卡與超商。沒帶就只有信用卡。Apple Pay 不用指定,條件符合時自動出現。詳見[付款方式](/developers/payment-methods/)。可用值:`card`、`cvs`、`linePay`、`atm`範例:`["cvs"]` | | `userId` | `string` | 選填 | 你系統裡的會員編號。 | | `userName` | `string` | 條件必填 | 消費者姓名。網域有開通電子發票時必填。 | | `userEmail` | `string (email)` | 條件必填 | 消費者 Email。網域有開通電子發票時必填,發票通知會寄到這裡。 | | `invoiceInfo` | `object` | 選填 | 電子發票資訊。網域有開通電子發票時才有作用;沒帶時開立雲端發票。 | | `invoiceType`invoiceInfo.invoiceType | `string` | 必填 | `cloud`:雲端發票;`company`:公司戶(打統編)可用值:`cloud`、`company` | | `carrierType`invoiceInfo.carrierType | `string` | 雲端發票必填 | 載具類型:`3J0002` 手機條碼、`CQ0001` 自然人憑證、空字串為會員載具可用值:`3J0002`、`CQ0001`、`` | | `carrierId`invoiceInfo.carrierId | `string` | 選填 | 載具號碼。手機條碼格式為 `/` 加 7 碼,會即時向財政部驗證;自然人憑證為 2 個英文字母加 14 碼數字 | | `buyerIdentifier`invoiceInfo.buyerIdentifier | `string` | 公司戶必填 | 買受人統一編號,8 碼,會檢查檢查碼 | | `buyerName`invoiceInfo.buyerName | `string` | 選填 | 買受人名稱 | | `email`invoiceInfo.email | `string (email)` | 選填 | 發票通知 Email | | `customId` | `string` | 選填 | 你自訂的資料,會原樣出現在付款通知與查詢結果中。 | | `note` | `string` | 選填 | 備註。 | | `use3d` | `boolean` | 選填 | 是否走 3D 驗證。網域被設定為一律 3D 時會強制開啟。預設:`false` | | `expectedPayoutDate` | `string` | 選填 | 期望撥款日期(台北日期),會顯示在 CRM 金流明細。限制:`yyyy/MM/dd` | ### 請求範例 [Section titled “請求範例”](#請求範例) 範例一律打測試環境。把 `OEN_API_TOKEN` 設成測試環境 CRM 產生的 token。 * cURL ```bash curl -X POST "https://payment-api.testing.oen.tw/checkout" \ -H "Authorization: Bearer $OEN_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "merchantId": "ming", "amount": 1200, "currency": "TWD", "orderId": "A20260928001", "successUrl": "https://shop.example.com/payment/success?order=A20260928001", "failureUrl": "https://shop.example.com/payment/failure/A20260928001", "userName": "王小明", "userEmail": "ming@example.com", "productDetails": [ { "productionCode": "SKU-001", "description": "手沖咖啡豆 200g", "quantity": 2, "unit": "包", "unitPrice": 600 } ], "allowedPaymentMethods": [ "cvs" ], "customId": "cart-8812" }' ``` * Node.js ```js const res = await fetch("https://payment-api.testing.oen.tw/checkout", { method: "POST", headers: { Authorization: `Bearer ${process.env.OEN_API_TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify({ "merchantId": "ming", "amount": 1200, "currency": "TWD", "orderId": "A20260928001", "successUrl": "https://shop.example.com/payment/success?order=A20260928001", "failureUrl": "https://shop.example.com/payment/failure/A20260928001", "userName": "王小明", "userEmail": "ming@example.com", "productDetails": [ { "productionCode": "SKU-001", "description": "手沖咖啡豆 200g", "quantity": 2, "unit": "包", "unitPrice": 600 } ], "allowedPaymentMethods": [ "cvs" ], "customId": "cart-8812" }), }); const result = await res.json(); if (result.code !== "S0000") { throw new Error(`${result.code} ${result.message}`); } ``` * PHP ```php 'POST', CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('OEN_API_TOKEN'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'merchantId' => 'ming', 'amount' => 1200, 'currency' => 'TWD', 'orderId' => 'A20260928001', 'successUrl' => 'https://shop.example.com/payment/success?order=A20260928001', 'failureUrl' => 'https://shop.example.com/payment/failure/A20260928001', 'userName' => '王小明', 'userEmail' => 'ming@example.com', 'productDetails' => [ [ 'productionCode' => 'SKU-001', 'description' => '手沖咖啡豆 200g', 'quantity' => 2, 'unit' => '包', 'unitPrice' => 600, ], ], 'allowedPaymentMethods' => [ 'cvs', ], 'customId' => 'cart-8812', ]), ]); $result = json_decode(curl_exec($ch), true); if ($result['code'] !== 'S0000') { throw new Exception($result['code'] . ' ' . $result['message']); } ``` * Python ```python import os import requests res = requests.request( "POST", "https://payment-api.testing.oen.tw/checkout", headers={"Authorization": f"Bearer {os.environ['OEN_API_TOKEN']}"}, json={ "merchantId": "ming", "amount": 1200, "currency": "TWD", "orderId": "A20260928001", "successUrl": "https://shop.example.com/payment/success?order=A20260928001", "failureUrl": "https://shop.example.com/payment/failure/A20260928001", "userName": "王小明", "userEmail": "ming@example.com", "productDetails": [ { "productionCode": "SKU-001", "description": "手沖咖啡豆 200g", "quantity": 2, "unit": "包", "unitPrice": 600, }, ], "allowedPaymentMethods": [ "cvs", ], "customId": "cart-8812", }, timeout=30, ) result = res.json() if result["code"] != "S0000": raise RuntimeError(f"{result['code']} {result['message']}") ``` ## 回應 [Section titled “回應”](#回應) | HTTP | 說明 | | ----- | ----------------------------------------------------- | | `200` | 建立成功。`data.id` 用來組結帳頁網址,`data.transactionHid` 用來退款與對帳 | | `400` | 參數錯誤、金流未開通或風控拒絕,見下方錯誤表 | | `401` | token 錯誤,回 `A0001` | | `403` | 正式環境的 IP 還沒綁定,見[固定 IP 白名單](/developers/ip-allowlist/) | ### 回應欄位 [Section titled “回應欄位”](#回應欄位) | 欄位 | 型別 | 出現 | 說明 | | ---------------- | -------- | --- | ------------------------------------- | | `id` | `string` | 一定有 | 交易的內部 id(27 字元),組結帳頁網址用,也是付款通知裡的 `id` | | `transactionHid` | `string` | 一定有 | 交易編號,`P` 開頭共 17 字元。退款要用這個 | ### 回應範例 [Section titled “回應範例”](#回應範例) ```json { "code": "S0000", "data": { "id": "2HhndgEquCbDzC5OyVxWSGZmd2l", "transactionHid": "P20260928AB12CD34" }, "message": "" } ``` ## 可能的錯誤 [Section titled “可能的錯誤”](#可能的錯誤) | 錯誤碼 | HTTP | 什麼時候會發生 | | ------- | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `V0001` | 400 | 欄位不符規則;`merchantId` 與 token 不符;品項合計不等於金額(`PRODUCT_AMOUNT_NOT_MATCH`);開通電子發票但缺姓名或 Email(`USER_NAME_AND_EMAIL_REQUIRED`);發票資訊錯誤;指定 `linePay` 但 LINE Pay 未開通(`LINE_PAY_NOT_ACTIVE`) | | `V0002` | 400 | 網域的金流服務尚未開通(`PAYMENT_SERVICE_NOT_ACTIVATE`) | | `K0001` | 400 | 個人身分商家的交易金額超過風控門檻 | | `A0001` | 401 | token 錯誤或已被重新產生 | | `F0001` | 500 | 系統錯誤,或 body 不是合法的 JSON | 完整清單與處理方式見[錯誤碼一覽](/api/error-codes/)。 ## 相關說明 [Section titled “相關說明”](#相關說明) 流程與注意事項見[單次付款](/developers/one-time/)。付款方式的組合見[付款方式](/developers/payment-methods/)。 # 建立預約定期定額 > 建立可以指定首期扣款日與扣款間隔的定期定額結帳頁。 POST `/checkout-schedule` * 正式環境:`https://payment-api.oen.tw/checkout-schedule` * 測試環境:`https://payment-api.testing.oen.tw/checkout-schedule` 建立可以指定首期扣款日與扣款間隔的定期定額,回傳 `id` 與定期定額編號 `subscriptionHid`。把消費者導到 `https://{merchantId}.oen.tw/checkout/schedule/{id}`。首期日是今天時,消費者付款當下扣第一期;是未來日期時,消費者只綁卡,到期由應援扣款。結帳頁 5 分鐘內有效。 Header 帶 `Authorization: Bearer ` 與 `Content-Type: application/json` 。token 的取得方式見[驗證與 token](/developers/authentication/)。 ## 請求 [Section titled “請求”](#請求) ### Body 欄位 [Section titled “Body 欄位”](#body-欄位) | 欄位 | 型別 | 必填 | 說明 | | ------------------------------------------------ | ---------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `merchantId` | `string` | 必填 | 你的網域名稱。例如應援頁是 `https://ming.oen.tw`,就填 `ming`。必須與 token 所屬的網域相同,不同會回 400 `V0001`。範例:`ming` | | `amount` | `integer` | 必填 | 每期金額(新台幣元)。必須等於 `productDetails` 的合計。限制:1 以上的整數範例:`1200` | | `currency` | `string` | 選填 | 幣別。目前只支援新台幣。可用值:`TWD`預設:`TWD` | | `numberOfPeriods` | `integer` | 選填 | 總期數。不帶就是不限期,直到取消。限制:2 以上 | | `paymentInterval` | `integer` | 選填 | 每幾個月扣款一次。限制:1 到 12預設:`1` | | `startDate` | `string \| null` | 選填 | 首期扣款日(台北日期)。不帶就是今天。限制:`yyyy/MM/dd`;今天到 12 個月內範例:`2026/10/15` | | `orderId` | `string` | 必填 | 你的訂單編號。之後可以用[用訂單編號查詢](/api/list-order-transactions/)找到這筆訂單的所有交易。限制:不檢查重複。同一個 `orderId` 可以建立多筆交易範例:`A20260928001` | | `successUrl` | `string (uri) \| null` | 必填 | 付款成功後把消費者導回的網址。**導回時不帶任何參數**,請在網址裡放自己的訂單編號,例如 `https://shop.example.com/thanks?order=A001`。導回不代表付款成功,請以付款通知或查詢結果為準。限制:要寫完整網址(含 `https://`);可以是 `null`,但欄位一定要有 | | `failureUrl` | `string (uri) \| null` | 必填 | 付款失敗或逾時後導回的網址,會加上 `payment_error` 參數,可能的值見[錯誤碼一覽](/api/error-codes/#結帳頁導回的-payment_error)。限制:同 `successUrl`。建議不要自帶 `?` 參數與 `#` 片段 | | `productDetails` | `array` | 必填 | 商品明細,用來開立電子發票與顯示在 CRM。**所有品項「數量 × 單價」的合計必須等於 `amount`**,否則回 400 `V0001`(`PRODUCT_AMOUNT_NOT_MATCH`)。 | | `productionCode`productDetails\[].productionCode | `string` | 必填 | 商品代碼 | | `description`productDetails\[].description | `string` | 必填 | 商品名稱。第一個品項的名稱也會當成交易的商品描述 | | `quantity`productDetails\[].quantity | `integer` | 必填 | 數量限制:1 以上的整數 | | `unit`productDetails\[].unit | `string` | 必填 | 單位,例如「個」「份」 | | `unitPrice`productDetails\[].unitPrice | `integer` | 必填 | 單價(新台幣元) | | `userId` | `string` | 選填 | 你系統裡的會員編號。 | | `userName` | `string` | 條件必填 | 消費者姓名。網域有開通電子發票時必填。 | | `userEmail` | `string (email)` | 條件必填 | 消費者 Email。網域有開通電子發票時必填,發票通知會寄到這裡。 | | `invoiceInfo` | `object` | 選填 | 電子發票資訊。網域有開通電子發票時才有作用;沒帶時開立雲端發票。 | | `invoiceType`invoiceInfo.invoiceType | `string` | 必填 | `cloud`:雲端發票;`company`:公司戶(打統編)可用值:`cloud`、`company` | | `carrierType`invoiceInfo.carrierType | `string` | 雲端發票必填 | 載具類型:`3J0002` 手機條碼、`CQ0001` 自然人憑證、空字串為會員載具可用值:`3J0002`、`CQ0001`、`` | | `carrierId`invoiceInfo.carrierId | `string` | 選填 | 載具號碼。手機條碼格式為 `/` 加 7 碼,會即時向財政部驗證;自然人憑證為 2 個英文字母加 14 碼數字 | | `buyerIdentifier`invoiceInfo.buyerIdentifier | `string` | 公司戶必填 | 買受人統一編號,8 碼,會檢查檢查碼 | | `buyerName`invoiceInfo.buyerName | `string` | 選填 | 買受人名稱 | | `email`invoiceInfo.email | `string (email)` | 選填 | 發票通知 Email | | `customId` | `string` | 選填 | 你自訂的資料,會原樣出現在付款通知與查詢結果中。 | | `note` | `string` | 選填 | 備註。 | | `use3d` | `boolean` | 選填 | 是否走 3D 驗證。網域被設定為一律 3D 時會強制開啟。預設:`false` | ### 請求範例 [Section titled “請求範例”](#請求範例) 範例一律打測試環境。把 `OEN_API_TOKEN` 設成測試環境 CRM 產生的 token。 * cURL ```bash curl -X POST "https://payment-api.testing.oen.tw/checkout-schedule" \ -H "Authorization: Bearer $OEN_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "merchantId": "ming", "amount": 1500, "numberOfPeriods": 4, "paymentInterval": 3, "startDate": "2026/10/15", "orderId": "SUB20260928002", "successUrl": "https://shop.example.com/subscribe/success?order=SUB20260928002", "failureUrl": "https://shop.example.com/subscribe/failure/SUB20260928002", "userName": "王小明", "userEmail": "ming@example.com", "productDetails": [ { "productionCode": "BOX-Q", "description": "季訂閱禮盒", "quantity": 1, "unit": "盒", "unitPrice": 1500 } ] }' ``` * Node.js ```js const res = await fetch("https://payment-api.testing.oen.tw/checkout-schedule", { method: "POST", headers: { Authorization: `Bearer ${process.env.OEN_API_TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify({ "merchantId": "ming", "amount": 1500, "numberOfPeriods": 4, "paymentInterval": 3, "startDate": "2026/10/15", "orderId": "SUB20260928002", "successUrl": "https://shop.example.com/subscribe/success?order=SUB20260928002", "failureUrl": "https://shop.example.com/subscribe/failure/SUB20260928002", "userName": "王小明", "userEmail": "ming@example.com", "productDetails": [ { "productionCode": "BOX-Q", "description": "季訂閱禮盒", "quantity": 1, "unit": "盒", "unitPrice": 1500 } ] }), }); const result = await res.json(); if (result.code !== "S0000") { throw new Error(`${result.code} ${result.message}`); } ``` * PHP ```php 'POST', CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('OEN_API_TOKEN'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'merchantId' => 'ming', 'amount' => 1500, 'numberOfPeriods' => 4, 'paymentInterval' => 3, 'startDate' => '2026/10/15', 'orderId' => 'SUB20260928002', 'successUrl' => 'https://shop.example.com/subscribe/success?order=SUB20260928002', 'failureUrl' => 'https://shop.example.com/subscribe/failure/SUB20260928002', 'userName' => '王小明', 'userEmail' => 'ming@example.com', 'productDetails' => [ [ 'productionCode' => 'BOX-Q', 'description' => '季訂閱禮盒', 'quantity' => 1, 'unit' => '盒', 'unitPrice' => 1500, ], ], ]), ]); $result = json_decode(curl_exec($ch), true); if ($result['code'] !== 'S0000') { throw new Exception($result['code'] . ' ' . $result['message']); } ``` * Python ```python import os import requests res = requests.request( "POST", "https://payment-api.testing.oen.tw/checkout-schedule", headers={"Authorization": f"Bearer {os.environ['OEN_API_TOKEN']}"}, json={ "merchantId": "ming", "amount": 1500, "numberOfPeriods": 4, "paymentInterval": 3, "startDate": "2026/10/15", "orderId": "SUB20260928002", "successUrl": "https://shop.example.com/subscribe/success?order=SUB20260928002", "failureUrl": "https://shop.example.com/subscribe/failure/SUB20260928002", "userName": "王小明", "userEmail": "ming@example.com", "productDetails": [ { "productionCode": "BOX-Q", "description": "季訂閱禮盒", "quantity": 1, "unit": "盒", "unitPrice": 1500, }, ], }, timeout=30, ) result = res.json() if result["code"] != "S0000": raise RuntimeError(f"{result['code']} {result['message']}") ``` ## 回應 [Section titled “回應”](#回應) | HTTP | 說明 | | ----- | -------- | | `200` | 建立成功 | | `400` | 參數錯誤 | | `401` | token 錯誤 | ### 回應欄位 [Section titled “回應欄位”](#回應欄位) | 欄位 | 型別 | 出現 | 說明 | | ----------------- | -------- | --- | ---------------------------------- | | `id` | `string` | 一定有 | 定期定額的內部 id,組結帳頁網址用 | | `subscriptionHid` | `string` | 一定有 | 定期定額編號,`S` 開頭共 17 字元。查詢與取消定期定額都用這個 | ### 回應範例 [Section titled “回應範例”](#回應範例) ```json { "code": "S0000", "data": { "id": "2HhnfZ1kqRmA7cX0TQwq8vBn3Ls", "subscriptionHid": "S20260928EF56GH78" }, "message": "" } ``` ## 可能的錯誤 [Section titled “可能的錯誤”](#可能的錯誤) | 錯誤碼 | HTTP | 什麼時候會發生 | | ------- | ---- | -------------------------------------------------------------------------------------------------------------- | | `V0001` | 400 | 欄位不符規則;首期日早於今天(`START_DATE_MUST_GREATER_THAN_TODAY`)或超過 12 個月(`START_DATE_MUST_LESS_THAN_12_MONTHS`);品項合計不等於金額 | | `K0001` | 400 | 個人身分商家的交易金額超過風控門檻 | | `A0001` | 401 | token 錯誤 | 完整清單與處理方式見[錯誤碼一覽](/api/error-codes/)。 ## 相關說明 [Section titled “相關說明”](#相關說明) 跟「建立定期定額」的差別見[定期定額](/developers/subscriptions/)。 # 建立定期定額 > 建立每月扣款的定期定額結帳頁。 POST `/checkout-subscription` * 正式環境:`https://payment-api.oen.tw/checkout-subscription` * 測試環境:`https://payment-api.testing.oen.tw/checkout-subscription` 建立每月扣款的定期定額,回傳交易的 `id`。把消費者導到 `https://{merchantId}.oen.tw/checkout/subscription/{id}`。消費者在結帳頁付款時扣第一期,之後每個月同一天由應援自動扣款。只支援信用卡。結帳頁 5 分鐘內有效。 Header 帶 `Authorization: Bearer ` 與 `Content-Type: application/json` 。token 的取得方式見[驗證與 token](/developers/authentication/)。 ## 請求 [Section titled “請求”](#請求) ### Body 欄位 [Section titled “Body 欄位”](#body-欄位) | 欄位 | 型別 | 必填 | 說明 | | ------------------------------------------------ | ---------------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `merchantId` | `string` | 必填 | 你的網域名稱。例如應援頁是 `https://ming.oen.tw`,就填 `ming`。必須與 token 所屬的網域相同,不同會回 400 `V0001`。範例:`ming` | | `amount` | `integer` | 必填 | 每期金額(新台幣元)。必須等於 `productDetails` 的合計。限制:1 以上的整數範例:`1200` | | `currency` | `string` | 選填 | 幣別。目前只支援新台幣。可用值:`TWD`預設:`TWD` | | `numberOfPeriods` | `integer` | 選填 | 總期數。不帶就是不限期,直到取消。限制:2 以上 | | `orderId` | `string` | 必填 | 你的訂單編號。之後可以用[用訂單編號查詢](/api/list-order-transactions/)找到這筆訂單的所有交易。限制:不檢查重複。同一個 `orderId` 可以建立多筆交易範例:`A20260928001` | | `successUrl` | `string (uri) \| null` | 必填 | 付款成功後把消費者導回的網址。**導回時不帶任何參數**,請在網址裡放自己的訂單編號,例如 `https://shop.example.com/thanks?order=A001`。導回不代表付款成功,請以付款通知或查詢結果為準。限制:要寫完整網址(含 `https://`);可以是 `null`,但欄位一定要有 | | `failureUrl` | `string (uri) \| null` | 必填 | 付款失敗或逾時後導回的網址,會加上 `payment_error` 參數,可能的值見[錯誤碼一覽](/api/error-codes/#結帳頁導回的-payment_error)。限制:同 `successUrl`。建議不要自帶 `?` 參數與 `#` 片段 | | `productDetails` | `array` | 必填 | 商品明細,用來開立電子發票與顯示在 CRM。**所有品項「數量 × 單價」的合計必須等於 `amount`**,否則回 400 `V0001`(`PRODUCT_AMOUNT_NOT_MATCH`)。 | | `productionCode`productDetails\[].productionCode | `string` | 必填 | 商品代碼 | | `description`productDetails\[].description | `string` | 必填 | 商品名稱。第一個品項的名稱也會當成交易的商品描述 | | `quantity`productDetails\[].quantity | `integer` | 必填 | 數量限制:1 以上的整數 | | `unit`productDetails\[].unit | `string` | 必填 | 單位,例如「個」「份」 | | `unitPrice`productDetails\[].unitPrice | `integer` | 必填 | 單價(新台幣元) | | `userId` | `string` | 選填 | 你系統裡的會員編號。 | | `userName` | `string` | 條件必填 | 消費者姓名。網域有開通電子發票時必填。 | | `userEmail` | `string (email)` | 條件必填 | 消費者 Email。網域有開通電子發票時必填,發票通知會寄到這裡。 | | `invoiceInfo` | `object` | 選填 | 電子發票資訊。網域有開通電子發票時才有作用;沒帶時開立雲端發票。 | | `invoiceType`invoiceInfo.invoiceType | `string` | 必填 | `cloud`:雲端發票;`company`:公司戶(打統編)可用值:`cloud`、`company` | | `carrierType`invoiceInfo.carrierType | `string` | 雲端發票必填 | 載具類型:`3J0002` 手機條碼、`CQ0001` 自然人憑證、空字串為會員載具可用值:`3J0002`、`CQ0001`、`` | | `carrierId`invoiceInfo.carrierId | `string` | 選填 | 載具號碼。手機條碼格式為 `/` 加 7 碼,會即時向財政部驗證;自然人憑證為 2 個英文字母加 14 碼數字 | | `buyerIdentifier`invoiceInfo.buyerIdentifier | `string` | 公司戶必填 | 買受人統一編號,8 碼,會檢查檢查碼 | | `buyerName`invoiceInfo.buyerName | `string` | 選填 | 買受人名稱 | | `email`invoiceInfo.email | `string (email)` | 選填 | 發票通知 Email | | `customId` | `string` | 選填 | 你自訂的資料,會原樣出現在付款通知與查詢結果中。 | | `note` | `string` | 選填 | 備註。 | | `use3d` | `boolean` | 選填 | 是否走 3D 驗證。網域被設定為一律 3D 時會強制開啟。預設:`false` | ### 請求範例 [Section titled “請求範例”](#請求範例) 範例一律打測試環境。把 `OEN_API_TOKEN` 設成測試環境 CRM 產生的 token。 * cURL ```bash curl -X POST "https://payment-api.testing.oen.tw/checkout-subscription" \ -H "Authorization: Bearer $OEN_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "merchantId": "ming", "amount": 299, "numberOfPeriods": 12, "orderId": "SUB20260928001", "successUrl": "https://shop.example.com/subscribe/success?order=SUB20260928001", "failureUrl": "https://shop.example.com/subscribe/failure/SUB20260928001", "userName": "王小明", "userEmail": "ming@example.com", "productDetails": [ { "productionCode": "PLAN-M", "description": "月費會員", "quantity": 1, "unit": "月", "unitPrice": 299 } ] }' ``` * Node.js ```js const res = await fetch("https://payment-api.testing.oen.tw/checkout-subscription", { method: "POST", headers: { Authorization: `Bearer ${process.env.OEN_API_TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify({ "merchantId": "ming", "amount": 299, "numberOfPeriods": 12, "orderId": "SUB20260928001", "successUrl": "https://shop.example.com/subscribe/success?order=SUB20260928001", "failureUrl": "https://shop.example.com/subscribe/failure/SUB20260928001", "userName": "王小明", "userEmail": "ming@example.com", "productDetails": [ { "productionCode": "PLAN-M", "description": "月費會員", "quantity": 1, "unit": "月", "unitPrice": 299 } ] }), }); const result = await res.json(); if (result.code !== "S0000") { throw new Error(`${result.code} ${result.message}`); } ``` * PHP ```php 'POST', CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('OEN_API_TOKEN'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'merchantId' => 'ming', 'amount' => 299, 'numberOfPeriods' => 12, 'orderId' => 'SUB20260928001', 'successUrl' => 'https://shop.example.com/subscribe/success?order=SUB20260928001', 'failureUrl' => 'https://shop.example.com/subscribe/failure/SUB20260928001', 'userName' => '王小明', 'userEmail' => 'ming@example.com', 'productDetails' => [ [ 'productionCode' => 'PLAN-M', 'description' => '月費會員', 'quantity' => 1, 'unit' => '月', 'unitPrice' => 299, ], ], ]), ]); $result = json_decode(curl_exec($ch), true); if ($result['code'] !== 'S0000') { throw new Exception($result['code'] . ' ' . $result['message']); } ``` * Python ```python import os import requests res = requests.request( "POST", "https://payment-api.testing.oen.tw/checkout-subscription", headers={"Authorization": f"Bearer {os.environ['OEN_API_TOKEN']}"}, json={ "merchantId": "ming", "amount": 299, "numberOfPeriods": 12, "orderId": "SUB20260928001", "successUrl": "https://shop.example.com/subscribe/success?order=SUB20260928001", "failureUrl": "https://shop.example.com/subscribe/failure/SUB20260928001", "userName": "王小明", "userEmail": "ming@example.com", "productDetails": [ { "productionCode": "PLAN-M", "description": "月費會員", "quantity": 1, "unit": "月", "unitPrice": 299, }, ], }, timeout=30, ) result = res.json() if result["code"] != "S0000": raise RuntimeError(f"{result['code']} {result['message']}") ``` ## 回應 [Section titled “回應”](#回應) | HTTP | 說明 | | ----- | ---------- | | `200` | 建立成功 | | `400` | 參數錯誤或金流未開通 | | `401` | token 錯誤 | ### 回應欄位 [Section titled “回應欄位”](#回應欄位) | 欄位 | 型別 | 出現 | 說明 | | ---------------- | -------- | --- | ---------------- | | `id` | `string` | 一定有 | 交易的內部 id,組結帳頁網址用 | | `transactionHid` | `string` | 一定有 | 第一期交易的編號(`P` 開頭) | ### 回應範例 [Section titled “回應範例”](#回應範例) ```json { "code": "S0000", "data": { "id": "2HhndgEquCbDzC5OyVxWSGZmd2l", "transactionHid": "P20260928AB12CD34" }, "message": "" } ``` ## 可能的錯誤 [Section titled “可能的錯誤”](#可能的錯誤) | 錯誤碼 | HTTP | 什麼時候會發生 | | ------- | ---- | ----------------------- | | `V0001` | 400 | 欄位不符規則、品項合計不等於金額、發票資訊錯誤 | | `V0002` | 400 | 網域的金流服務尚未開通 | | `K0001` | 400 | 個人身分商家的交易金額超過風控門檻 | | `A0001` | 401 | token 錯誤 | 完整清單與處理方式見[錯誤碼一覽](/api/error-codes/)。 ## 相關說明 [Section titled “相關說明”](#相關說明) 流程、扣款時間與失敗處理見[定期定額](/developers/subscriptions/)。 # 建立綁卡頁 > 建立綁卡頁,消費者完成 3D 驗證後,你會從付款通知拿到 token。 POST `/checkout-token` * 正式環境:`https://payment-api.oen.tw/checkout-token` * 測試環境:`https://payment-api.testing.oen.tw/checkout-token` 建立綁卡頁,回傳 `id`。把消費者導到 `https://{merchantId}.oen.tw/checkout/subscription/create/{id}` 完成 3D 驗證並綁卡。**綁卡頁 10 分鐘內有效。token 只會透過付款通知(`purpose` 為 `token`)送給你**,導回網址不帶 token,也沒有查詢 API。 Header 帶 `Authorization: Bearer ` 與 `Content-Type: application/json` 。token 的取得方式見[驗證與 token](/developers/authentication/)。 ## 請求 [Section titled “請求”](#請求) ### Body 欄位 [Section titled “Body 欄位”](#body-欄位) | 欄位 | 型別 | 必填 | 說明 | | ------------ | ---------------------- | -- | ------------------------------------------------------------------------------------------ | | `merchantId` | `string` | 必填 | 你的網域名稱。例如應援頁是 `https://ming.oen.tw`,就填 `ming`。必須與 token 所屬的網域相同,不同會回 400 `V0001`。範例:`ming` | | `successUrl` | `string (uri) \| null` | 必填 | 綁卡成功後導回的網址。**不帶 token**。限制:要寫完整網址(含 `https://`);可以是 `null`,但欄位一定要有 | | `failureUrl` | `string (uri) \| null` | 必填 | 綁卡失敗或逾時後導回的網址,會加上 `payment_error`。限制:同 `successUrl`。建議不要自帶 `?` 參數與 `#` 片段 | | `customId` | `string` | 選填 | 你自訂的資料,會出現在 token 通知裡,方便你對應是哪一位會員。 | | `note` | `string` | 選填 | 備註。 | | `payerEmail` | `string (email)` | 選填 | 持卡人 Email。沒帶時消費者要在綁卡頁自行填寫。 | ### 請求範例 [Section titled “請求範例”](#請求範例) 範例一律打測試環境。把 `OEN_API_TOKEN` 設成測試環境 CRM 產生的 token。 * cURL ```bash curl -X POST "https://payment-api.testing.oen.tw/checkout-token" \ -H "Authorization: Bearer $OEN_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "merchantId": "ming", "successUrl": "https://shop.example.com/cards/added?member=M001", "failureUrl": "https://shop.example.com/cards/failed/M001", "customId": "M001", "payerEmail": "ming@example.com" }' ``` * Node.js ```js const res = await fetch("https://payment-api.testing.oen.tw/checkout-token", { method: "POST", headers: { Authorization: `Bearer ${process.env.OEN_API_TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify({ "merchantId": "ming", "successUrl": "https://shop.example.com/cards/added?member=M001", "failureUrl": "https://shop.example.com/cards/failed/M001", "customId": "M001", "payerEmail": "ming@example.com" }), }); const result = await res.json(); if (result.code !== "S0000") { throw new Error(`${result.code} ${result.message}`); } ``` * PHP ```php 'POST', CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('OEN_API_TOKEN'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'merchantId' => 'ming', 'successUrl' => 'https://shop.example.com/cards/added?member=M001', 'failureUrl' => 'https://shop.example.com/cards/failed/M001', 'customId' => 'M001', 'payerEmail' => 'ming@example.com', ]), ]); $result = json_decode(curl_exec($ch), true); if ($result['code'] !== 'S0000') { throw new Exception($result['code'] . ' ' . $result['message']); } ``` * Python ```python import os import requests res = requests.request( "POST", "https://payment-api.testing.oen.tw/checkout-token", headers={"Authorization": f"Bearer {os.environ['OEN_API_TOKEN']}"}, json={ "merchantId": "ming", "successUrl": "https://shop.example.com/cards/added?member=M001", "failureUrl": "https://shop.example.com/cards/failed/M001", "customId": "M001", "payerEmail": "ming@example.com", }, timeout=30, ) result = res.json() if result["code"] != "S0000": raise RuntimeError(f"{result['code']} {result['message']}") ``` ## 回應 [Section titled “回應”](#回應) | HTTP | 說明 | | ----- | ---------- | | `200` | 建立成功 | | `400` | 參數錯誤或金流未開通 | | `401` | token 錯誤 | ### 回應欄位 [Section titled “回應欄位”](#回應欄位) | 欄位 | 型別 | 出現 | 說明 | | ---- | -------- | --- | ------------------------------------- | | `id` | `string` | 一定有 | 綁卡請求的 id。組綁卡頁網址用,也會出現在 token 通知的 `id` | ### 回應範例 [Section titled “回應範例”](#回應範例) ```json { "code": "S0000", "data": { "id": "2HhngP9sVb3mK1xYdQe7Wc0RtUi" }, "message": "" } ``` ## 可能的錯誤 [Section titled “可能的錯誤”](#可能的錯誤) | 錯誤碼 | HTTP | 什麼時候會發生 | | ------- | ---- | ----------- | | `V0001` | 400 | 欄位不符規則 | | `V0002` | 400 | 網域的金流服務尚未開通 | | `A0001` | 401 | token 錯誤 | 完整清單與處理方式見[錯誤碼一覽](/api/error-codes/)。 ## 相關說明 [Section titled “相關說明”](#相關說明) 完整流程見[存卡與後續扣款](/developers/saved-cards/)。 # 錯誤碼一覽 > Payment API 回應的所有錯誤碼、HTTP 狀態碼與建議處理方式,以及結帳頁導回 failureUrl 時的 payment_error。 ## API 回應的 code [Section titled “API 回應的 code”](#api-回應的-code) | 錯誤碼 | HTTP | 意思 | 建議處理 | | ------- | ---- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `S0000` | 200 | 成功 | — | | `A0001` | 401 | 未授權:token 缺少、格式錯誤、已被重新產生,或網域沒有開啟 API 串接 | 確認 header 是 `Authorization: Bearer `,以及用對了測試或正式環境的 token。token 被重新產生過就要換新的 | | `V0001` | 400 | 請求參數錯誤 | 依 `message` 修正後重送。欄位驗證錯誤的訊息不含欄位路徑,請對照 API 參考逐欄檢查 | | `V0002` | 400 | 狀態不允許:金流尚未開通,或交易、定期定額目前的狀態不能做這個動作 | 先查詢目前狀態再決定 | | `V0003` | 400 | 超過單筆最高金額(200,000 元以上需要開通高額交易) | 拆成多筆,或聯絡應援開通 | | `T0001` | 400 | 交易失敗(一般原因) | 請消費者換卡或稍後再試 | | `T0002` | 400 | 安全碼錯誤 | 請消費者確認卡片資料 | | `T0003` | 400 | 卡片過期 | 請消費者換卡 | | `T0004` | 400 | 額度不足 | 請消費者換卡或聯絡發卡銀行 | | `T0005` | 400 | 發卡銀行拒絕授權 | 請消費者聯絡發卡銀行或換卡 | | `R0001` | 400 | 退款失敗 | 先查詢交易狀態。之後重試都回 `V0002` 時,代表退款結果需要人工確認,請聯絡應援 | | `K0001` | 400 | 風控拒絕:個人身分商家的當月累計或單筆金額超過門檻 | 回應的 `detail.subCode` 說明原因,請聯絡應援 | | `C026` | 409 | 付款結果不明,回應帶 `"retryable": false` | **不要重試,也不要換一筆新訂單重扣**。稍後用[查詢交易](/api/get-transaction/)或[訂單編號查詢](/api/list-order-transactions/)確認;久未確定請聯絡應援 | | `F0001` | 500 | 系統錯誤或未分類的錯誤 | 看 `message`:`Invalid date`(日期參數錯誤)、`Pagination parse failed`(`page` 分頁標記無效)、`Current charged amount too low`(退款時網域待撥款金額不足)請依原因處理;其他請稍後重試,建立交易類的請求重試前先查詢 | 處理原則見[錯誤處理](/developers/errors/)。 ## 結帳頁導回的 payment_error [Section titled “結帳頁導回的 payment_error”](#結帳頁導回的-payment_error) 消費者付款失敗或逾時時,會被導回你的 `failureUrl`,網址加上 `payment_error=<代碼>`,例如 `https://shop.example.com/payment/failure/A001?payment_error=T0004`。 | 錯誤碼 | HTTP | 意思 | 建議處理 | | ------- | ---- | ----------------------------------- | ------------- | | `V0002` | 導回參數 | 結帳逾時、交易已處理過或找不到交易;未歸類的錯誤也是這個值 | 重新建立交易 | | `V0003` | 導回參數 | 付款結果尚未確定。**和 API 回應的 `V0003` 意思不同** | 等付款通知,或稍後查詢交易 | | `T0001` | 導回參數 | 付款失敗,包含 3D 驗證失敗 | 請消費者換卡或稍後再試 | | `T0002` | 導回參數 | 安全碼錯誤 | 請消費者確認卡片資料 | | `T0003` | 導回參數 | 卡片過期 | 請消費者換卡 | | `T0004` | 導回參數 | 額度不足 | 請消費者換卡 | | `T0005` | 導回參數 | 發卡銀行拒絕 | 請消費者聯絡發卡銀行或換卡 | | `Y003` | 導回參數 | 綁卡頁已逾時(10 分鐘) | 重新建立綁卡頁 | * 導回的代碼只用來顯示訊息給消費者,**請以付款通知或查詢結果為準**。 * LINE Pay 付款失敗或取消時,導回 `failureUrl` 不帶 `payment_error`。 ## 其他 HTTP 狀態 [Section titled “其他 HTTP 狀態”](#其他-http-狀態) | HTTP | 什麼時候 | body | | ---- | --------------------------------------------------------- | ------------------------------------------ | | 403 | 正式環境的呼叫來源 IP 還沒綁定,見[固定 IP 白名單](/developers/ip-allowlist/) | 只有 `message` 欄位(通常是 `Forbidden`),沒有 `code` | | 404 | 路徑不存在 | 純文字 `Not Found` | | 405 | 不支援的 HTTP method | 純文字 `Method Not Allowed` | | 504 | 處理超過 29 秒。**請求可能已經成功**,先查詢再決定要不要重送 | — | # 查詢定期定額明細 > 查詢一筆定期定額的狀態與下次扣款時間。 GET `/subscriptions/:id` * 正式環境:`https://payment-api.oen.tw/subscriptions/:id` * 測試環境:`https://payment-api.testing.oen.tw/subscriptions/:id` 查詢一筆 Payment API 定期定額的狀態、期數與下次扣款時間。 Header 帶 `Authorization: Bearer ` 。token 的取得方式見[驗證與 token](/developers/authentication/)。 ## 請求 [Section titled “請求”](#請求) ### 路徑參數 [Section titled “路徑參數”](#路徑參數) | 欄位 | 型別 | 必填 | 說明 | | ---- | -------- | -- | -------------------------- | | `id` | `string` | 必填 | `S` 開頭的定期定額編號,或定期定額的內部 id。 | ### 請求範例 [Section titled “請求範例”](#請求範例) 範例一律打測試環境。把 `OEN_API_TOKEN` 設成測試環境 CRM 產生的 token。 * cURL ```bash curl -X GET "https://payment-api.testing.oen.tw/subscriptions/S20260928EF56GH78" \ -H "Authorization: Bearer $OEN_API_TOKEN" ``` * Node.js ```js const res = await fetch("https://payment-api.testing.oen.tw/subscriptions/S20260928EF56GH78", { method: "GET", headers: { Authorization: `Bearer ${process.env.OEN_API_TOKEN}`, }, }); const result = await res.json(); if (result.code !== "S0000") { throw new Error(`${result.code} ${result.message}`); } ``` * PHP ```php 'GET', CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('OEN_API_TOKEN'), ], ]); $result = json_decode(curl_exec($ch), true); if ($result['code'] !== 'S0000') { throw new Exception($result['code'] . ' ' . $result['message']); } ``` * Python ```python import os import requests res = requests.request( "GET", "https://payment-api.testing.oen.tw/subscriptions/S20260928EF56GH78", headers={"Authorization": f"Bearer {os.environ['OEN_API_TOKEN']}"}, timeout=30, ) result = res.json() if result["code"] != "S0000": raise RuntimeError(f"{result['code']} {result['message']}") ``` ## 回應 [Section titled “回應”](#回應) | HTTP | 說明 | | ----- | ------------------------------------------------ | | `200` | 回傳 [Subscription 物件](/api/objects/subscription/) | | `400` | 找不到,或不屬於你的網域(`V0001`) | | `401` | token 錯誤 | ### 回應範例 [Section titled “回應範例”](#回應範例) ```json { "code": "S0000", "data": { "id": "S20260928EF56GH78", "status": "ongoing", "period": 1, "numberOfPeriods": 12, "paymentInterval": 1, "startedAt": "2026-09-28T02:44:35.592Z", "endedAt": "2027-08-28T02:44:35.592Z", "nextChargeAt": "2026-10-28T01:00:00.000Z", "createdAt": "2026-09-28T02:44:35.592Z", "amount": 299, "userName": "王小明", "orderId": "SUB20260928001" }, "message": "" } ``` ## 可能的錯誤 [Section titled “可能的錯誤”](#可能的錯誤) | 錯誤碼 | HTTP | 什麼時候會發生 | | ------- | ---- | ---------------- | | `V0001` | 400 | 找不到定期定額,或不屬於你的網域 | | `A0001` | 401 | token 錯誤 | 完整清單與處理方式見[錯誤碼一覽](/api/error-codes/)。 ## 相關說明 [Section titled “相關說明”](#相關說明) 狀態說明見 [Subscription 物件](/api/objects/subscription/)。 # 查詢交易明細 > 查詢一筆交易的最新狀態。 GET `/transactions/:id` * 正式環境:`https://payment-api.oen.tw/transactions/:id` * 測試環境:`https://payment-api.testing.oen.tw/transactions/:id` 查詢一筆交易的最新狀態。收到付款通知後,請用這支回查確認結果再出貨。**請用 27 字元的 `id` 查詢**;用 `P` 開頭的交易編號也查得到,但回應不會有 `productDetails` 與 `numberOfPeriods`。 Header 帶 `Authorization: Bearer ` 。token 的取得方式見[驗證與 token](/developers/authentication/)。 ## 請求 [Section titled “請求”](#請求) ### 路徑參數 [Section titled “路徑參數”](#路徑參數) | 欄位 | 型別 | 必填 | 說明 | | ---- | -------- | -- | ------------------------------------------------------ | | `id` | `string` | 必填 | 交易的內部 id(建立交易時回傳的 `id`,或付款通知裡的 `id`),也可以是 `P` 開頭的交易編號。 | ### 請求範例 [Section titled “請求範例”](#請求範例) 範例一律打測試環境。把 `OEN_API_TOKEN` 設成測試環境 CRM 產生的 token。 * cURL ```bash curl -X GET "https://payment-api.testing.oen.tw/transactions/2HhndgEquCbDzC5OyVxWSGZmd2l" \ -H "Authorization: Bearer $OEN_API_TOKEN" ``` * Node.js ```js const res = await fetch("https://payment-api.testing.oen.tw/transactions/2HhndgEquCbDzC5OyVxWSGZmd2l", { method: "GET", headers: { Authorization: `Bearer ${process.env.OEN_API_TOKEN}`, }, }); const result = await res.json(); if (result.code !== "S0000") { throw new Error(`${result.code} ${result.message}`); } ``` * PHP ```php 'GET', CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('OEN_API_TOKEN'), ], ]); $result = json_decode(curl_exec($ch), true); if ($result['code'] !== 'S0000') { throw new Exception($result['code'] . ' ' . $result['message']); } ``` * Python ```python import os import requests res = requests.request( "GET", "https://payment-api.testing.oen.tw/transactions/2HhndgEquCbDzC5OyVxWSGZmd2l", headers={"Authorization": f"Bearer {os.environ['OEN_API_TOKEN']}"}, timeout=30, ) result = res.json() if result["code"] != "S0000": raise RuntimeError(f"{result['code']} {result['message']}") ``` ## 回應 [Section titled “回應”](#回應) | HTTP | 說明 | | ----- | ---------------------------------------------- | | `200` | 回傳 [Transaction 物件](/api/objects/transaction/) | | `400` | 找不到交易,或交易不屬於你的網域(`V0001`) | | `401` | token 錯誤 | ### 回應範例 [Section titled “回應範例”](#回應範例) ```json { "code": "S0000", "data": { "id": "P20260928AB12CD34", "transactionId": "2HhndgEquCbDzC5OyVxWSGZmd2l", "action": "onetime", "amount": 1200, "paymentMethod": "card", "paymentInfo": { "method": "card", "cardNum": "424242******4242", "cardName": "WANG XIAO MING", "cardType": "Visa", "cardIssuerCountry": "TW" }, "status": "charged", "userName": "王小明", "userEmail": "ming@example.com", "orderId": "A20260928001", "createdAt": "2026-09-28T02:40:25.502Z", "paidAt": "2026-09-28T02:41:03.118Z", "refundAmount": 0, "authCode": "831000", "customId": "cart-8812", "use3d": false, "productDetails": [ { "productionCode": "SKU-001", "description": "手沖咖啡豆 200g", "quantity": 2, "unit": "包", "unitPrice": 600 } ] }, "message": "" } ``` ## 可能的錯誤 [Section titled “可能的錯誤”](#可能的錯誤) | 錯誤碼 | HTTP | 什麼時候會發生 | | ------- | ---- | ---------------- | | `V0001` | 400 | 找不到交易,或交易不屬於你的網域 | | `A0001` | 401 | token 錯誤 | 完整清單與處理方式見[錯誤碼一覽](/api/error-codes/)。 ## 相關說明 [Section titled “相關說明”](#相關說明) 收到付款通知後怎麼回查,見[付款通知](/developers/webhooks/)。欄位說明見 [Transaction 物件](/api/objects/transaction/)。 # 用訂單編號查詢交易 > 列出同一個訂單編號的所有交易。 GET `/order/:orderId/transactions` * 正式環境:`https://payment-api.oen.tw/order/:orderId/transactions` * 測試環境:`https://payment-api.testing.oen.tw/order/:orderId/transactions` 列出同一個 `orderId` 的所有 API 交易,包含失敗的交易與定期定額每一期。一次回傳全部,不分頁,也不保證順序。消費者重試付款、或結帳頁過期後重新建立時,一個訂單可能有多筆交易,請用這支確認有沒有成功的那一筆。 Header 帶 `Authorization: Bearer ` 。token 的取得方式見[驗證與 token](/developers/authentication/)。 ## 請求 [Section titled “請求”](#請求) ### 路徑參數 [Section titled “路徑參數”](#路徑參數) | 欄位 | 型別 | 必填 | 說明 | | --------- | -------- | -- | ------------------ | | `orderId` | `string` | 必填 | 建立交易時帶的 `orderId`。 | ### 請求範例 [Section titled “請求範例”](#請求範例) 範例一律打測試環境。把 `OEN_API_TOKEN` 設成測試環境 CRM 產生的 token。 * cURL ```bash curl -X GET "https://payment-api.testing.oen.tw/order/A20260928001/transactions" \ -H "Authorization: Bearer $OEN_API_TOKEN" ``` * Node.js ```js const res = await fetch("https://payment-api.testing.oen.tw/order/A20260928001/transactions", { method: "GET", headers: { Authorization: `Bearer ${process.env.OEN_API_TOKEN}`, }, }); const result = await res.json(); if (result.code !== "S0000") { throw new Error(`${result.code} ${result.message}`); } ``` * PHP ```php 'GET', CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('OEN_API_TOKEN'), ], ]); $result = json_decode(curl_exec($ch), true); if ($result['code'] !== 'S0000') { throw new Exception($result['code'] . ' ' . $result['message']); } ``` * Python ```python import os import requests res = requests.request( "GET", "https://payment-api.testing.oen.tw/order/A20260928001/transactions", headers={"Authorization": f"Bearer {os.environ['OEN_API_TOKEN']}"}, timeout=30, ) result = res.json() if result["code"] != "S0000": raise RuntimeError(f"{result['code']} {result['message']}") ``` ## 回應 [Section titled “回應”](#回應) | HTTP | 說明 | | ----- | ---------------- | | `200` | 回傳交易陣列。查無資料時是空陣列 | | `401` | token 錯誤 | ### 回應欄位 [Section titled “回應欄位”](#回應欄位) | 欄位 | 型別 | 出現 | 說明 | | -------------- | -------------------- | --- | ------------------------------------------------------------------ | | `transactions` | `array` | 一定有 | [Transaction 物件](/api/objects/transaction/)的陣列,不含 `productDetails` | ### 回應範例 [Section titled “回應範例”](#回應範例) ```json { "code": "S0000", "data": { "transactions": [ { "id": "P20260928AB12CD34", "transactionId": "2HhndgEquCbDzC5OyVxWSGZmd2l", "action": "onetime", "amount": 1200, "paymentMethod": "card", "status": "charged", "orderId": "A20260928001", "createdAt": "2026-09-28T02:40:25.502Z", "paidAt": "2026-09-28T02:41:03.118Z", "refundAmount": 0 } ] }, "message": "" } ``` ## 可能的錯誤 [Section titled “可能的錯誤”](#可能的錯誤) | 錯誤碼 | HTTP | 什麼時候會發生 | | ------- | ---- | -------- | | `A0001` | 401 | token 錯誤 | 完整清單與處理方式見[錯誤碼一覽](/api/error-codes/)。 ## 相關說明 [Section titled “相關說明”](#相關說明) 什麼時候會有多筆,見[查詢與對帳](/developers/querying/)。 # 查詢商店定期購訂單列表 > 列出應援商店的定期購訂單。 GET `/subscriptions` * 正式環境:`https://payment-api.oen.tw/subscriptions` * 測試環境:`https://payment-api.testing.oen.tw/subscriptions` **列出的是應援商店的「定期購」訂單**,不是用 Payment API 建立的定期定額。只有在應援商店販售訂閱制商品的商家才需要。Payment API 定期定額請用[查詢定期定額明細](/api/get-subscription/)。依建立時間由新到舊,每頁最多 50 筆;用 `status` 篩選時,一頁可能少於 50 筆但仍有下一頁。 Header 帶 `Authorization: Bearer ` 。token 的取得方式見[驗證與 token](/developers/authentication/)。 ## 請求 [Section titled “請求”](#請求) ### 查詢參數 [Section titled “查詢參數”](#查詢參數) | 欄位 | 型別 | 必填 | 說明 | | -------- | -------- | -- | ---------------------------------------------------------------------------------------------------- | | `status` | `string` | 選填 | 用逗號分隔的狀態。不帶就是全部。可用值:`ongoing`、`cancelled`、`done`、`error`、`retryScheduled`範例:`ongoing,retryScheduled` | | `page` | `string` | 選填 | 下一頁的分頁標記。把上一頁回應的 `page` 原封不動帶入。 | ### 請求範例 [Section titled “請求範例”](#請求範例) 範例一律打測試環境。把 `OEN_API_TOKEN` 設成測試環境 CRM 產生的 token。 * cURL ```bash curl -X GET "https://payment-api.testing.oen.tw/subscriptions?status=ongoing" \ -H "Authorization: Bearer $OEN_API_TOKEN" ``` * Node.js ```js const res = await fetch("https://payment-api.testing.oen.tw/subscriptions?status=ongoing", { method: "GET", headers: { Authorization: `Bearer ${process.env.OEN_API_TOKEN}`, }, }); const result = await res.json(); if (result.code !== "S0000") { throw new Error(`${result.code} ${result.message}`); } ``` * PHP ```php 'GET', CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('OEN_API_TOKEN'), ], ]); $result = json_decode(curl_exec($ch), true); if ($result['code'] !== 'S0000') { throw new Exception($result['code'] . ' ' . $result['message']); } ``` * Python ```python import os import requests res = requests.request( "GET", "https://payment-api.testing.oen.tw/subscriptions?status=ongoing", headers={"Authorization": f"Bearer {os.environ['OEN_API_TOKEN']}"}, timeout=30, ) result = res.json() if result["code"] != "S0000": raise RuntimeError(f"{result['code']} {result['message']}") ``` ## 回應 [Section titled “回應”](#回應) | HTTP | 說明 | | ----- | --------------- | | `200` | 回傳定期購訂單列表與下一頁代碼 | | `401` | token 錯誤 | ### 回應欄位 [Section titled “回應欄位”](#回應欄位) | 欄位 | 型別 | 出現 | 說明 | | ------------------------------------------------- | ---------------- | ---- | -------------------------------------------------------------------------- | | `subscriptions` | `array` | 一定有 | 定期購訂單 | | `id`subscriptions\[].id | `string` | 可能沒有 | 訂單的內部 id | | `status`subscriptions\[].status | `string` | 可能沒有 | `ongoing` 進行中、`retryScheduled` 待重扣、`error` 扣款失敗、`cancelled` 已取消、`done` 已完成 | | `items`subscriptions\[].items | `array` | 可能沒有 | 訂購的商品 | | `totalAmount`subscriptions\[].totalAmount | `number` | 可能沒有 | 每期金額 | | `period`subscriptions\[].period | `integer` | 可能沒有 | 已扣期數 | | `numberOfPeriods`subscriptions\[].numberOfPeriods | `integer` | 可能沒有 | 總期數 | | `lastChargedAt`subscriptions\[].lastChargedAt | `string` | 可能沒有 | 最近一次扣款時間(台北時間,帶 `+08:00`) | | `user`subscriptions\[].user | `object` | 可能沒有 | 訂購人 `{ name, email }` | | `createdAt`subscriptions\[].createdAt | `string` | 可能沒有 | 建立時間 | | `cancelledAt`subscriptions\[].cancelledAt | `string` | 可能沒有 | 取消時間 | | `endedAt`subscriptions\[].endedAt | `string` | 可能沒有 | 結束時間(台北時間,帶 `+08:00`) | | `page` | `string \| null` | 一定有 | 下一頁的分頁標記;沒有下一頁時是 `null` | ## 可能的錯誤 [Section titled “可能的錯誤”](#可能的錯誤) | 錯誤碼 | HTTP | 什麼時候會發生 | | ------- | ---- | ------------- | | `V0001` | 400 | `status` 值不正確 | | `A0001` | 401 | token 錯誤 | 完整清單與處理方式見[錯誤碼一覽](/api/error-codes/)。 ## 相關說明 [Section titled “相關說明”](#相關說明) Payment API 建立的定期定額請用[查詢定期定額明細](/api/get-subscription/)。 # 查詢交易列表 > 依期間列出交易,用來對帳。 GET `/transactions` * 正式環境:`https://payment-api.oen.tw/transactions` * 測試環境:`https://payment-api.testing.oen.tw/transactions` 依建立時間由新到舊列出交易,每頁 50 筆。**列出的是網域的所有款項**,除了 API 建立的交易,也包含商店訂單、捐款等其他收款,以及撥款後退款產生的負數調整款項。只要 API 交易時,請用 `orderId` 比對,或改用[用訂單編號查詢](/api/list-order-transactions/)。 Header 帶 `Authorization: Bearer ` 。token 的取得方式見[驗證與 token](/developers/authentication/)。 ## 請求 [Section titled “請求”](#請求) ### 查詢參數 [Section titled “查詢參數”](#查詢參數) | 欄位 | 型別 | 必填 | 說明 | | ------- | -------- | -- | ------------------------------------------------------------------------------------------------------- | | `start` | `string` | 選填 | 起始日期,以台北時間的當天 00:00 起算。可以寫 `2026-09-01`,或 Unix 毫秒時間戳。限制:要和 `end` 一起帶;只帶一個會回 500 `F0001`(`Invalid date`) | | `end` | `string` | 選填 | 結束日期,算到台北時間的當天 23:59:59。 | | `page` | `string` | 選填 | 下一頁的分頁標記。把上一頁回應的 `page` 原封不動帶入。 | ### 請求範例 [Section titled “請求範例”](#請求範例) 範例一律打測試環境。把 `OEN_API_TOKEN` 設成測試環境 CRM 產生的 token。 * cURL ```bash curl -X GET "https://payment-api.testing.oen.tw/transactions?start=2026-09-01&end=2026-09-28" \ -H "Authorization: Bearer $OEN_API_TOKEN" ``` * Node.js ```js const res = await fetch("https://payment-api.testing.oen.tw/transactions?start=2026-09-01&end=2026-09-28", { method: "GET", headers: { Authorization: `Bearer ${process.env.OEN_API_TOKEN}`, }, }); const result = await res.json(); if (result.code !== "S0000") { throw new Error(`${result.code} ${result.message}`); } ``` * PHP ```php 'GET', CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('OEN_API_TOKEN'), ], ]); $result = json_decode(curl_exec($ch), true); if ($result['code'] !== 'S0000') { throw new Exception($result['code'] . ' ' . $result['message']); } ``` * Python ```python import os import requests res = requests.request( "GET", "https://payment-api.testing.oen.tw/transactions?start=2026-09-01&end=2026-09-28", headers={"Authorization": f"Bearer {os.environ['OEN_API_TOKEN']}"}, timeout=30, ) result = res.json() if result["code"] != "S0000": raise RuntimeError(f"{result['code']} {result['message']}") ``` ## 回應 [Section titled “回應”](#回應) | HTTP | 說明 | | ----- | ------------------------------ | | `200` | 回傳交易列表與下一頁代碼 | | `401` | token 錯誤 | | `500` | 日期格式錯誤或 `page` 分頁標記無效(`F0001`) | ### 回應欄位 [Section titled “回應欄位”](#回應欄位) | 欄位 | 型別 | 出現 | 說明 | | -------------- | -------------------- | --- | ------------------------------------------------------------------ | | `transactions` | `array` | 一定有 | [Transaction 物件](/api/objects/transaction/)的陣列,不含 `productDetails` | | `page` | `string \| null` | 一定有 | 下一頁的分頁標記;沒有下一頁時是 `null` | ### 回應範例 [Section titled “回應範例”](#回應範例) ```json { "code": "S0000", "data": { "transactions": [ { "id": "P20260928AB12CD34", "transactionId": "2HhndgEquCbDzC5OyVxWSGZmd2l", "action": "onetime", "amount": 1200, "paymentMethod": "card", "status": "charged", "orderId": "A20260928001", "createdAt": "2026-09-28T02:40:25.502Z", "paidAt": "2026-09-28T02:41:03.118Z", "refundAmount": 0 } ], "page": "eyJMaW1pdCI6NTAsIkxhc3RFdmFsdWF0ZWRLZXkiOnt9fQ==" }, "message": "" } ``` ## 可能的錯誤 [Section titled “可能的錯誤”](#可能的錯誤) | 錯誤碼 | HTTP | 什麼時候會發生 | | ------- | ---- | ------------------------------------- | | `F0001` | 500 | `start`、`end` 只帶一個或格式錯誤;`page` 分頁標記無效 | | `A0001` | 401 | token 錯誤 | 完整清單與處理方式見[錯誤碼一覽](/api/error-codes/)。 ## 相關說明 [Section titled “相關說明”](#相關說明) 對帳做法見[查詢與對帳](/developers/querying/)。 # Subscription 物件 > 查詢定期定額明細的回應欄位與定期定額狀態的意思。 [查詢定期定額明細](/api/get-subscription/)的回應。沒有值的欄位不會出現。 | 欄位 | 型別 | 出現 | 說明 | | ----------------- | --------- | ---- | ----------------------------------------------------------------------- | | `id` | `string` | 一定有 | 定期定額編號(`S` 開頭,17 字元) | | `status` | `string` | 一定有 | 狀態,見下方狀態表 | | `period` | `integer` | 可能沒有 | 已扣期數 | | `numberOfPeriods` | `integer` | 可能沒有 | 總期數。不限期時不會出現 | | `paymentInterval` | `integer` | 可能沒有 | 每幾個月扣一次 | | `amount` | `number` | 可能沒有 | 每期金額 | | `startedAt` | `string` | 可能沒有 | 開始時間 | | `endedAt` | `string` | 可能沒有 | 預計結束時間 | | `nextChargeAt` | `string` | 可能沒有 | 下次扣款時間,只在 `ongoing` 時出現。固定是扣款日台北時間上午 9 點(例如 `2026-10-28T01:00:00.000Z`) | | `cancelledAt` | `string` | 可能沒有 | 取消時間 | | `reason` | `string` | 可能沒有 | 取消原因,或扣款失敗的原因 | | `orderId` | `string` | 可能沒有 | 你的訂單編號 | | `userId` | `string` | 可能沒有 | 你帶入的會員編號 | | `userName` | `string` | 可能沒有 | 消費者姓名 | | `note` | `string` | 可能沒有 | 備註 | | `createdAt` | `string` | 一定有 | 建立時間 | ### 範例 ```json { "id": "S20260928EF56GH78", "status": "ongoing", "period": 1, "numberOfPeriods": 12, "paymentInterval": 1, "amount": 299, "startedAt": "2026-09-28T02:44:35.592Z", "endedAt": "2027-08-28T02:44:35.592Z", "nextChargeAt": "2026-10-28T01:00:00.000Z", "orderId": "SUB20260928001", "userName": "王小明", "createdAt": "2026-09-28T02:44:35.592Z" } ``` ## 定期定額狀態 [Section titled “定期定額狀態”](#定期定額狀態) | status | 意思 | 在 CRM 顯示為 | | ---------------- | ------------------------- | --------- | | `initiated` | 已建立,消費者還沒完成結帳 | — | | `processing` | 消費者正在結帳(例如 3D 驗證中) | — | | `scheduled` | 首期日在未來,已完成綁卡,等待首期扣款 | — | | `ongoing` | 進行中 | 進行中 | | `retryScheduled` | 這一期扣款失敗,已排定重新扣款 | 異常 - 待重扣 | | `error` | 扣款失敗且不再重試,**之後的期數都不會再扣款** | 異常 | | `cancelled` | 已取消,或已到結束日 | 已取消 | | `done` | 所有期數都扣完了 | 已結束 | `trialing`、`terminated` 只會出現在 [Subscription API](/products/subscription-api/) 建立的訂閱。 可以取消的狀態:`scheduled`、`ongoing`、`retryScheduled`。見[取消定期定額](/api/cancel-subscription/)。 # Transaction 物件 > 交易查詢、列表與退款回應共用的 Transaction 物件:每個欄位、paymentInfo 的形狀,以及交易狀態的意思。 查詢交易、交易列表、用訂單編號查詢與退款的回應都用這個物件。沒有值的欄位不會出現。 | 欄位 | 型別 | 出現 | 說明 | | ----------------- | --------- | ---- | --------------------------------------------------- | | `id` | `string` | 一定有 | 交易編號。API 交易是 `P` 開頭共 17 字元;交易列表裡其他來源的款項是 `C` 開頭 | | `transactionId` | `string` | 一定有 | 交易的內部 id(27 字元),和付款通知的 `id` 相同 | | `action` | `string` | 可能沒有 | `onetime` 單次、`subscription` 定期定額 | | `amount` | `number` | 一定有 | 金額。交易列表中的負數是撥款後退款產生的調整款項 | | `fee` | `number` | 可能沒有 | 手續費 | | `platformFee` | `number` | 可能沒有 | 平台費 | | `paymentMethod` | `string` | 可能沒有 | `card`、`applePay`、`linePay`、`cvs`、`atm` | | `paymentInfo` | `object` | 可能沒有 | 付款方式的細節,依付款方式不同,見下方說明 | | `status` | `string` | 一定有 | 交易狀態,見下方狀態表 | | `orderId` | `string` | 可能沒有 | 你的訂單編號 | | `userId` | `string` | 可能沒有 | 你帶入的會員編號 | | `userName` | `string` | 可能沒有 | 消費者姓名 | | `userEmail` | `string` | 可能沒有 | 消費者 Email | | `customId` | `string` | 可能沒有 | 你帶入的自訂資料 | | `note` | `string` | 可能沒有 | 備註 | | `reason` | `string` | 可能沒有 | 付款失敗的原因(收單機構回傳的原始訊息) | | `authCode` | `string` | 可能沒有 | 信用卡授權碼 | | `use3d` | `boolean` | 可能沒有 | 是否走了 3D 驗證 | | `createdAt` | `string` | 一定有 | 建立時間(UTC,ISO 8601) | | `paidAt` | `string` | 可能沒有 | 付款完成時間 | | `refundAmount` | `number` | 可能沒有 | 已退款金額,預設 0 | | `refundedAt` | `string` | 可能沒有 | 退款時間 | | `payoutId` | `string` | 可能沒有 | 撥款單 id | | `payoutAt` | `string` | 可能沒有 | 撥款時間 | | `subscriptionId` | `string` | 可能沒有 | 所屬定期定額的編號(`S` 開頭),定期定額交易才有 | | `period` | `integer` | 可能沒有 | 定期定額的第幾期 | | `productDetails` | `array` | 可能沒有 | 商品明細。只在[查詢交易明細](/api/get-transaction/)並以內部 id 查詢時出現 | | `numberOfPeriods` | `integer` | 可能沒有 | 定期定額總期數(0 為不限期)。出現條件同 `productDetails` | | `success` | `boolean` | 可能沒有 | 只在退款回應中出現 | ### 範例 ```json { "id": "P20260928AB12CD34", "transactionId": "2HhndgEquCbDzC5OyVxWSGZmd2l", "action": "onetime", "amount": 1200, "paymentMethod": "card", "paymentInfo": { "method": "card", "cardNum": "424242******4242", "cardName": "WANG XIAO MING", "cardType": "Visa", "cardIssuerCountry": "TW" }, "status": "charged", "orderId": "A20260928001", "userName": "王小明", "userEmail": "ming@example.com", "createdAt": "2026-09-28T02:40:25.502Z", "paidAt": "2026-09-28T02:41:03.118Z", "refundAmount": 0, "authCode": "831000" } ``` ## paymentInfo [Section titled “paymentInfo”](#paymentinfo) 依付款方式不同: | 付款方式 | 內容 | | -------------- | ------------------------------------------------------------------------------------------------------------------------- | | 信用卡 | `method`、`cardNum`(遮罩為前 6 碼與後 4 碼,例如 `424242******4242`)、`cardName`、`cardType`(Visa、MasterCard、JCB 等)、`cardIssuerCountry` | | 用 token 扣款的信用卡 | 只有 `{ "method": "card" }` | | 超商代碼 | `method`、`cvsCode`、`cvsName`、`totalAmount`、`expiredAt`,以及 `credentials: { cvsName, code, expiredAt }` | | ATM | `method`、`bankName`、`bankCode`、`account`(虛擬帳號)、`expiredAt` 等 | | LINE Pay | `method`、`transactionId`(LINE Pay 交易編號)等 | 程式請只讀上表列出的欄位,其他欄位可能調整。 ## 交易狀態 [Section titled “交易狀態”](#交易狀態) | status | 意思 | 在 CRM 顯示為 | | -------------------- | ------------------------------------------------- | ----------- | | `initiated` | 已建立,消費者還沒付款。消費者離開結帳頁時會一直停在這個狀態,不會變成失敗 | 已建立付款意向 | | `charging` | 處理中。超商代碼、ATM 已取號等待繳費;LINE Pay 等待確認;付款結果不明時也停在這裡 | 尚未付款 | | `authorized` | 已授權、尚未請款(高額交易) | 已授權 | | `charged` | 付款成功,款項由應援代收、等待撥款 | 付款成功 | | `claimed` | 付款成功且款項已撥給你;LINE Pay、藍新等由收單機構直接撥款的付款方式,付款成功就是這個狀態 | 已入帳 | | `failed` | 付款失敗或逾期未繳 | 扣款或授權失敗、已失效 | | `cancelled` | 已取消(尚未繳費的超商代碼被取消) | 已取消 | | `refunding` | 退款處理中(已付款的超商代碼、ATM 等待匯款) | 退款中 | | `refunded` | 已退款。`refundAmount` 小於 `amount` 時是部分退款 | 全額退款、部分退款 | | `refundedPostPayout` | 撥款後才退款,會從下次撥款扣回 | 撥款後退款 | 判斷「付款成功」請用 `charged` 或 `claimed`,不要只看 `charged`。`authorized` 只會出現在需要審核的高額交易,遇到時請聯絡應援確認後續流程。 # 付款通知內容 > Payment API 付款通知(webhook)的 HTTP 格式與每一種 purpose 的欄位:charge、token、schedule_subscription,以及需要另外開啟的退款與訂閱事件。 付款通知的處理方式、重送規則與回查做法,請先看[付款通知(webhook)](/developers/webhooks/)。這一頁列出每一種通知的欄位。 ## HTTP 格式 [Section titled “HTTP 格式”](#http-格式) * `POST` 到你在 CRM 設定的「交易資料回傳網址位置」。 * Header 只有 `Content-Type: application/json`。**沒有簽章,也沒有事件編號**。 * 用 body 的 `purpose` 分辨是哪一種通知。 | purpose | 什麼時候送 | 預設會送嗎 | | ------------------------ | ----------------------------------------- | ------ | | `charge` | 單次付款與定期定額每一期的結果;超商代碼、ATM 取號;LINE Pay 等待確認 | 會 | | `token` | 綁卡頁完成或失敗 | 會 | | `schedule_subscription` | 預約定期定額在結帳頁完成或失敗 | 會 | | `refund` | 退款建立、完成或取消 | 要請應援開啟 | | `subscription_cancelled` | 定期定額被取消(API 或 CRM) | 要請應援開啟 | | `subscription_retry` | 定期定額扣款失敗後,已排定重扣或重扣次數用完 | 要請應援開啟 | `refund`、`subscription_cancelled`、`subscription_retry` 三種要請應援在後台開啟「Webhook 新版事件」才會送出。 ## charge [Section titled “charge”](#charge) 單次付款、定期定額每一期的結果。同一筆交易可能收到多則,例如超商代碼先送取號(`charging`),繳費後再送 `charged`。 | 欄位 | 型別 | 出現 | 說明 | | ----------------- | ------------------ | ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `purpose` | `string` | 一定有 | `charge` | | `id` | `string` | 一定有 | 交易的內部 id。用它呼叫 `GET /transactions/{id}` 回查 | | `transactionId` | `string` | 一定有 | 同 `id` | | `transactionHid` | `string` | 一定有 | 交易編號(`P` 開頭) | | `merchantId` | `string` | 一定有 | 你的網域名稱 | | `orderId` | `string` | 可能沒有 | 你的訂單編號 | | `action` | `string` | 可能沒有 | `onetime` 或 `subscription` | | `status` | `string` | 一定有 | 交易狀態:`charging`、`charged`、`claimed`、`authorized`、`failed` 等 | | `success` | `boolean` | 可能沒有 | 只在最終結果才有:`status` 不是 `failed` 時為 `true`。取號、等待確認(`charging`)時沒有這個欄位 | | `paymentMethod` | `string` | 可能沒有 | `card`、`applePay`、`linePay`、`cvs`、`atm` | | `amount` | `number` | 可能沒有 | 金額 | | `currency` | `string` | 可能沒有 | 幣別,**小寫**,例如 `twd` | | `paymentInfo` | `string \| object` | 可能沒有 | 信用卡:卡號末四碼字串(Apple Pay 的付款通知可能不帶 `paymentInfo`);超商:`{ cvsName, code, expiredAt }`;ATM:`{ bankName, bankCode, account, expiredAt }`;LINE Pay:LINE Pay 交易編號 | | `authCode` | `string` | 可能沒有 | 授權碼 | | `customId` | `string` | 可能沒有 | 你帶入的自訂資料 | | `productDetails` | `array` | 可能沒有 | 建立交易時的商品明細 | | `userPhone` | `string` | 可能沒有 | 消費者在結帳頁填的手機 | | `createdAt` | `string` | 可能沒有 | **交易建立時間**,不是通知送出的時間 | | `paidAt` | `string` | 可能沒有 | 付款完成時間 | | `message` | `string` | 可能沒有 | 失敗原因。超商代碼、ATM、LINE Pay 逾期時是 `PAYMENT_EXPIRED`;3D 驗證開始後 10 分鐘沒完成是 `3DS_ABANDONED` | | `subscriptionId` | `string` | 可能沒有 | 定期定額編號(`S` 開頭),定期定額才有 | | `period` | `integer` | 可能沒有 | 定期定額的第幾期 | | `numberOfPeriods` | `integer` | 可能沒有 | 定期定額總期數 | | `isRetry` | `boolean` | 可能沒有 | 定期定額:這次是否為失敗後的重新扣款 | | `failureCode` | `string` | 可能沒有 | 定期定額扣款失敗的原因:`invalid_card_number`、`invalid_cvv`、`expired_card`、`exceeds_credit_limit`、`payment_refused`、`duplicate_payment`、`charge_failed` | | `scheduleStatus` | `string` | 可能沒有 | 定期定額狀態:`pending`、`trialing`、`active`、`retrying`、`error`、`cancelled`、`completed` | | `nextChargeAt` | `string` | 可能沒有 | 定期定額下次扣款時間。請以[查詢定期定額明細](/api/get-subscription/)為準 | ### 範例 ```json { "purpose": "charge", "action": "onetime", "merchantId": "ming", "orderId": "A20260928001", "id": "2HhndgEquCbDzC5OyVxWSGZmd2l", "transactionId": "2HhndgEquCbDzC5OyVxWSGZmd2l", "transactionHid": "P20260928AB12CD34", "status": "charged", "success": true, "paymentMethod": "card", "amount": 1200, "currency": "twd", "paymentInfo": "4242", "authCode": "831000", "customId": "cart-8812", "createdAt": "2026-09-28T02:40:25.502Z", "paidAt": "2026-09-28T02:41:03.118Z", "productDetails": [ { "productionCode": "SKU-001", "description": "手沖咖啡豆 200g", "quantity": 2, "unit": "包", "unitPrice": 600 } ], "message": "" } ``` 超商代碼取號時(還沒繳費,沒有 `success` 欄位): ```json { "purpose": "charge", "action": "onetime", "merchantId": "ming", "orderId": "A20260928002", "id": "2HhnkQ3vXy8LpA1cB0dE9fG2hIj", "transactionId": "2HhnkQ3vXy8LpA1cB0dE9fG2hIj", "transactionHid": "P20260928UV12WX34", "status": "charging", "paymentMethod": "cvs", "amount": 500, "currency": "twd", "paymentInfo": { "cvsName": "超商代碼繳費", "code": "XXXXXXXXXXXX", "expiredAt": "2026-09-30T02:00:00.000Z" }, "message": "" } ``` 消費者繳費後,同一個 `id` 會再收到一則 `status: charged`、`success: true` 的通知。逾期未繳則是 `status: failed`、`message: PAYMENT_EXPIRED`。 ## token [Section titled “token”](#token) [建立綁卡頁](/api/checkout-token/)後,消費者完成或放棄綁卡時送出。**這是拿到 token 的唯一管道**。 | 欄位 | 型別 | 出現 | 說明 | | --------------- | --------- | ---- | ---------------------------------------------------------- | | `purpose` | `string` | 一定有 | `token` | | `id` | `string` | 一定有 | 建立綁卡頁時回傳的 `id` | | `transactionId` | `string` | 一定有 | 同 `id` | | `merchantId` | `string` | 一定有 | 你的網域名稱 | | `success` | `boolean` | 一定有 | 是否綁卡成功 | | `token` | `string` | 可能沒有 | 成功時才有。用來呼叫[用 token 扣款](/api/token-transactions/),請當成密碼一樣保存 | | `paymentInfo` | `string` | 可能沒有 | 卡號末四碼 | | `customId` | `string` | 可能沒有 | 建立綁卡頁時帶的自訂資料 | | `message` | `string` | 可能沒有 | 失敗原因。3D 驗證開始後 10 分鐘沒完成是 `3DS_ABANDONED` | ### 範例 ```json { "purpose": "token", "merchantId": "ming", "id": "2HhngP9sVb3mK1xYdQe7Wc0RtUi", "transactionId": "2HhngP9sVb3mK1xYdQe7Wc0RtUi", "success": true, "token": "2HhnhWm8Qe0sPz4LbXv9KdYt1Ra", "paymentInfo": "4242", "customId": "M001", "message": "" } ``` 警告 沒有 API 可以回查 token。這則通知沒收到,token 就拿不回來,只能請消費者重新綁卡。請確認你的接收端穩定,並在 10 秒內回應 2xx。 ## schedule_subscription [Section titled “schedule_subscription”](#schedule_subscription) [建立預約定期定額](/api/checkout-schedule/)後,消費者在結帳頁完成或失敗時送出。之後每一期的扣款結果以 `charge` 通知送出。 | 欄位 | 型別 | 出現 | 說明 | | ----------------- | --------- | ---- | ----------------------- | | `purpose` | `string` | 一定有 | `schedule_subscription` | | `id` | `string` | 一定有 | 建立時回傳的 `id` | | `subscriptionId` | `string` | 一定有 | 定期定額編號(`S` 開頭) | | `merchantId` | `string` | 一定有 | 你的網域名稱 | | `success` | `boolean` | 一定有 | 是否成功 | | `amount` | `number` | 可能沒有 | 每期金額 | | `currency` | `string` | 可能沒有 | 幣別(小寫) | | `period` | `integer` | 可能沒有 | 已扣期數 | | `numberOfPeriods` | `integer` | 可能沒有 | 總期數 | | `interval` | `integer` | 可能沒有 | 每幾個月扣一次 | | `startedAt` | `string` | 可能沒有 | 開始時間 | | `nextChargeAt` | `string` | 可能沒有 | 下次扣款時間 | | `paymentInfo` | `string` | 可能沒有 | 成功時為卡號末四碼 | | `customId` | `string` | 可能沒有 | 你帶入的自訂資料 | | `productDetails` | `array` | 可能沒有 | 商品明細 | | `message` | `string` | 可能沒有 | 失敗原因 | ### 範例 ```json { "purpose": "schedule_subscription", "merchantId": "ming", "id": "2HhnfZ1kqRmA7cX0TQwq8vBn3Ls", "subscriptionId": "S20260928EF56GH78", "success": true, "amount": 1500, "currency": "twd", "period": 0, "numberOfPeriods": 4, "interval": 3, "startedAt": "2026-10-15T00:00:00.000Z", "nextChargeAt": "2026-10-15T01:00:00.000Z", "paymentInfo": "4242", "message": "" } ``` ## refund(需開啟) [Section titled “refund(需開啟)”](#refund需開啟) | 欄位 | 說明 | | -------------------------- | ----------------------------------------------- | | `purpose` | `refund` | | `id` | 這則退款事件的 id,每則都不同 | | `refundId` | 退款單 id | | `chargeId`、`chargeHumanId` | 原交易的內部 id 與交易編號(`P` 開頭) | | `amount`、`origAmount` | 退款金額、原交易金額 | | `currency` | 幣別(小寫) | | `refundStatus` | `refunding`(處理中)、`refunded`(完成)、`cancelled`(取消) | | `reason` | 退款原因 | | `paymentMethod` | 原交易的付款方式 | | `requestedAt`、`resolvedAt` | 申請時間、完成時間 | | `customId` | 原交易的自訂資料 | | `subscriptionId`、`period` | 定期定額交易才有 | ## subscription_cancelled(需開啟) [Section titled “subscription_cancelled(需開啟)”](#subscription_cancelled需開啟) | 欄位 | 說明 | | -------------------------- | ----------------------------------- | | `purpose` | `subscription_cancelled` | | `subscriptionId` | 定期定額編號 | | `source` | 誰取消的:`merchant_api`(API)或 `crm`(後台) | | `reason` | 取消原因 | | `cancelledAt` | 取消時間 | | `cancelledFromStatus` | 取消前的狀態 | | `period`、`numberOfPeriods` | 已扣期數、總期數 | | `customId` | 自訂資料 | ## subscription_retry(需開啟) [Section titled “subscription_retry(需開啟)”](#subscription_retry需開啟) | 欄位 | 說明 | | -------------------------- | --------------------------------------------- | | `purpose` | `subscription_retry` | | `outcome` | `scheduled`:已排定重新扣款;`exhausted`:重扣次數用完,定期定額停止 | | `retryAttemptNumber` | 第幾次重扣 | | `nextRetryAt` | 下次重扣時間(`scheduled` 才有) | | `failureCode` | 扣款失敗的原因 | | `chargeId`、`chargeHumanId` | 失敗那一期的交易 | | `subscriptionId`、`period` | 定期定額編號與期數 | # 退款 > 為一筆交易退款。 POST `/refunds/:transactionHid` * 正式環境:`https://payment-api.oen.tw/refunds/:transactionHid` * 測試環境:`https://payment-api.testing.oen.tw/refunds/:transactionHid` 為一筆交易退款。**每筆交易只能用 API 成功退款一次**,部分退款後剩下的金額不能再退,請一次決定好金額。信用卡、Apple Pay、LINE Pay 即時退款;已付款的超商代碼與 ATM 要帶退款帳戶,由應援匯款;尚未繳費的超商代碼會直接取消代碼。 Header 帶 `Authorization: Bearer ` 與 `Content-Type: application/json` 。token 的取得方式見[驗證與 token](/developers/authentication/)。 ## 請求 [Section titled “請求”](#請求) ### 路徑參數 [Section titled “路徑參數”](#路徑參數) | 欄位 | 型別 | 必填 | 說明 | | ---------------- | -------- | -- | ----------------------------------- | | `transactionHid` | `string` | 必填 | `P` 開頭的交易編號(17 字元)。不接受 27 字元的內部 id。 | ### Body 欄位 [Section titled “Body 欄位”](#body-欄位) | 欄位 | 型別 | 必填 | 說明 | | ---------------------------------- | --------- | ---- | ------------------------------------------------------------------------------------------ | | `merchantId` | `string` | 必填 | 你的網域名稱。例如應援頁是 `https://ming.oen.tw`,就填 `ming`。必須與 token 所屬的網域相同,不同會回 400 `V0001`。範例:`ming` | | `amount` | `integer` | 選填 | 退款金額。不帶就是全額退款。限制:1 以上,不能大於原交易金額 | | `reason` | `string` | 選填 | 退款原因。 | | `productDetails` | `array` | 條件必填 | 原交易有開立電子發票時必填,用來開立折讓。品項合計必須等於退款金額。欄位同建立交易時的 `productDetails`。 | | `remitInfo` | `object` | 條件必填 | 退款匯款帳戶。**已付款的超商代碼或 ATM 交易**必填;信用卡類與尚未繳費的超商代碼不用帶。 | | `bankCode`remitInfo.bankCode | `string` | 必填 | 銀行代碼 | | `bankName`remitInfo.bankName | `string` | 必填 | 銀行名稱 | | `branchCode`remitInfo.branchCode | `string` | 必填 | 分行代碼 | | `branchName`remitInfo.branchName | `string` | 必填 | 分行名稱 | | `account`remitInfo.account | `string` | 必填 | 帳號限制:8 到 14 字元 | | `accountName`remitInfo.accountName | `string` | 必填 | 戶名 | ### 請求範例 [Section titled “請求範例”](#請求範例) 範例一律打測試環境。把 `OEN_API_TOKEN` 設成測試環境 CRM 產生的 token。 * cURL ```bash curl -X POST "https://payment-api.testing.oen.tw/refunds/P20260928AB12CD34" \ -H "Authorization: Bearer $OEN_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "merchantId": "ming", "amount": 600, "reason": "消費者取消一包", "productDetails": [ { "productionCode": "SKU-001", "description": "手沖咖啡豆 200g", "quantity": 1, "unit": "包", "unitPrice": 600 } ] }' ``` * Node.js ```js const res = await fetch("https://payment-api.testing.oen.tw/refunds/P20260928AB12CD34", { method: "POST", headers: { Authorization: `Bearer ${process.env.OEN_API_TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify({ "merchantId": "ming", "amount": 600, "reason": "消費者取消一包", "productDetails": [ { "productionCode": "SKU-001", "description": "手沖咖啡豆 200g", "quantity": 1, "unit": "包", "unitPrice": 600 } ] }), }); const result = await res.json(); if (result.code !== "S0000") { throw new Error(`${result.code} ${result.message}`); } ``` * PHP ```php 'POST', CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('OEN_API_TOKEN'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'merchantId' => 'ming', 'amount' => 600, 'reason' => '消費者取消一包', 'productDetails' => [ [ 'productionCode' => 'SKU-001', 'description' => '手沖咖啡豆 200g', 'quantity' => 1, 'unit' => '包', 'unitPrice' => 600, ], ], ]), ]); $result = json_decode(curl_exec($ch), true); if ($result['code'] !== 'S0000') { throw new Exception($result['code'] . ' ' . $result['message']); } ``` * Python ```python import os import requests res = requests.request( "POST", "https://payment-api.testing.oen.tw/refunds/P20260928AB12CD34", headers={"Authorization": f"Bearer {os.environ['OEN_API_TOKEN']}"}, json={ "merchantId": "ming", "amount": 600, "reason": "消費者取消一包", "productDetails": [ { "productionCode": "SKU-001", "description": "手沖咖啡豆 200g", "quantity": 1, "unit": "包", "unitPrice": 600, }, ], }, timeout=30, ) result = res.json() if result["code"] != "S0000": raise RuntimeError(f"{result['code']} {result['message']}") ``` ## 回應 [Section titled “回應”](#回應) | HTTP | 說明 | | ----- | --------------------------------------------------------------------------- | | `200` | 退款已受理。回傳更新後的 [Transaction 物件](/api/objects/transaction/),另加 `success: true` | | `400` | 參數錯誤、交易狀態不能退款,或收單機構退款失敗 | | `401` | token 錯誤 | | `500` | 系統錯誤,或網域待撥款金額不足(見下方錯誤表) | ### 回應範例 [Section titled “回應範例”](#回應範例) ```json { "code": "S0000", "data": { "success": true, "id": "P20260928AB12CD34", "transactionId": "2HhndgEquCbDzC5OyVxWSGZmd2l", "action": "onetime", "amount": 1200, "paymentMethod": "card", "status": "refunded", "orderId": "A20260928001", "createdAt": "2026-09-28T02:40:25.502Z", "paidAt": "2026-09-28T02:41:03.118Z", "refundAmount": 600, "refundedAt": "2026-09-28T03:00:00.000Z", "authCode": "831000" }, "message": "" } ``` ## 可能的錯誤 [Section titled “可能的錯誤”](#可能的錯誤) | 錯誤碼 | HTTP | 什麼時候會發生 | | ------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `V0001` | 400 | 找不到交易或不屬於你的網域;金額錯誤(`INVALID_REFUND_AMOUNT`)或超過原金額(`REFUND_AMOUNT_EXCEED_CHARGE_AMOUNT`);缺少折讓品項(`PRODUCT_DETAILS_REQUIRED`、`PRODUCT_AMOUNT_NOT_MATCH`);缺少退款帳戶(`REMITTANCE_INFO_REQUIRED`) | | `V0002` | 400 | 交易狀態不能退款,例如尚未付款、已經退過款 | | `R0001` | 400 | 收單機構退款失敗。先查詢交易狀態;之後重試都回 `V0002` 時,請聯絡應援 | | `F0001` | 500 | 訊息為 `Current charged amount too low`:你的網域尚未撥款的已收款淨額低於 200 元,暫時無法退款,請聯絡應援。其他訊息為系統錯誤 | | `A0001` | 401 | token 錯誤 | 完整清單與處理方式見[錯誤碼一覽](/api/error-codes/)。 ## 相關說明 [Section titled “相關說明”](#相關說明) 各付款方式的退款規則見[退款](/developers/refunds/)。 # 用 token 建立定期定額 > 用綁卡取得的 token 建立定期定額。 POST `/token/subscriptions` * 正式環境:`https://payment-api.oen.tw/token/subscriptions` * 測試環境:`https://payment-api.testing.oen.tw/token/subscriptions` 用綁卡取得的 token 建立定期定額。不帶 `startDate` 時當下扣第一期,結果以回應為準(第一期不送付款通知,之後每期會送);帶未來日期時只建立排程,到期才扣款。 Header 帶 `Authorization: Bearer ` 與 `Content-Type: application/json` 。token 的取得方式見[驗證與 token](/developers/authentication/)。 ## 請求 [Section titled “請求”](#請求) ### Body 欄位 [Section titled “Body 欄位”](#body-欄位) | 欄位 | 型別 | 必填 | 說明 | | ------------------------------------------------ | ---------------- | ------ | ---------------------------------------------------------------------------------------------------------------- | | `merchantId` | `string` | 必填 | 你的網域名稱。例如應援頁是 `https://ming.oen.tw`,就填 `ming`。必須與 token 所屬的網域相同,不同會回 400 `V0001`。範例:`ming` | | `amount` | `integer` | 必填 | 每期金額(新台幣元)。必須等於 `productDetails` 的合計。限制:1 以上的整數範例:`1200` | | `currency` | `string` | 選填 | 幣別,只能是 `TWD`。可用值:`TWD`預設:`TWD` | | `token` | `string` | 必填 | 綁卡通知裡的 `token`。 | | `numberOfPeriods` | `integer` | 選填 | 總期數。不帶就是不限期,直到取消。限制:2 以上 | | `paymentInterval` | `integer` | 選填 | 每幾個月扣款一次。限制:1 到 12預設:`1` | | `startDate` | `string` | 選填 | 首期扣款日(台北日期)。不帶就立即扣第一期。限制:`yyyy/MM/dd`;**必須晚於今天**,最晚 12 個月內 | | `orderId` | `string` | 必填 | 你的訂單編號。之後可以用[用訂單編號查詢](/api/list-order-transactions/)找到這筆訂單的所有交易。限制:不檢查重複。同一個 `orderId` 可以建立多筆交易範例:`A20260928001` | | `productDetails` | `array` | 必填 | 商品明細,用來開立電子發票與顯示在 CRM。**所有品項「數量 × 單價」的合計必須等於 `amount`**,否則回 400 `V0001`(`PRODUCT_AMOUNT_NOT_MATCH`)。 | | `productionCode`productDetails\[].productionCode | `string` | 必填 | 商品代碼 | | `description`productDetails\[].description | `string` | 必填 | 商品名稱。第一個品項的名稱也會當成交易的商品描述 | | `quantity`productDetails\[].quantity | `integer` | 必填 | 數量限制:1 以上的整數 | | `unit`productDetails\[].unit | `string` | 必填 | 單位,例如「個」「份」 | | `unitPrice`productDetails\[].unitPrice | `integer` | 必填 | 單價(新台幣元) | | `userId` | `string` | 選填 | 你系統裡的會員編號。 | | `userName` | `string` | 條件必填 | 消費者姓名。網域有開通電子發票時必填。 | | `userEmail` | `string (email)` | 條件必填 | 消費者 Email。網域有開通電子發票時必填,發票通知會寄到這裡。 | | `userPhone` | `string` | 選填 | 消費者手機號碼。網域設定為手機必填時,必須是有效的電話號碼。 | | `invoiceInfo` | `object` | 選填 | 電子發票資訊。網域有開通電子發票時才有作用;沒帶時開立雲端發票。 | | `invoiceType`invoiceInfo.invoiceType | `string` | 必填 | `cloud`:雲端發票;`company`:公司戶(打統編)可用值:`cloud`、`company` | | `carrierType`invoiceInfo.carrierType | `string` | 雲端發票必填 | 載具類型:`3J0002` 手機條碼、`CQ0001` 自然人憑證、空字串為會員載具可用值:`3J0002`、`CQ0001`、`` | | `carrierId`invoiceInfo.carrierId | `string` | 選填 | 載具號碼。手機條碼格式為 `/` 加 7 碼,會即時向財政部驗證;自然人憑證為 2 個英文字母加 14 碼數字 | | `buyerIdentifier`invoiceInfo.buyerIdentifier | `string` | 公司戶必填 | 買受人統一編號,8 碼,會檢查檢查碼 | | `buyerName`invoiceInfo.buyerName | `string` | 選填 | 買受人名稱 | | `email`invoiceInfo.email | `string (email)` | 選填 | 發票通知 Email | | `note` | `string` | 選填 | 備註。 | ### 請求範例 [Section titled “請求範例”](#請求範例) 範例一律打測試環境。把 `OEN_API_TOKEN` 設成測試環境 CRM 產生的 token。 * cURL ```bash curl -X POST "https://payment-api.testing.oen.tw/token/subscriptions" \ -H "Authorization: Bearer $OEN_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "merchantId": "ming", "amount": 299, "token": "2HhnhWm8Qe0sPz4LbXv9KdYt1Ra", "numberOfPeriods": 12, "paymentInterval": 1, "orderId": "SUB20260928003", "userName": "王小明", "userEmail": "ming@example.com", "productDetails": [ { "productionCode": "PLAN-M", "description": "月費會員", "quantity": 1, "unit": "月", "unitPrice": 299 } ] }' ``` * Node.js ```js const res = await fetch("https://payment-api.testing.oen.tw/token/subscriptions", { method: "POST", headers: { Authorization: `Bearer ${process.env.OEN_API_TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify({ "merchantId": "ming", "amount": 299, "token": "2HhnhWm8Qe0sPz4LbXv9KdYt1Ra", "numberOfPeriods": 12, "paymentInterval": 1, "orderId": "SUB20260928003", "userName": "王小明", "userEmail": "ming@example.com", "productDetails": [ { "productionCode": "PLAN-M", "description": "月費會員", "quantity": 1, "unit": "月", "unitPrice": 299 } ] }), }); const result = await res.json(); if (result.code !== "S0000") { throw new Error(`${result.code} ${result.message}`); } ``` * PHP ```php 'POST', CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('OEN_API_TOKEN'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'merchantId' => 'ming', 'amount' => 299, 'token' => '2HhnhWm8Qe0sPz4LbXv9KdYt1Ra', 'numberOfPeriods' => 12, 'paymentInterval' => 1, 'orderId' => 'SUB20260928003', 'userName' => '王小明', 'userEmail' => 'ming@example.com', 'productDetails' => [ [ 'productionCode' => 'PLAN-M', 'description' => '月費會員', 'quantity' => 1, 'unit' => '月', 'unitPrice' => 299, ], ], ]), ]); $result = json_decode(curl_exec($ch), true); if ($result['code'] !== 'S0000') { throw new Exception($result['code'] . ' ' . $result['message']); } ``` * Python ```python import os import requests res = requests.request( "POST", "https://payment-api.testing.oen.tw/token/subscriptions", headers={"Authorization": f"Bearer {os.environ['OEN_API_TOKEN']}"}, json={ "merchantId": "ming", "amount": 299, "token": "2HhnhWm8Qe0sPz4LbXv9KdYt1Ra", "numberOfPeriods": 12, "paymentInterval": 1, "orderId": "SUB20260928003", "userName": "王小明", "userEmail": "ming@example.com", "productDetails": [ { "productionCode": "PLAN-M", "description": "月費會員", "quantity": 1, "unit": "月", "unitPrice": 299, }, ], }, timeout=30, ) result = res.json() if result["code"] != "S0000": raise RuntimeError(f"{result['code']} {result['message']}") ``` ## 回應 [Section titled “回應”](#回應) | HTTP | 說明 | | ----- | ----------------------------- | | `200` | 建立成功 | | `400` | 第一期扣款失敗(`T0001`~`T0005`)或參數錯誤 | | `401` | token 錯誤 | | `409` | 第一期付款結果不明(`C026`),不要重試 | ### 回應欄位 [Section titled “回應欄位”](#回應欄位) | 欄位 | 型別 | 出現 | 說明 | | ---------------- | -------- | ---- | ----------------------------------- | | `subscriptionId` | `string` | 一定有 | 定期定額編號(`S` 開頭) | | `transactionId` | `string` | 可能沒有 | 第一期交易編號(`P` 開頭)。帶未來 `startDate` 時沒有 | | `authCode` | `string` | 可能沒有 | 第一期的授權碼。帶未來 `startDate` 時沒有 | ### 回應範例 [Section titled “回應範例”](#回應範例) ```json { "code": "S0000", "data": { "subscriptionId": "S20260928MN34OP56", "transactionId": "P20260928QR78ST90", "authCode": "831000" }, "message": "" } ``` ## 可能的錯誤 [Section titled “可能的錯誤”](#可能的錯誤) | 錯誤碼 | HTTP | 什麼時候會發生 | | ------- | ---- | ---------------------------------------------------------------------------------------- | | `T0001` | 400 | 第一期扣款失敗。定期定額不會成立 | | `V0001` | 400 | 欄位不符規則;首期日不存在(`INVALID_START_DATE`)、不晚於今天(`START_DATE_MUST_GREATER_THAN_TODAY`)或超過 12 個月 | | `V0002` | 400 | 網域的金流服務尚未開通 | | `C026` | 409 | 第一期付款結果不明 | | `A0001` | 401 | token 錯誤 | 完整清單與處理方式見[錯誤碼一覽](/api/error-codes/)。 ## 相關說明 [Section titled “相關說明”](#相關說明) 見[存卡與後續扣款](/developers/saved-cards/)與[定期定額](/developers/subscriptions/)。 # 用 token 扣款 > 用綁卡取得的 token 直接扣款,消費者不需要在場。 POST `/token/transactions` * 正式環境:`https://payment-api.oen.tw/token/transactions` * 測試環境:`https://payment-api.testing.oen.tw/token/transactions` 用綁卡取得的 token 直接扣款,消費者不需要在場,也不走 3D 驗證。**結果以這支 API 的回應為準,不會送付款通知**。沒有防重複扣款的機制:逾時或收到 5xx 時,請先用[訂單編號查詢](/api/list-order-transactions/)確認,不要直接重送。 Header 帶 `Authorization: Bearer ` 與 `Content-Type: application/json` 。token 的取得方式見[驗證與 token](/developers/authentication/)。 ## 請求 [Section titled “請求”](#請求) ### Body 欄位 [Section titled “Body 欄位”](#body-欄位) | 欄位 | 型別 | 必填 | 說明 | | ------------------------------------------------ | ---------------- | ------ | ---------------------------------------------------------------------------------------------------------------- | | `merchantId` | `string` | 必填 | 你的網域名稱。例如應援頁是 `https://ming.oen.tw`,就填 `ming`。必須與 token 所屬的網域相同,不同會回 400 `V0001`。範例:`ming` | | `amount` | `integer` | 必填 | 金額(新台幣元)。必須等於 `productDetails` 各品項「數量 × 單價」的合計。限制:1 以上的整數範例:`1200` | | `currency` | `string` | 選填 | 幣別,只能是 `TWD`。可用值:`TWD`預設:`TWD` | | `token` | `string` | 必填 | 綁卡通知裡的 `token`。 | | `orderId` | `string` | 必填 | 你的訂單編號。之後可以用[用訂單編號查詢](/api/list-order-transactions/)找到這筆訂單的所有交易。限制:不檢查重複。同一個 `orderId` 可以建立多筆交易範例:`A20260928001` | | `productDetails` | `array` | 必填 | 商品明細,用來開立電子發票與顯示在 CRM。**所有品項「數量 × 單價」的合計必須等於 `amount`**,否則回 400 `V0001`(`PRODUCT_AMOUNT_NOT_MATCH`)。 | | `productionCode`productDetails\[].productionCode | `string` | 必填 | 商品代碼 | | `description`productDetails\[].description | `string` | 必填 | 商品名稱。第一個品項的名稱也會當成交易的商品描述 | | `quantity`productDetails\[].quantity | `integer` | 必填 | 數量限制:1 以上的整數 | | `unit`productDetails\[].unit | `string` | 必填 | 單位,例如「個」「份」 | | `unitPrice`productDetails\[].unitPrice | `integer` | 必填 | 單價(新台幣元) | | `userId` | `string` | 選填 | 你系統裡的會員編號。 | | `userName` | `string` | 條件必填 | 消費者姓名。網域有開通電子發票時必填。 | | `userEmail` | `string (email)` | 條件必填 | 消費者 Email。網域有開通電子發票時必填,發票通知會寄到這裡。 | | `userPhone` | `string` | 選填 | 消費者手機號碼。網域設定為手機必填時,必須是有效的電話號碼。 | | `invoiceInfo` | `object` | 選填 | 電子發票資訊。網域有開通電子發票時才有作用;沒帶時開立雲端發票。 | | `invoiceType`invoiceInfo.invoiceType | `string` | 必填 | `cloud`:雲端發票;`company`:公司戶(打統編)可用值:`cloud`、`company` | | `carrierType`invoiceInfo.carrierType | `string` | 雲端發票必填 | 載具類型:`3J0002` 手機條碼、`CQ0001` 自然人憑證、空字串為會員載具可用值:`3J0002`、`CQ0001`、`` | | `carrierId`invoiceInfo.carrierId | `string` | 選填 | 載具號碼。手機條碼格式為 `/` 加 7 碼,會即時向財政部驗證;自然人憑證為 2 個英文字母加 14 碼數字 | | `buyerIdentifier`invoiceInfo.buyerIdentifier | `string` | 公司戶必填 | 買受人統一編號,8 碼,會檢查檢查碼 | | `buyerName`invoiceInfo.buyerName | `string` | 選填 | 買受人名稱 | | `email`invoiceInfo.email | `string (email)` | 選填 | 發票通知 Email | | `note` | `string` | 選填 | 備註。 | | `expectedPayoutDate` | `string` | 選填 | 期望撥款日期(台北日期),會顯示在 CRM 金流明細。限制:`yyyy/MM/dd` | ### 請求範例 [Section titled “請求範例”](#請求範例) 範例一律打測試環境。把 `OEN_API_TOKEN` 設成測試環境 CRM 產生的 token。 * cURL ```bash curl -X POST "https://payment-api.testing.oen.tw/token/transactions" \ -H "Authorization: Bearer $OEN_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "merchantId": "ming", "amount": 1200, "token": "2HhnhWm8Qe0sPz4LbXv9KdYt1Ra", "orderId": "A20260928003", "userName": "王小明", "userEmail": "ming@example.com", "productDetails": [ { "productionCode": "SKU-001", "description": "手沖咖啡豆 200g", "quantity": 2, "unit": "包", "unitPrice": 600 } ] }' ``` * Node.js ```js const res = await fetch("https://payment-api.testing.oen.tw/token/transactions", { method: "POST", headers: { Authorization: `Bearer ${process.env.OEN_API_TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify({ "merchantId": "ming", "amount": 1200, "token": "2HhnhWm8Qe0sPz4LbXv9KdYt1Ra", "orderId": "A20260928003", "userName": "王小明", "userEmail": "ming@example.com", "productDetails": [ { "productionCode": "SKU-001", "description": "手沖咖啡豆 200g", "quantity": 2, "unit": "包", "unitPrice": 600 } ] }), }); const result = await res.json(); if (result.code !== "S0000") { throw new Error(`${result.code} ${result.message}`); } ``` * PHP ```php 'POST', CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('OEN_API_TOKEN'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'merchantId' => 'ming', 'amount' => 1200, 'token' => '2HhnhWm8Qe0sPz4LbXv9KdYt1Ra', 'orderId' => 'A20260928003', 'userName' => '王小明', 'userEmail' => 'ming@example.com', 'productDetails' => [ [ 'productionCode' => 'SKU-001', 'description' => '手沖咖啡豆 200g', 'quantity' => 2, 'unit' => '包', 'unitPrice' => 600, ], ], ]), ]); $result = json_decode(curl_exec($ch), true); if ($result['code'] !== 'S0000') { throw new Exception($result['code'] . ' ' . $result['message']); } ``` * Python ```python import os import requests res = requests.request( "POST", "https://payment-api.testing.oen.tw/token/transactions", headers={"Authorization": f"Bearer {os.environ['OEN_API_TOKEN']}"}, json={ "merchantId": "ming", "amount": 1200, "token": "2HhnhWm8Qe0sPz4LbXv9KdYt1Ra", "orderId": "A20260928003", "userName": "王小明", "userEmail": "ming@example.com", "productDetails": [ { "productionCode": "SKU-001", "description": "手沖咖啡豆 200g", "quantity": 2, "unit": "包", "unitPrice": 600, }, ], }, timeout=30, ) result = res.json() if result["code"] != "S0000": raise RuntimeError(f"{result['code']} {result['message']}") ``` ## 回應 [Section titled “回應”](#回應) | HTTP | 說明 | | ----- | ---------------------------------------------- | | `200` | 扣款成功 | | `400` | 扣款失敗(`T0001`~`T0005`)或參數錯誤。失敗的交易仍會建立,可以用訂單編號查到 | | `401` | token 錯誤 | | `409` | 付款結果不明(`C026`)。**不要重試**,先查詢交易狀態 | ### 回應欄位 [Section titled “回應欄位”](#回應欄位) | 欄位 | 型別 | 出現 | 說明 | | ---------- | -------- | ---- | ------------------ | | `id` | `string` | 一定有 | 交易編號(`P` 開頭),退款用這個 | | `authCode` | `string` | 可能沒有 | 授權碼 | ### 回應範例 [Section titled “回應範例”](#回應範例) ```json { "code": "S0000", "data": { "id": "P20260928IJ90KL12", "authCode": "831000" }, "message": "" } ``` ## 可能的錯誤 [Section titled “可能的錯誤”](#可能的錯誤) | 錯誤碼 | HTTP | 什麼時候會發生 | | ------- | ---- | ------------------------------- | | `T0001` | 400 | 交易失敗(一般原因) | | `T0002` | 400 | 安全碼錯誤 | | `T0003` | 400 | 卡片過期 | | `T0004` | 400 | 額度不足 | | `T0005` | 400 | 發卡銀行拒絕授權 | | `V0001` | 400 | 欄位不符規則、品項合計不等於金額 | | `V0002` | 400 | 網域的金流服務尚未開通 | | `V0003` | 400 | 單筆 200,000 元以上,但網域沒有開通高額交易 | | `C026` | 409 | 付款結果不明,回應帶 `"retryable": false` | | `A0001` | 401 | token 錯誤 | 完整清單與處理方式見[錯誤碼一覽](/api/error-codes/)。 ## 相關說明 [Section titled “相關說明”](#相關說明) 什麼時候適合用、如何避免重複扣款,見[存卡與後續扣款](/developers/saved-cards/)。 # 用 AI 助手串接 > 讓 Claude、ChatGPT、Cursor 等 AI 助手根據最新文件幫你寫串接程式。 AI 助手寫金流串接程式很快,但它記得的常常是舊版規則。最可靠的做法,是把本站的最新內容直接交給它。 ## 給 AI 讀的檔案 [Section titled “給 AI 讀的檔案”](#給-ai-讀的檔案) | 檔案 | 內容 | 適合 | | -------------------------------------------------------- | ---------------------------- | ----------------------- | | [`/llms.txt`](/llms.txt) | 全站頁面索引與摘要 | 讓 AI 知道有哪些頁面,再自己去讀 | | [`/llms-full.txt`](/llms-full.txt) | 全站內容的純文字版 | 一次交給 AI 完整內容 | | [`/openapi/payment-api.json`](/openapi/payment-api.json) | Payment API 的 OpenAPI 3.1 定義 | 產生 client 程式、匯入 Postman | 使用方式: * **只需要一頁時**:每一頁標題下方都有「複製成 Markdown(給 AI 用)」,以及「在 ChatGPT 開啟」「在 Claude 開啟」。 * **Claude、ChatGPT 等對話介面**:把 `https://developer.oen.tw/llms-full.txt` 的網址或內容貼進對話,再說明你要做什麼。 * **Cursor、Claude Code 等開發工具**:把 `llms-full.txt` 下載到專案,或在工具的文件設定加入本站網址。 * **Postman**:選 Import,貼上 `https://developer.oen.tw/openapi/payment-api.json`。 不要把 token 貼給 AI API token、Secret Key、Webhook Secret 都不要貼進任何 AI 對話或聊天工具。範例程式請用環境變數(例如 `OEN_API_TOKEN`)。如果已經貼出去了,請到 CRM 重新產生,舊的會立即失效。 ## 請 AI 做事的範例 [Section titled “請 AI 做事的範例”](#請-ai-做事的範例) ```text 請依照 https://developer.oen.tw/llms-full.txt 的應援 Payment API 文件, 用 Node.js(Express)寫: 1. 建立單次付款結帳頁的 API(金額與品項由我的訂單資料計算) 2. 接收付款通知的 endpoint:收到後用 GET /transactions/{id} 回查狀態再更新訂單 3. token 從環境變數 OEN_API_TOKEN 讀取,先打測試環境 ``` 拿到程式後,請對照[上線檢查清單](/developers/go-live/)逐項確認,特別是: * `productDetails` 的品項合計要等於 `amount`。 * 付款通知沒有簽章,一定要回查。 * 付款結果不確定(HTTP 409)時,不要直接重新扣款。 ## 應援提供的 AI 工具 [Section titled “應援提供的 AI 工具”](#應援提供的-ai-工具) [Payment Skill](/ai/skill/)給 Claude Code 等 AI 開發工具的技能檔,公開可安裝。 [Payment MCP](/ai/mcp/)讓 AI 直接呼叫 Payment API 的 MCP server。目前是內部預覽版。 # Payment MCP > 讓 AI 助手直接建立結帳頁、查詢交易的 MCP server。目前是內部預覽版,尚未對外開放。 內部預覽,尚未對外開放 Payment MCP 目前只提供應援內部測試,外部商家還無法安裝,也只能連到測試環境。正式開放時會在[更新紀錄](/changelog/)公告。 現在想讓 AI 協助串接,請改用 [Payment Skill](/ai/skill/) 或把 [`/llms-full.txt`](/llms-full.txt) 提供給 AI。 ## 它能做什麼 [Section titled “它能做什麼”](#它能做什麼) [MCP(Model Context Protocol)](https://modelcontextprotocol.io/) 讓 AI 助手可以直接呼叫外部工具。Payment MCP 把 Payment API 包成 AI 可以使用的工具,例如在對話中說「幫我開一個 500 元的結帳連結」,AI 就會呼叫 API 並回傳結帳頁網址。 預覽版提供的工具: | 工具 | 對應的 API | | --------------------------------- | ---------------------------------------- | | `checkout_link` | [建立單次付款](/api/checkout/) | | `subscription_checkout` | [建立定期定額](/api/checkout-subscription/) | | `scheduled_subscription_checkout` | [建立預約定期定額](/api/checkout-schedule/) | | `exchange_token_by_3d` | [建立綁卡頁](/api/checkout-token/) | | `get_transaction` | [查詢交易明細](/api/get-transaction/) | | `get_transactions` | [查詢交易列表](/api/list-transactions/) | | `get_transactions_by_order_id` | [用訂單編號查詢](/api/list-order-transactions/) | | `get_subscription` | [查詢定期定額明細](/api/get-subscription/) | | `cancel_subscription` | [取消定期定額](/api/cancel-subscription/) | | `read_oen_docs` | 讀取內建的開發者文件(本站文件的快照,可以指定頁面) | | `get_config` | 查看目前的設定(固定連測試環境) | 預覽版沒有退款與用 token 扣款的工具。MCP 只連測試環境,不需要設定環境;指定正式環境或填入正式環境的 token 都會拒絕啟動。 ## 安全提醒 [Section titled “安全提醒”](#安全提醒) MCP 會用你的 API token 實際呼叫 API。正式開放後,也請先在測試環境使用,並只把測試環境的 token 交給 AI 工具。 # Payment Skill > 安裝應援的 Payment Skill,讓 Claude Code 等 AI 開發工具在你提到金流時自動帶入串接知識。 Payment Skill 是一份給 AI 開發工具讀的技能檔(`SKILL.md` 加上參考文件)。安裝後,當你在工具裡提到「應援金流」「oen payment」「定期定額」「退款」等關鍵字,AI 會自動載入 Payment API 的串接知識,幫你產生程式、解釋錯誤碼。 * 原始碼:[github.com/OEN-Tech/oen-payment-skill](https://github.com/OEN-Tech/oen-payment-skill)(MIT 授權) * 支援:Claude Code,以及其他支援 `SKILL.md` 格式的工具 ## 安裝 [Section titled “安裝”](#安裝) 1. 把 repo 複製到工具的技能目錄。以 Claude Code 為例: ```bash # 所有專案都能用 git clone https://github.com/OEN-Tech/oen-payment-skill.git ~/.claude/skills/oen-payment # 或只給目前的專案用 git clone https://github.com/OEN-Tech/oen-payment-skill.git .claude/skills/oen-payment ``` 2. 重新啟動 AI 工具。 3. 試著問:「幫我用 TypeScript 串接應援金流,建立一個結帳頁」。 ## 使用前要知道 [Section titled “使用前要知道”](#使用前要知道) Skill 的參考文件從本站匯出 Skill 的 `references/docs/` 是從本站匯出的,內容與匯出當時的本站一致。兩邊如果不同,以本站為準,因為本站比較新。建議同時把 [`/llms-full.txt`](/llms-full.txt) 提供給 AI。 幾個 AI 最容易寫錯、請你特別檢查的地方: * `productDetails` 在所有結帳 API 都是**必填**,品項的「數量 × 單價」合計必須等於 `amount`。見[單次付款](/developers/one-time/)。 * 退款不只信用卡:LINE Pay、超商代碼、ATM 也能用 API 退,超商與 ATM 要帶退款帳戶。見[退款](/developers/refunds/)。 * 付款通知最多送 3 次,間隔 2 秒、4 秒,而且沒有簽章。見[付款通知](/developers/webhooks/)。 * 取消定期定額只接受 `S` 開頭的定期定額編號。見[取消定期定額](/api/cancel-subscription/)。 * Payment MCP 目前是內部預覽版,尚未對外開放,見 [Payment MCP](/ai/mcp/)。 ## 串接仍然需要 token [Section titled “串接仍然需要 token”](#串接仍然需要-token) Skill 只提供知識,實際呼叫 API 仍需要 CRM 產生的 token。取得方式見[驗證與 token](/developers/authentication/)。 # 更新紀錄 > Payment API 與文件的變更紀錄。 ## 文件 [Section titled “文件”](#文件) ### 2026-09 新文件網站 [Section titled “2026-09 新文件網站”](#2026-09-新文件網站) * Payment API 文件從 Postman 移到本站,依正式環境 v10.3.1.1 逐項核對。舊的 Postman 文件保留但不再更新。 * 新增:存卡與後續扣款(`/checkout-token`、`/token/transactions`、`/token/subscriptions`)、付款通知的完整欄位、錯誤碼一覽、固定 IP 白名單、環境與測試、商家操作指南。 * 更正舊文件: * 所有建立交易的 API 都**必須**帶 `productDetails`,品項合計要等於 `amount`。 * 付款通知最多送 3 次,間隔約 2 秒、4 秒(舊文件寫 2、4、6 秒)。 * 退款不只信用卡:Apple Pay、LINE Pay、超商代碼、ATM 都可以用 API 退。`amount` 不帶就是全額。 * `POST /checkout-schedule` 的回應是 `subscriptionHid`,不是 `transactionHid`。 * 失敗導回的 `payment_error` 不會是 `V0001`。 * 「金額大於 100 元成功」不是應援系統的規則,請不要依賴。 * 新增 [OpenAPI 定義檔](/openapi/payment-api.json) 與 [llms.txt](/llms.txt)。 ## Payment API [Section titled “Payment API”](#payment-api) ### v10.3.1(2026-09) [Section titled “v10.3.1(2026-09)”](#v10312026-09) * 結帳頁與綁卡頁會先擋下已過期的信用卡。 * 付款結果不明的交易不再自動重試扣款,避免重複扣款。 ### v10.3.0(2026-09) [Section titled “v10.3.0(2026-09)”](#v10302026-09) * **`allowedPaymentMethods` 在結帳頁實際生效**:結帳頁照清單顯示付款方式並把關。信用卡仍然一定會出現,`["cvs"]` 是信用卡加超商代碼。 * 新增 409 `C026`(付款結果不明),回應帶 `"retryable": false`。 * 綁卡頁(`/checkout-token`)與預約定期定額:部分收單機構綁卡時會先授權新台幣 1 元再退回,頁面會事先告知消費者。 * 定期定額的結帳頁,付款人 Email 改為必填。 * 修正定期定額在特定情況下停止扣款的問題。 # 驗證與 token > Payment API 的 token 怎麼產生、怎麼帶、會不會過期、重新產生的影響,以及 401 的常見原因。 Payment API 用 CRM 產生的 token 驗證。每個網域在每個環境同時只有一把有效的 token。 ## 產生 token [Section titled “產生 token”](#產生-token) 1. 確認網域已開啟 API 串接(開發者模式)。沒開時 CRM 不會出現「開發者」分頁,請商家聯絡應援。 2. 進入 CRM「總設定」→「開發者」分頁 →「應援金流設定」→「存取 Token」,按「產生 Token」。 3. 立刻複製保存。離開頁面後就看不到完整的 token。 需要後台管理員,或「金流串接管理員」角色。詳細步驟見[交給工程師串接前的準備](/merchant/api-access/)。 ## 帶上 token [Section titled “帶上 token”](#帶上-token) ```http Authorization: Bearer ``` * `Bearer` 要照這個大小寫,中間只有一個空白。 * 有 body 的請求要帶 `merchantId`,而且必須是 token 所屬的網域,否則回 400 `V0001`。查詢類的 GET 請求不需要帶,網域由 token 決定。 ## token 的特性 [Section titled “token 的特性”](#token-的特性) | 特性 | 說明 | | ---- | ---------------------------------- | | 有效期限 | **沒有期限**,直到重新產生 | | 重新產生 | 舊 token **立刻失效**,沒有緩衝期 | | 環境 | 測試與正式各自產生,不能互用 | | 數量 | 每個網域、每個環境同時只有一把 | | 放在哪裡 | 只放伺服器。API 不接受瀏覽器跨網域請求,也不要放進前端或 App | 更換 token 會中斷服務 重新產生後,舊 token 立刻失效,CRM 也沒有確認視窗。要更換時,請事先安排: 1. 準備好能立即更新 token 的部署方式。 2. 在離峰時間產生新 token。 3. 立刻部署,並確認 API 呼叫恢復正常。 ## token 外洩時 [Section titled “token 外洩時”](#token-外洩時) 1. 立刻到 CRM 重新產生,讓外洩的 token 失效。 2. 更新伺服器設定。 3. 用[查詢交易列表](/api/list-transactions/)檢查外洩期間有沒有異常的建立或退款。 請不要把 token 貼到 email、聊天工具、AI 助手或公開的程式碼庫。 ## 收到 401 `A0001` [Section titled “收到 401 A0001”](#收到-401-a0001) | 可能原因 | 怎麼確認 | | ------------- | --------------------------------------------- | | token 被重新產生過 | 問商家最近有沒有按過「重新產生 Token」 | | 用錯環境 | 測試環境的 token 打到正式環境,或反過來 | | header 格式錯誤 | 確認是 `Authorization: Bearer `,沒有多餘空白或換行 | | 網域的 API 串接被關閉 | 請商家聯絡應援 | 正式環境收到的是 **403**(沒有 `code` 欄位)時,原因是 IP 還沒綁定,不是 token 的問題,見[固定 IP 白名單](/developers/ip-allowlist/)。 # 環境與測試 > 測試環境與正式環境的網址、token、IP 限制與付款通知的差異,以及測試付款時要注意的事。 ## 兩個環境 [Section titled “兩個環境”](#兩個環境) | | 測試環境 | 正式環境 | | -------------------- | --------------------------------------------- | -------------------------------------------------- | | API | `https://payment-api.testing.oen.tw` | `https://payment-api.oen.tw` | | 結帳頁 | `https://{網域名稱}.testing.oen.tw/checkout/{id}` | `https://{網域名稱}.oen.tw/checkout/{id}` | | CRM(產生 token、設定通知網址) | `https://{網域名稱}.testing.oen.tw/crm` | `https://{網域名稱}.oen.tw/crm` | | token | 測試環境 CRM 產生 | 正式環境 CRM 產生 | | 固定 IP 白名單 | 沒有 | **有**,要先申請,見[固定 IP 白名單](/developers/ip-allowlist/) | | 付款通知 | **會實際送到你設定的網址** | 會 | | 實際扣款 | 不會,連到收單機構的測試系統 | 會 | * 兩個環境的 token 不能互用,用錯會回 401 `A0001`。 * 兩個環境的通知網址要分別在各自的 CRM 設定。 * 測試環境帳號與網域需要請應援開設,請商家聯絡業務人員。 ## 各種頁面網址 [Section titled “各種頁面網址”](#各種頁面網址) API 只回傳 `id`,結帳頁網址要自己組: | API | 結帳頁路徑 | 有效時間 | | ------------------------------------------------------------ | ------------------------------------ | ----- | | [`POST /checkout`](/api/checkout/) | `/checkout/{id}` | 5 分鐘 | | [`POST /checkout-subscription`](/api/checkout-subscription/) | `/checkout/subscription/{id}` | 5 分鐘 | | [`POST /checkout-schedule`](/api/checkout-schedule/) | `/checkout/schedule/{id}` | 5 分鐘 | | [`POST /checkout-token`](/api/checkout-token/) | `/checkout/subscription/create/{id}` | 10 分鐘 | 有效時間從呼叫 API 的那一刻起算,不是從消費者打開頁面起算。 ## 測試付款 [Section titled “測試付款”](#測試付款) 測試環境連到收單機構的測試系統,不會真的扣款。 不要依賴舊文件的測試規則 舊版 Postman 文件寫「金額大於 100 元會成功、小於 100 元會失敗」,以及幾組固定的測試卡號。這些不是應援系統的規則,實際結果依收單機構的測試系統而定,請不要依賴。 * 各網域在測試環境使用的收單設定可能不同,可以用的測試卡號請向應援的聯絡人索取。 * 到期日請填未來的日期。結帳頁會直接擋下已過期的卡片。 * 要測試逾時,讓結帳頁停留超過 5 分鐘即可,頁面會導回 `failureUrl?payment_error=V0002`。 * 超商代碼、ATM 繳費完成的流程需要應援協助模擬,請聯絡聯絡人。 ## 測試付款通知 [Section titled “測試付款通知”](#測試付款通知) * 測試環境會把付款通知送到你在**測試環境 CRM** 設定的網址。 * 本機開發可以用 tunnel 工具取得公開的 https 網址。網址必須是 https、使用 443 port,而且可以從網際網路連到。 * 付款通知沒有「重送」按鈕,也沒有測試事件 API。要重複測試,請再建立一筆交易付款。 ## 換到正式環境 [Section titled “換到正式環境”](#換到正式環境) 1. 在正式環境 CRM 產生 token,更新伺服器設定。 2. 在正式環境 CRM 設定通知網址。 3. 把 API 網址與結帳頁網址中的 `.testing` 拿掉。 4. 確認已完成[固定 IP 白名單](/developers/ip-allowlist/)申請。 5. 走一遍[上線檢查清單](/developers/go-live/)。 # 錯誤處理 > 怎麼讀錯誤回應、哪些錯誤可以重試,以及付款結果不明、逾時時如何避免重複扣款。 ## 錯誤回應 [Section titled “錯誤回應”](#錯誤回應) ```json { "code": "V0001", "data": {}, "message": "PRODUCT_AMOUNT_NOT_MATCH" } ``` * 程式判斷請看 HTTP 狀態碼與 `code`。`message` 的文字可能調整,只用來記錄與除錯。 * 所有錯誤碼見[錯誤碼一覽](/api/error-codes/)。 ## 可不可以重試 [Section titled “可不可以重試”](#可不可以重試) | 你收到 | 可以重試嗎 | 怎麼做 | | ------------------- | ----------- | -------------------------------------------------- | | 400 `V0001` | 不行 | 依 `message` 修正參數 | | 400 `V0002` | 不行 | 先查詢目前狀態 | | 400 `T0001`~`T0005` | 不要用同一張卡立刻重試 | 請消費者換卡或聯絡發卡銀行 | | 400 `K0001`、`V0003` | 不行 | 聯絡應援 | | 401 `A0001` | 不行 | 檢查 token,見[驗證與 token](/developers/authentication/) | | 403(沒有 `code`) | 不行 | IP 還沒綁定,見[固定 IP 白名單](/developers/ip-allowlist/) | | 409 `C026` | **不行** | 付款結果不明,見下方 | | 500 `F0001` | 看情況 | 先看 `message`,見下方 | | 504、逾時、連線中斷 | **先查再說** | 請求可能已經成功,見下方 | ## 付款結果不明時,不要再扣一次 [Section titled “付款結果不明時,不要再扣一次”](#付款結果不明時不要再扣一次) Payment API 沒有 Idempotency-Key 同樣的請求送兩次,就會建立兩筆交易;用 token 扣款時就是**扣兩次**。所以結果不明時,絕對不要直接重送。 ### 409 `C026` [Section titled “409 C026”](#409-c026) 回應是: ```json { "code": "C026", "data": {}, "message": "PAYMENT_STATUS_UNKNOWN", "retryable": false } ``` 扣款可能已經發生,只是結果還沒確定。交易會停在 `charging`: 1. 不要重送,也不要建立新的訂單再扣一次。 2. 稍後用[訂單編號查詢](/api/list-order-transactions/)確認交易狀態。 3. 一段時間後仍是 `charging`,請帶交易編號聯絡應援。 ### 504、逾時、連線中斷 [Section titled “504、逾時、連線中斷”](#504逾時連線中斷) API 最多處理 29 秒,超過會回 504,但後面的處理可能還在進行,甚至已經成功。 1. 用[訂單編號查詢](/api/list-order-transactions/),看有沒有剛剛那筆交易。 2. 有,而且是 `charged` 或 `claimed`:當作成功,不要重送。 3. 有,而且是 `charging`:等一下再查。 4. 沒有,或是 `failed`:才可以重送。 ### 500 `F0001` [Section titled “500 F0001”](#500-f0001) | `message` | 原因 | 怎麼做 | | -------------------------------- | ------------------------------ | ------------------------- | | `Invalid date` | 查詢列表的 `start`、`end` 格式錯誤,或只帶一個 | 修正參數 | | `Pagination parse failed` | `page` 分頁標記無效 | 用上一頁回傳的原值 | | `Current charged amount too low` | 退款時,網域尚未撥款的金額低於 200 元 | 聯絡應援 | | JSON 解析錯誤 | body 不是合法的 JSON | 修正 body | | 其他 | 系統錯誤 | 查詢類可以稍後重試;建立交易、退款類請先查詢再決定 | ## 欄位驗證錯誤 [Section titled “欄位驗證錯誤”](#欄位驗證錯誤) * 驗證失敗回 400 `V0001`,`message` 會列出所有不符合的規則,例如 `must have required property 'productDetails'; must be >= 1`。 * `message` 不一定有欄位名稱,請對照 API 參考的欄位表逐一檢查。 * 規格裡沒有的欄位會被忽略,不會報錯。欄位名稱拼錯時,常見的現象是「送了但沒效果」或「必填欄位缺少」。 ## 常見錯誤 [Section titled “常見錯誤”](#常見錯誤) | message | 原因 | | ---------------------------------------------- | ------------------------------------------------ | | `must have required property 'productDetails'` | 沒帶商品明細。所有建立交易的 API 都要帶 | | `PRODUCT_AMOUNT_NOT_MATCH` | 品項「數量 × 單價」的合計不等於 `amount` | | `USER_NAME_AND_EMAIL_REQUIRED` | 網域有開通電子發票,但沒帶 `userName` 或 `userEmail` | | `INVALID_PARAMS` | `merchantId` 和 token 的網域不同;或查詢的交易不屬於你的網域 | | `PAYMENT_SERVICE_NOT_ACTIVATE` | 網域的金流服務還沒開通 | | `LINE_PAY_NOT_ACTIVE` | 帶了 `linePay`,但 LINE Pay 還沒開通 | | `START_DATE_MUST_GREATER_THAN_TODAY` | `startDate` 早於今天(用 token 建立定期定額時,今天也不行) | | `must match format "uri"` | `successUrl` 或 `failureUrl` 不是完整網址,少了 `https://` | ## 回報問題時 [Section titled “回報問題時”](#回報問題時) 請提供:網域名稱、環境(測試或正式)、呼叫時間、端點、交易編號或 `orderId`,以及完整的錯誤回應。**不要附上 token。** # 上線檢查清單 > 切換到正式環境前逐項確認:token、固定 IP、付款通知、結果判斷、重試與對帳。 上線前逐項確認。每一項都附上說明頁。 ## 帳號與設定 [Section titled “帳號與設定”](#帳號與設定) * [ ] 正式環境的 token 已產生,只放在伺服器的環境變數或密鑰管理服務,沒有進版本控制。([驗證與 token](/developers/authentication/)) * [ ] 伺服器的對外 IP 已綁定,從正式環境呼叫查詢 API 不會回 403。([固定 IP 白名單](/developers/ip-allowlist/)) * [ ] 正式環境 CRM 已設定「交易資料回傳網址位置」,是 https、443 port、可以從外部連到。([付款通知](/developers/webhooks/)) * [ ] API 與結帳頁網址已經拿掉 `.testing`。 * [ ] 要用的付款方式都已開通,結帳頁實際出現的方式符合預期(信用卡一定會出現)。([付款方式](/developers/payment-methods/)) * [ ] 需要退款、取消通知的話,已請應援開啟「Webhook 新版事件」。 ## 建立交易 [Section titled “建立交易”](#建立交易) * [ ] 每一筆都帶 `productDetails`,品項合計等於 `amount`。 * [ ] 消費者按下付款時才建立結帳,建立後立即導轉(結帳頁 5 分鐘內有效)。 * [ ] `successUrl`、`failureUrl` 用路徑或參數帶上自己的訂單編號,`failureUrl` 沒有自帶 `?`。 * [ ] 網域有開通電子發票時,有帶 `userName` 與 `userEmail`。 ## 判斷結果 [Section titled “判斷結果”](#判斷結果) * [ ] 出貨以回查結果為準,不是導回網址,也不是付款通知的內容。 * [ ] 收到付款通知後用 `id` 呼叫 `GET /transactions/{id}` 回查,並比對 `orderId` 與金額。 * [ ] `charged` 與 `claimed` 都視為付款成功。 * [ ] 收到 `charging`(超商、ATM、LINE Pay)時不出貨,等下一則通知。 * [ ] 付款通知的接收端在 10 秒內回 200,耗時工作放到背景;同一則通知重複收到也不會重複出貨。 ## 重試與對帳 [Section titled “重試與對帳”](#重試與對帳) * [ ] 逾時、504、409 `C026` 時不直接重送,先用訂單編號查詢。([錯誤處理](/developers/errors/)) * [ ] 有定期工作處理「超過 15 分鐘還沒結果」的訂單,補救漏掉的付款通知。 * [ ] 同一個 `orderId` 有兩筆以上成功時,會通知人員處理。 * [ ] 每天和應援的交易資料對帳一次。([查詢與對帳](/developers/querying/)) ## 定期定額與存卡(有用到才需要) [Section titled “定期定額與存卡(有用到才需要)”](#定期定額與存卡有用到才需要) * [ ] 已在 CRM 開啟「定期交易重試機制」,或有機制處理一期失敗後整個停止的情況。([定期定額](/developers/subscriptions/)) * [ ] 消費者提出取消時,會立即呼叫取消 API。 * [ ] token 通知的接收端穩定,並用 `customId` 對應會員;token 只存在伺服器。([存卡與後續扣款](/developers/saved-cards/)) * [ ] 用 token 扣款逾時時,先查詢再決定是否重送。 ## 退款 [Section titled “退款”](#退款) * [ ] 退款前確認交易還沒退過,金額一次決定好。 * [ ] 超商代碼、ATM 退款會帶退款帳戶。 * [ ] 有開發票的交易,退款會帶 `productDetails`。 ## 最後測試 [Section titled “最後測試”](#最後測試) * [ ] 在測試環境跑過:付款成功、付款失敗、結帳頁逾時、消費者放棄付款、退款。 * [ ] 上線後用正式環境跑一筆小額交易並退款,確認付款通知、回查與退款都正常。 # 固定 IP 白名單 > 正式環境的 Payment API 只接受已綁定的 IP。要準備什麼、怎麼申請、被擋下時會看到什麼。 正式環境一定要先綁定 正式環境 `payment-api.oen.tw` 只接受已綁定的 IP,其他來源的請求一律回 HTTP 403。**測試環境沒有這個限制**,所以常見的狀況是「測試環境都正常,一上線就 403」。 ## 哪些會受影響 [Section titled “哪些會受影響”](#哪些會受影響) | | 要綁定 IP 嗎 | | ------------------------------------ | -------- | | 你的伺服器呼叫 `payment-api.oen.tw` | **要** | | 你的伺服器呼叫 `payment-api.testing.oen.tw` | 不用 | | 消費者打開應援結帳頁付款 | 不用 | | 應援送付款通知給你 | 不用(方向相反) | 透過已和應援串接好的合作平台收款的商家,不需要申請。 ## 準備固定的對外 IP [Section titled “準備固定的對外 IP”](#準備固定的對外-ip) 你的伺服器呼叫 API 時,對外使用的 IP 必須固定: * 自己的主機或 VPS:通常就是主機的公開 IP。 * 雲端服務(容器、Serverless、自動擴展的主機):對外 IP 通常會變動,請設定 NAT Gateway 或固定出口 IP,讓所有呼叫都從固定的 IP 出去。 * 有多台伺服器、多個區域或備援機房時,每一個出口 IP 都要申請。 要確認伺服器實際的對外 IP,可以在伺服器上執行 `curl https://checkip.amazonaws.com`。 ## 申請 [Section titled “申請”](#申請) 1. 準備:網域名稱、要綁定的 IP 清單。 2. 請商家聯絡應援客服或業務人員提出申請。 3. 應援完成設定後會通知你。 4. 從伺服器呼叫一次查詢類的 API(例如[查詢交易列表](/api/list-transactions/)),確認不再回 403。 更換主機或出口 IP 時,請**先**申請新的 IP,確認生效後再切換,避免中斷。 ## 被擋下時會看到什麼 [Section titled “被擋下時會看到什麼”](#被擋下時會看到什麼) ```http HTTP/1.1 403 Forbidden {"message":"Forbidden"} ``` * body 只有 `message`(通常是 `Forbidden`),**沒有** `code` 欄位,和 API 自己的錯誤格式不同。 * 用這點就能分辨:有 `code: "A0001"` 是 token 的問題;沒有 `code` 的 403 是 IP 還沒綁定。 ## 付款通知的來源 IP [Section titled “付款通知的來源 IP”](#付款通知的來源-ip) 你的防火牆需要限制來源時,請向應援索取付款通知的來源 IP。付款通知從固定的出口送出。 # 單次付款 > 用 POST /checkout 建立結帳頁、處理 5 分鐘時效與重新付款、判斷付款結果、帶入發票資訊,以及各種付款方式的流程差異。 ## 流程 [Section titled “流程”](#流程) 1. 消費者在你的網站按下「付款」。 2. 你的伺服器呼叫 [`POST /checkout`](/api/checkout/),拿到 `id` 與 `transactionHid`,存進訂單。 3. 把消費者導到 `https://{網域名稱}.oen.tw/checkout/{id}`。 4. 消費者在應援結帳頁選擇付款方式並付款。 5. 應援送出[付款通知](/developers/webhooks/)到你的伺服器,你用 [`GET /transactions/{id}`](/api/get-transaction/) 回查後更新訂單。 6. 消費者被導回 `successUrl` 或 `failureUrl`。 ## 建立結帳的重點 [Section titled “建立結帳的重點”](#建立結帳的重點) * **`productDetails` 必填**,各品項「數量 × 單價」的合計必須等於 `amount`。有折扣或運費時,請調整品項(例如把折扣攤進單價、把運費列成一個品項),讓合計等於實付金額。 * **金額是新台幣整數**。`currency` 可以不帶,預設就是 `TWD`。 * **網址自己組**:回應只有 `id`,結帳頁是 `https://{網域名稱}.oen.tw/checkout/{id}`,測試環境是 `{網域名稱}.testing.oen.tw`。 * **沒有的欄位會被忽略**:例如 `webhookUrl`、`cancelUrl` 都不是 Payment API 的欄位,送了也不會有效果。付款通知網址在 CRM 設定。 完整欄位見 [API 參考](/api/checkout/)。 ## 結帳頁 5 分鐘內有效 [Section titled “結帳頁 5 分鐘內有效”](#結帳頁-5-分鐘內有效) 結帳頁從你呼叫 `POST /checkout` 的那一刻起算,**5 分鐘**內有效: * 消費者停在結帳頁、倒數到 0 時,會看到「付款時限已過」,8 秒後被導回 `failureUrl?payment_error=V0002`。 * 過期後才打開連結,會看到「此付款連結資訊已過期」,**不會**導回你的網站。 所以請在消費者真的要付款時才建立,建立後立刻導過去。不要先建好連結再用 email 或訊息寄給消費者。 5 分鐘後結果仍可能改變 5 分鐘只限制「打開結帳頁」。消費者在時限內送出付款後,3D 驗證、LINE Pay 確認、超商或 ATM 繳費的結果,都可能在 5 分鐘之後才確定。不要在 5 分鐘到的時候就把訂單判定為失敗、之後又不接受成功的通知。 ## 消費者要重新付款 [Section titled “消費者要重新付款”](#消費者要重新付款) 消費者付款失敗、逾時或改變心意時,重新呼叫 `POST /checkout` 建立新的交易即可: * **同一個 `orderId` 可以建立多筆交易**,應援不會擋。 * 每一筆都是獨立的交易,有自己的 `id`。 * 極少數情況下,消費者可能在兩個結帳頁都完成付款。請用 [`GET /order/{orderId}/transactions`](/api/list-order-transactions/) 檢查同一個訂單有幾筆成功,多的那筆請[退款](/developers/refunds/)。 ## 消費者放棄付款 [Section titled “消費者放棄付款”](#消費者放棄付款) 消費者沒有付款、直接離開時,**應援不會送任何通知**,交易會一直停在 `initiated`。請在你的系統自行處理,例如: * 建立後 30 分鐘仍沒有結果的訂單,先用[訂單編號查詢](/api/list-order-transactions/)確認沒有成功或處理中的交易,再把訂單標為未付款。 * 查到 `charging`(超商、ATM 等待繳費,或 LINE Pay 等待確認)時,繼續等待。 ## 判斷付款結果 [Section titled “判斷付款結果”](#判斷付款結果) | 你看到的 | 意思 | 怎麼做 | | -------------------------------- | -------------------------------- | -------- | | `status` 是 `charged` 或 `claimed` | 付款成功 | 出貨 | | `status` 是 `charging` | 處理中:超商、ATM 已取號等待繳費;LINE Pay 等待確認 | 等下一則通知 | | `status` 是 `failed` | 付款失敗或逾期未繳 | 讓消費者重新付款 | | `status` 是 `initiated` | 消費者還沒送出付款 | 等待,或視為放棄 | 一律以[回查](/api/get-transaction/)的結果為準,不要只看付款通知或導回網址。 ## 導回網址 [Section titled “導回網址”](#導回網址) * **成功**:導回 `successUrl`,**不帶任何參數**。請在網址放自己的訂單編號,例如 `?order=A001`,頁面再到你的後端查訂單狀態。 * **失敗**:導回 `failureUrl`,加上 `payment_error`,可能的值見[錯誤碼一覽](/api/error-codes/#%E7%B5%90%E5%B8%B3%E9%A0%81%E5%B0%8E%E5%9B%9E%E7%9A%84-payment_error)。`#` 之後的片段會被拿掉。 * **超商代碼、ATM**:消費者取號後停在應援的繳費資訊頁,按「返回網站」才回到 `successUrl`。 * **LINE Pay**:失敗或取消時導回 `failureUrl`,不帶 `payment_error`。 failureUrl 不要自帶參數 部分 3D 驗證失敗的情況,會直接在 `failureUrl` 後面接上 `?payment_error=T0001`。`failureUrl` 原本就有 `?` 時,網址會變成兩個 `?`。請讓 `failureUrl` 用路徑帶訂單編號,例如 `https://shop.example.com/payment/failure/A001`。 ## 付款方式 [Section titled “付款方式”](#付款方式) 用 `allowedPaymentMethods` 加開超商代碼、LINE Pay、ATM。**信用卡一定會出現**,Apple Pay 條件符合時自動出現。詳見[付款方式](/developers/payment-methods/)。 ## 3D 驗證 [Section titled “3D 驗證”](#3d-驗證) * `use3d: true` 時,信用卡付款會走 3D 驗證。預設是 `false`。 * 應援可以把你的網域設定為一律 3D,這時不管帶什麼都會走 3D。 * 3D 驗證開始後 10 分鐘沒有完成,交易會變成失敗,並送出 `message` 為 `3DS_ABANDONED` 的付款通知。 * Apple Pay 不走 3D 驗證。 ## 電子發票 [Section titled “電子發票”](#電子發票) 網域開通「應援代開電子發票」後,付款成功會自動開立發票: * `userName` 與 `userEmail` 變成必填,發票通知寄到 `userEmail`。 * 發票品項來自 `productDetails`。 * 要開手機條碼、自然人憑證或公司戶發票,帶 `invoiceInfo`: ```json { "invoiceInfo": { "invoiceType": "cloud", "carrierType": "3J0002", "carrierId": "/ABC+123" } } ``` ```json { "invoiceInfo": { "invoiceType": "company", "buyerIdentifier": "12345675", "buyerName": "應援範例股份有限公司" } } ``` 手機條碼會即時向財政部驗證,不存在的條碼會回 400 `V0001`(`INVALID_CARRIER_ID`);統一編號會檢查檢查碼(`INVALID_TAX_ID_NUMBER`)。沒帶 `invoiceInfo` 時開立雲端發票。 ## 帶上你自己的資料 [Section titled “帶上你自己的資料”](#帶上你自己的資料) * `customId`:原樣出現在付款通知與查詢結果,適合放購物車編號等資料。 * `userId`:你系統裡的會員編號,會出現在查詢結果與 CRM。 * `note`:備註,會出現在 CRM 金流明細。 # 付款方式 > 結帳頁支援的付款方式、開通條件、金額範圍與繳費期限,以及 allowedPaymentMethods 的實際效果。 ## 支援的付款方式 [Section titled “支援的付款方式”](#支援的付款方式) | 付款方式 | `allowedPaymentMethods` 的值 | 開通 | 金額範圍(新台幣) | 付款期限 | | --------- | -------------------------- | ------------------------------- | ------------------------ | --------- | | 信用卡 | `card`(一定會出現) | 隨金流開通 | 1 以上;200,000 以上要另外開通高額交易 | — | | Apple Pay | 不用指定,自動出現 | 隨信用卡 | 同信用卡 | — | | 超商代碼 | `cvs` | 請應援開通 | 80 到 20,000 | 取號後 48 小時 | | ATM 虛擬帳號 | `atm` | 請應援開通(需要藍新金流) | 100 到 20,000 | 3 天 | | LINE Pay | `linePay` | 自備 LINE Pay 商店帳號,請應援開通後在 CRM 設定 | 30 到 200,000 | 20 分鐘 | * 不支援 Google Pay、信用卡分期。 * 幣別只支援新台幣。 * 金額不在範圍內的付款方式,結帳頁不會顯示。 * 超商代碼可以繳費的超商依網域設定而定:全家,或 7-ELEVEN、全家、OK、萊爾富。 * 只有[單次付款](/api/checkout/)可以選擇付款方式。定期定額與存卡只支援信用卡。 ## allowedPaymentMethods 的實際效果 [Section titled “allowedPaymentMethods 的實際效果”](#allowedpaymentmethods-的實際效果) 這是「加開」,不是「只開」 信用卡一定會出現在結帳頁,目前沒辦法只開超商代碼或只開 LINE Pay。 | 你傳入 | 結帳頁會出現 | | -------------------- | -------------------------------------------- | | 不傳,或 `[]` | 信用卡(+ Apple Pay) | | `["cvs"]` | 信用卡、超商代碼(+ Apple Pay) | | `["linePay"]` | 信用卡、LINE Pay(+ Apple Pay) | | `["atm"]` | 信用卡、ATM(+ Apple Pay)。網域沒有設定藍新 ATM 時,ATM 不會出現 | | `["cvs", "linePay"]` | 信用卡、超商代碼、LINE Pay(+ Apple Pay) | * Apple Pay 只在支援的裝置與瀏覽器(例如 iPhone 的 Safari)顯示。 * 帶 `linePay` 但網域的 LINE Pay 還沒開通時,建立結帳會直接回 400 `V0001`(`LINE_PAY_NOT_ACTIVE`)。 * 帶 `cvs` 但網域沒有開通超商代碼時,建立結帳會成功,但消費者選超商時會失敗。請先確認開通狀態。 * 從 v10.3.0 起,結帳頁會照你傳入的清單顯示並把關。 ## 各付款方式的流程差異 [Section titled “各付款方式的流程差異”](#各付款方式的流程差異) ### 信用卡、Apple Pay [Section titled “信用卡、Apple Pay”](#信用卡apple-pay) 消費者當場付款,成功後立刻導回 `successUrl`,付款通知的 `status` 是 `charged` 或 `claimed`。 ### 超商代碼、ATM [Section titled “超商代碼、ATM”](#超商代碼atm) 警告 這兩種方式會收到**兩則**付款通知,而且 `id` 相同: 1. 取號時:`status: charging`,沒有 `success` 欄位,`paymentInfo` 裡有繳費代碼或虛擬帳號。 2. 繳費後:`status: charged`、`success: true`;逾期未繳則是 `status: failed`、`message: PAYMENT_EXPIRED`。 收到第一則時不要出貨。 * 取號後,消費者停在應援的繳費資訊頁,按「返回網站」才回到 `successUrl`。建議你也在訂單頁顯示繳費代碼與期限(取自付款通知的 `paymentInfo`)。 * 退款要帶消費者的銀行帳戶,由應援匯款,見[退款](/developers/refunds/)。尚未繳費的超商代碼可以用退款 API 直接取消。 ### LINE Pay [Section titled “LINE Pay”](#line-pay) * 消費者被導到 LINE Pay 付款,確認前會先收到 `status: charging` 的通知,確認後再收到最終結果。 * 20 分鐘內沒有完成,交易會變成失敗(`PAYMENT_EXPIRED`)。 * 付款失敗或取消時導回 `failureUrl`,不帶 `payment_error`。 * LINE Pay 的款項由 LINE Pay 直接撥給商家,付款成功的狀態是 `claimed`。 ## 商家要先開通 [Section titled “商家要先開通”](#商家要先開通) 各付款方式的開通方式,請把[開通金流與付款方式](/merchant/activate/)轉給商家。 # 查詢與對帳 > 三種查詢交易的方式怎麼選、列表的範圍與分頁,以及建議的每日對帳流程。 ## 三種查詢 [Section titled “三種查詢”](#三種查詢) | 想知道 | 用這支 | 備註 | | ----------- | -------------------------------------------------------------------- | ------------------------------------ | | 某一筆交易的最新狀態 | [`GET /transactions/{id}`](/api/get-transaction/) | 用 27 字元的 `id` 查,才會有 `productDetails` | | 某個訂單的所有付款嘗試 | [`GET /order/{orderId}/transactions`](/api/list-order-transactions/) | 只有 API 交易,含失敗與定期定額各期;一次回傳全部 | | 一段期間的所有款項 | [`GET /transactions?start=&end=`](/api/list-transactions/) | 每頁 50 筆,由新到舊;**包含非 API 的款項** | | 定期定額的狀態 | [`GET /subscriptions/{id}`](/api/get-subscription/) | 用 `S` 開頭的編號或內部 id | ## 交易列表的範圍 [Section titled “交易列表的範圍”](#交易列表的範圍) 列表包含網域的所有款項 `GET /transactions` 列出的是整個網域的款項,除了 API 建立的交易,也包含應援商店訂單、捐款等其他收款,以及撥款後退款產生的負數調整款項。這些款項的 `id` 是 `C` 開頭。只要 API 交易時,請用 `orderId` 比對你的訂單,或篩選 `id` 為 `P` 開頭的項目。 ### 期間與分頁 [Section titled “期間與分頁”](#期間與分頁) * `start` 與 `end` 要一起帶,以台北時間的整天計算:`start` 當天 00:00 到 `end` 當天 23:59:59。 * 日期可以寫 `2026-09-01`,或 Unix **毫秒**時間戳。只帶一個或格式錯誤會回 500 `F0001`(`Invalid date`)。 * 依交易**建立時間**篩選,不是付款時間。 * 回應的 `page` 不是 `null` 時,把它原封不動放進下一次請求的 `?page=`,直到 `page` 是 `null`。 ```js async function listAll(start, end) { const all = []; let page = null; do { const qs = new URLSearchParams({ start, end, ...(page ? { page } : {}) }); const res = await fetch(`https://payment-api.oen.tw/transactions?${qs}`, { headers: { Authorization: `Bearer ${process.env.OEN_API_TOKEN}` }, }); const result = await res.json(); if (result.code !== "S0000") throw new Error(`${result.code} ${result.message}`); all.push(...result.data.transactions); page = result.data.page; } while (page); return all; } ``` ## 建議的對帳流程 [Section titled “建議的對帳流程”](#建議的對帳流程) 1. **每 15 分鐘**:處理還沒有結果的訂單(做法見[沒收到通知時](/developers/webhooks/#%E6%B2%92%E6%94%B6%E5%88%B0%E9%80%9A%E7%9F%A5%E6%99%82))。 2. **每天**:用 `GET /transactions` 列出前一天建立的款項,只取 `P` 開頭的項目,和你的訂單逐筆比對: * 你的系統是「已付款」,應援是 `failed` 或 `initiated`:查明原因,不要出貨。 * 應援是 `charged` 或 `claimed`,你的系統不是「已付款」:補標。 * 同一個 `orderId` 有兩筆以上成功:[退款](/developers/refunds/)多的那筆。 3. **每次撥款後**:到 CRM「撥款列表」匯出撥款細項,核對手續費與撥款金額。見[查詢交易與對帳](/merchant/transactions/)。 ## 查詢的注意事項 [Section titled “查詢的注意事項”](#查詢的注意事項) * 請不要高頻率地輪詢單筆交易。付款通知到了再查,或用上面的定期工作即可。 * 交易剛完成的極短時間內,查到的可能還是舊狀態,稍等幾秒再查。 * 用 `P` 開頭的交易編號查詢單筆時,回應不含 `productDetails` 與 `numberOfPeriods`。 * 回應裡沒有 `currency` 欄位,Payment API 的交易都是新台幣。 # 退款 > 用 API 退款的規則:每筆只能退一次、各付款方式的差異、超商與 ATM 的退款帳戶、發票折讓,以及退款失敗時怎麼處理。 用 [`POST /refunds/{transactionHid}`](/api/refund/) 退款。路徑要用 `P` 開頭的交易編號,不是 27 字元的 `id`。 每筆交易只能退一次 不論用 API 或 CRM,每筆交易都只能成功退款一次。部分退款之後,剩下的金額不能再退。請一次決定好金額。 ## 可以退的交易 [Section titled “可以退的交易”](#可以退的交易) 交易狀態必須是 `charged` 或 `claimed`,而且還沒退過款。 | 原付款方式 | 怎麼退 | 要帶 `remitInfo` 嗎 | 退款後的狀態 | | ------------- | ------------- | ---------------- | ------------------------------------- | | 信用卡、Apple Pay | 即時退回卡片 | 不用 | `refunded`;已撥款的是 `refundedPostPayout` | | LINE Pay | 即時退回 LINE Pay | 不用 | `refunded` | | 超商代碼、ATM(已繳費) | 由應援匯款到消費者的帳戶 | **要** | `refunding`,匯款完成後更新 | | 超商代碼(還沒繳費) | 取消繳費代碼 | 不用 | `cancelled` | * `amount` 不帶就是全額退款;有帶就是部分退款,不能大於原金額。 * 退款成功時,回應的 `status` 已經是新的狀態,`refundAmount` 是這次退款的金額(超商、ATM 要等匯款完成才更新)。 ## 超商代碼與 ATM [Section titled “超商代碼與 ATM”](#超商代碼與-atm) 已繳費的交易要帶消費者的銀行帳戶: ```json { "merchantId": "ming", "reason": "商品缺貨", "remitInfo": { "bankCode": "004", "bankName": "臺灣銀行", "branchCode": "0037", "branchName": "營業部", "account": "12345678901234", "accountName": "王小明" } } ``` * 6 個欄位都必填;`account` 是 8 到 14 字元。 * 退款會停在 `refunding`,由應援處理匯款。 ## 有開電子發票的交易 [Section titled “有開電子發票的交易”](#有開電子發票的交易) 原交易有開電子發票時: * **`productDetails` 必填**,品項合計必須等於這次的退款金額,用來開立折讓。 * 信用卡與 LINE Pay 退款成功後,系統會自動開立折讓。 * 發票還沒開出就全額退款時,系統直接取消待開的發票。 ```json { "merchantId": "ming", "amount": 600, "reason": "消費者取消一包", "productDetails": [ { "productionCode": "SKU-001", "description": "手沖咖啡豆 200g", "quantity": 1, "unit": "包", "unitPrice": 600 } ] } ``` ## 退款失敗 [Section titled “退款失敗”](#退款失敗) | 你收到 | 意思 | 怎麼做 | | -------------------------------------------- | ----------------------------- | ------------------------------------------- | | 400 `V0002` | 交易狀態不能退款:還沒付款、已經退過,或正在處理另一筆退款 | 用[查詢交易明細](/api/get-transaction/)確認狀態 | | 400 `R0001` | 收單機構拒絕退款 | 先查詢交易狀態。之後重試都回 `V0002` 時,代表退款結果需要人工確認,請聯絡應援 | | 400 `V0001` `REMITTANCE_INFO_REQUIRED` | 已繳費的超商代碼或 ATM 沒帶 `remitInfo` | 補上退款帳戶 | | 400 `V0001` `PRODUCT_DETAILS_REQUIRED` | 有開發票的交易沒帶 `productDetails` | 補上折讓品項 | | 500 `F0001` `Current charged amount too low` | 你的網域尚未撥款的已收款金額低於 200 元,暫時無法退款 | 請聯絡應援 | | 逾時、500 其他訊息 | 結果不明 | **不要直接重送**。先查詢交易的 `status` 與 `refundAmount` | ## 已撥款的交易 [Section titled “已撥款的交易”](#已撥款的交易) 款項已經撥給商家後才退款,交易會變成 `refundedPostPayout`,退款金額與退款手續費會從下一次撥款扣除。 ## 退款通知 [Section titled “退款通知”](#退款通知) 退款不會送 `charge` 通知。要收到退款通知(`purpose: refund`),請應援為你的網域開啟「Webhook 新版事件」,欄位見[付款通知內容](/api/objects/webhook/#refund%E9%9C%80%E9%96%8B%E5%95%9F)。 ## 在 CRM 退款 [Section titled “在 CRM 退款”](#在-crm-退款) 商家也可以在 CRM 的金流明細退款,規則相同,見[商家指南的退款說明](/merchant/refunds/)。 # 存卡與後續扣款 > 讓消費者在綁卡頁完成 3D 驗證,透過付款通知拿到 token,之後由你的伺服器用 token 扣款或建立定期定額。 存卡適合「金額或時間由你決定」的情境,例如儲值、按用量計費、一鍵回購。消費者只需要綁一次卡,之後由你的伺服器用 token 扣款,消費者不必在場。 ## 流程 [Section titled “流程”](#流程) 1. 你的伺服器呼叫 [`POST /checkout-token`](/api/checkout-token/),拿到 `id`。 2. 把消費者導到 `https://{網域名稱}.oen.tw/checkout/subscription/create/{id}`。**10 分鐘內有效。** 3. 消費者輸入卡片並完成 3D 驗證。 4. 應援送出 `purpose` 為 `token` 的[付款通知](/api/objects/webhook/#token),裡面有 `token`。把它存進這位會員的資料。 5. 消費者被導回 `successUrl`(**不帶 token**)。 6. 之後要扣款時,呼叫 [`POST /token/transactions`](/api/token-transactions/) 或 [`POST /token/subscriptions`](/api/token-subscriptions/)。 token 只能從付款通知拿到 * 導回網址不帶 token,也**沒有 API 可以查詢 token**。 * 付款通知最多送 3 次(間隔約 2 秒、4 秒),都失敗就不會再送,這個 token 就拿不回來,只能請消費者重新綁卡。 * 請確認接收端穩定,並用 `customId` 帶上會員編號,收到通知時才知道是誰的卡。 ## 綁卡頁 [Section titled “綁卡頁”](#綁卡頁) * 消費者一定要填 Email。建立時帶 `payerEmail` 可以預先填好。 * 部分收單機構綁卡時會**先授權新台幣 1 元再立即退回**,頁面會事先告知消費者,也可能要求填寫英文持卡人姓名。 * 逾時(10 分鐘)後導回 `failureUrl?payment_error=Y003`。 * 3D 驗證開始後 10 分鐘沒完成,會送出 `success: false`、`message: 3DS_ABANDONED` 的通知。 ## token 的特性 [Section titled “token 的特性”](#token-的特性) | 特性 | 說明 | | -------- | --------------------------------- | | 有效期限 | 沒有期限。卡片本身過期後,扣款會被發卡銀行拒絕 | | 同一張卡再綁一次 | 會得到**新的** token,舊的仍然可以用 | | 刪除 | 沒有刪除 API。會員要求移除卡片時,請在你的系統刪除 token | | 換卡 | 沒有更新卡片的 API。請消費者重新綁卡,換成新的 token | | 保存 | token 可以直接扣款,請當成密碼一樣保存,不要傳到前端 | ## 用 token 扣款 [Section titled “用 token 扣款”](#用-token-扣款) ```bash curl -X POST "https://payment-api.testing.oen.tw/token/transactions" \ -H "Authorization: Bearer $OEN_API_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "merchantId": "ming", "amount": 1200, "token": "2HhnhWm8Qe0sPz4LbXv9KdYt1Ra", "orderId": "A20260928003", "productDetails": [ { "productionCode": "SKU-001", "description": "手沖咖啡豆 200g", "quantity": 2, "unit": "包", "unitPrice": 600 } ] }' ``` * 不走 3D 驗證,**結果就是 API 回應**,不會送付款通知。 * 成功回傳 `id`(`P` 開頭的交易編號)與 `authCode`。 * 失敗回 400 與 `T0001`~`T0005`。失敗的交易也會建立,可以用訂單編號查到。 * 只能是新台幣,`amount` 必須是整數。 * 單筆 200,000 元以上需要網域開通高額交易,否則回 `V0003`。 不要直接重送 Payment API 沒有防重複扣款的機制:同樣的請求送兩次,就會扣兩次。遇到下列情況時,**先用[訂單編號查詢](/api/list-order-transactions/)確認**,再決定要不要重送: * 請求逾時,或收到 HTTP 504、500。 * 收到 409 `C026`(付款結果不明)。這時請不要重送,稍後再查詢;久未確定請聯絡應援。 ## 用 token 建立定期定額 [Section titled “用 token 建立定期定額”](#用-token-建立定期定額) [`POST /token/subscriptions`](/api/token-subscriptions/) 用 token 建立定期定額,之後由應援自動扣款: * 不帶 `startDate`:當下扣第一期,結果以回應為準;第一期失敗時定期定額不會成立。 * 帶未來日期:只建立排程(狀態 `scheduled`),**不會先驗證 token**,到期才扣款。 扣款時間與失敗處理見[定期定額](/developers/subscriptions/)。 # 定期定額 > 三種建立定期定額的方式怎麼選、每期何時扣款、扣款失敗的處理與重扣設定、取消與查詢,以及要注意的日期規則。 定期定額是「消費者同意一次,之後由應援依週期自動扣款」。只支援信用卡,扣款週期以月為單位。 ## 三種建立方式 [Section titled “三種建立方式”](#三種建立方式) | | [建立定期定額](/api/checkout-subscription/) | [建立預約定期定額](/api/checkout-schedule/) | [用 token 建立定期定額](/api/token-subscriptions/) | | ----------- | ------------------------------------- | ----------------------------------- | ------------------------------------------- | | 消費者要做的事 | 在結帳頁付第一期 | 在結帳頁綁卡(首期是今天時同時付款) | 不用,之前已經[綁卡](/developers/saved-cards/) | | 第一期何時扣 | 消費者付款當下 | `startDate`:今天就是付款當下,未來日期就在當天扣 | 不帶 `startDate` 就在呼叫 API 當下;帶未來日期就在當天扣 | | 扣款間隔 | 固定每 1 個月 | 1 到 12 個月(`paymentInterval`) | 1 到 12 個月 | | `startDate` | 不支援 | 今天到 12 個月內 | **必須晚於今天**,12 個月內 | | 回應 | `id`、`transactionHid` | `id`、`subscriptionHid` | `subscriptionId`、`transactionId`、`authCode` | | 適合 | 最簡單的月費 | 季繳、年繳、下個月才開始扣 | 已經有存卡的會員 | * `numberOfPeriods` 是總期數,最少 2 期;不帶就是不限期,直到取消。 * 每期金額固定,就是建立時的 `amount`。要改金額,請取消後重新建立。 * 定期定額編號是 `S` 開頭,查詢與取消都用它。用「建立定期定額」時,第一期付款成功後,付款通知與查詢結果的 `subscriptionId` 就是這個編號。 ## 每期什麼時候扣款 [Section titled “每期什麼時候扣款”](#每期什麼時候扣款) * 應援每天**台北時間上午 9 點**執行扣款。 * 下一期日期 = 上一期實際扣款的日期 + 間隔月數。 * 每次扣款前 3 天,應援會寄扣款提醒信給消費者(有 Email 時)。 * 每一期的結果都會送出 `purpose` 為 `charge`、`action` 為 `subscription` 的[付款通知](/api/objects/webhook/)。 日期會往前或往後移 * 起始日是 29、30、31 日時,遇到較短的月份會移到較早的日期,之後就維持在那一天。例如 1/31 起始,之後可能是 2/28、3/28。 * 某一期扣款失敗、重扣成功後,之後各期會以重扣成功的日期往後推。 如果你的系統需要固定在某一天扣款,請把起始日設在每月 28 日以前。以 [`GET /subscriptions/{id}`](/api/get-subscription/) 的 `nextChargeAt` 為準。 ## 扣款失敗 [Section titled “扣款失敗”](#扣款失敗) 預設不重扣:一期失敗,整個定期定額就停止 重扣機制**預設是關閉的**。某一期扣款失敗時,定期定額會變成 `error`,**之後所有期數都不會再扣款**,也不會自動恢復。 建議開啟重扣:CRM「總設定」→「開發者」→「定期交易重試機制(Beta)」→「定期購買」。開啟後: * 失敗後最多重扣 **2 次**,每次間隔約 24 小時。 * 發卡銀行明確拒絕(例如卡片停用)或付款結果不明時,不會重扣。 * 重扣中的狀態是 `retryScheduled`;重扣成功就回到 `ongoing`,用完仍失敗就變成 `error`。 * 每一次重扣都會送付款通知,`isRetry` 為 `true`。 定期定額停止後,請聯絡消費者重新訂閱,例如換一張卡走一次[建立定期定額](/api/checkout-subscription/)。 ## 取消 [Section titled “取消”](#取消) 呼叫 [`PUT /subscriptions/{subscriptionHid}`](/api/cancel-subscription/),或在 CRM「定期購買」按「終止定期購買」。 * 只接受 `S` 開頭的定期定額編號。 * **請一定要送 body**(至少 `merchantId`),沒送會回 500。 * 可以取消的狀態:`scheduled`、`ongoing`、`retryScheduled`。 * 取消後不能恢復。 消費者提出取消,就應該立即取消,不需要等消費者再次確認;取消之後才扣到的款項,請退款給消費者。 ## 查詢 [Section titled “查詢”](#查詢) * 單筆:[`GET /subscriptions/{id}`](/api/get-subscription/),可以用 `S` 編號或內部 id。 * 某一期的交易:付款通知裡的 `id`,或用[訂單編號查詢](/api/list-order-transactions/)列出同一個 `orderId` 的所有期數。 * 狀態說明見 [Subscription 物件](/api/objects/subscription/)。 GET /subscriptions 不是查這裡的定期定額 [`GET /subscriptions`](/api/list-subscriptions/)(不帶 id)列出的是應援商店的「定期購」訂單,不是用 Payment API 建立的定期定額。 ## 3D 驗證與綁卡 [Section titled “3D 驗證與綁卡”](#3d-驗證與綁卡) * `use3d: true` 時,消費者在結帳頁走 3D 驗證。應援可能依收單設定強制 3D。 * 部分收單機構不支援 0 元驗證,綁卡時會**先授權新台幣 1 元再立即退回**,結帳頁會事先告知消費者。 * 首期日在未來時,消費者在結帳頁只綁卡,卡片是否可用要到首期扣款時才知道。 ## 消費者要換卡 [Section titled “消費者要換卡”](#消費者要換卡) Payment API 目前沒有公開的換卡流程。常見做法:取消舊的定期定額,請消費者重新走一次結帳建立新的。 ## 付款通知 [Section titled “付款通知”](#付款通知) | 時機 | purpose | | -------------------- | ---------------------------------------------------- | | 「建立定期定額」第一期付款 | `charge`(`action: subscription`) | | 「建立預約定期定額」結帳完成 | `schedule_subscription` | | 之後每一期(含重扣) | `charge`(`action: subscription`) | | 「用 token 建立定期定額」的第一期 | 不送,以 API 回應為準 | | 取消、重扣排定或用完 | `subscription_cancelled`、`subscription_retry`,需請應援開啟 | 欄位見[付款通知內容](/api/objects/webhook/)。 # 付款通知(webhook) > 付款通知在哪裡設定、什麼時候送、送幾次、怎麼確認真假、同一筆交易收到多則怎麼處理,以及沒收到時如何補救。 付款結果確定時,應援會 `POST` 一則通知到你的伺服器。請把它當成「提醒你去查」,**以回查的結果為準**。 ## 設定網址 [Section titled “設定網址”](#設定網址) 通知網址只能在 CRM 設定,API 請求沒有這個欄位: * 位置:CRM「總設定」→「開發者」→「應援金流設定」→「交易資料回傳網址位置」。 * 必須是 `https://`、使用 443 port,而且可以從網際網路連到。內部網址、私有 IP 會被擋下。 * 測試環境與正式環境分別設定。 * **沒有設定網址的期間,通知會直接略過**,之後補設也不會補送。 ## 什麼時候會送 [Section titled “什麼時候會送”](#什麼時候會送) | 情境 | purpose | status | | --------------------- | ------------------------------------------------------ | ------------------------------------ | | 信用卡、Apple Pay 付款成功或失敗 | `charge` | `charged`、`claimed` 或 `failed` | | 3D 驗證開始後 10 分鐘沒完成 | `charge` | `failed`(`message: 3DS_ABANDONED`) | | 超商代碼、ATM 取號 | `charge` | `charging`(沒有 `success`) | | 超商代碼、ATM 繳費完成 | `charge` | `charged` 或 `claimed` | | 超商代碼、ATM 逾期未繳 | `charge` | `failed`(`message: PAYMENT_EXPIRED`) | | LINE Pay 等待確認、確認結果 | `charge` | `charging`,之後是最終結果 | | 定期定額每一期(含重扣) | `charge` | 同上,`action: subscription` | | 綁卡頁完成或失敗 | `token` | — | | 預約定期定額結帳完成或失敗 | `schedule_subscription` | — | | 退款、定期定額取消、重扣排定或用完 | `refund`、`subscription_cancelled`、`subscription_retry` | 需請應援開啟 | **不會**送通知的情況: * 消費者沒有付款就離開,交易停在 `initiated`。 * [用 token 扣款](/api/token-transactions/)、[用 token 建立定期定額](/api/token-subscriptions/)的第一期。結果以 API 回應為準。 * 付款前的檢查就失敗(例如金額超出付款方式的範圍)。 每一種通知的欄位見[付款通知內容](/api/objects/webhook/)。 ## 送出規則 [Section titled “送出規則”](#送出規則) | 項目 | 規則 | | ------ | --------------------------------------- | | 格式 | `POST`,`Content-Type: application/json` | | 逾時 | 每次 10 秒 | | 成功的條件 | 回應 HTTP 2xx。不看回應內容 | | 轉址 | 不跟隨。回 3xx 會被當成失敗 | | 次數 | **最多 3 次**,間隔約 2 秒、4 秒 | | 3 次都失敗 | **不會再送**,也沒有後台重送功能 | | 順序 | 不保證 | 3 次都失敗就不會再送 短暫的部署或當機,就可能漏掉通知。請一定要有下面的[補救機制](#%E6%B2%92%E6%94%B6%E5%88%B0%E9%80%9A%E7%9F%A5%E6%99%82)。 ## 確認通知的真假 [Section titled “確認通知的真假”](#確認通知的真假) 付款通知沒有簽章 付款通知沒有簽章,任何人都可以偽造一則送到你的網址。**不要直接相信通知的內容。** 1. 收到通知,先回 HTTP 200。 2. 用 body 的 `id`(27 字元)呼叫 [`GET /transactions/{id}`](/api/get-transaction/)。 3. 確認查到的交易屬於你的訂單:比對 `orderId` 與 `amount`。 4. 依查到的 `status` 更新訂單。 * 剛付款完成的極短時間內,查到的可能還是舊狀態。查到 `charging` 或 `initiated` 時,隔幾秒再查一次。 * `token` 通知沒有 API 可以回查。請確認 `id` 是你剛建立的[綁卡頁](/api/checkout-token/),並核對 `customId`。 * 程式範例見[快速開始](/developers/quickstart/#4-%E6%8E%A5%E6%94%B6%E4%BB%98%E6%AC%BE%E9%80%9A%E7%9F%A5%E4%B8%A6%E5%9B%9E%E6%9F%A5)。 ## 同一筆交易的多則通知 [Section titled “同一筆交易的多則通知”](#同一筆交易的多則通知) * 超商代碼、ATM、LINE Pay 會先收到 `charging`,之後才收到結果,兩則的 `id` 相同。 * 通知可能重複送達,也可能順序顛倒。 * 通知沒有事件編號,所以不能用「收過這個 id」判斷重複。請以**回查到的狀態**決定要做什麼,並讓「標記為已付款」這類動作重複執行也安全。 ## 回應要快 [Section titled “回應要快”](#回應要快) * 應援每次只等 10 秒。請先回 200,再把回查、出貨、寄信等工作放到背景處理。 * 不要回 3xx,也不要讓網址需要登入。 * 防火牆有擋外部連線時,請向應援索取付款通知的來源 IP。 ## 沒收到通知時 [Section titled “沒收到通知時”](#沒收到通知時) 請在你的系統排一個定期工作,處理「超過一段時間還沒有結果」的訂單: 1. 找出建立超過 15 分鐘、還沒有最終結果的訂單。 2. 用 [`GET /order/{orderId}/transactions`](/api/list-order-transactions/) 查這個訂單的所有交易。 3. 有 `charged` 或 `claimed`:補標為已付款。 4. 有 `charging`:繼續等待(超商代碼最多 48 小時、ATM 3 天)。 5. 全部是 `initiated` 或 `failed`:視為未付款。 每天也可以用[查詢交易列表](/api/list-transactions/)做一次完整對帳,見[查詢與對帳](/developers/querying/)。 ## 測試 [Section titled “測試”](#測試) * 測試環境**會實際送出**付款通知,送到測試環境 CRM 設定的網址。 * 沒有測試事件 API。要測試接收端,可以建立一筆測試交易並付款,或用 cURL 自己送一則模擬的 body 到你的網址(因為沒有簽章,模擬的 body 和真的格式相同)。 # 商家指南總覽 > 給商家老闆、營運與客服的操作指南。不用寫程式,照著後台步驟就能完成。 這一區寫給**不寫程式的人**:商家老闆、營運、客服與財務。內容都是在 CRM 後台點哪裡、會看到什麼、要注意什麼。需要工程師處理的部分,會明確寫出「請轉給工程師」並附上該看的頁面。 ## 依你的階段 [Section titled “依你的階段”](#依你的階段) 準備開始收款 * [開通金流與付款方式](/merchant/activate/) * [消費者看到的付款流程](/merchant/payment-experience/) * [交給工程師串接前的準備](/merchant/api-access/) 每天的營運 * [查詢交易與對帳](/merchant/transactions/) * [退款](/merchant/refunds/) * [定期定額](/merchant/subscriptions/) * [電子發票](/merchant/invoices/) ## 不想串接 API? [Section titled “不想串接 API?”](#不想串接-api) [WooCommerce 外掛](/merchant/woocommerce/)網站是 WordPress + WooCommerce 的話,裝外掛就能收款。 ## 你的工程師需要什麼 [Section titled “你的工程師需要什麼”](#你的工程師需要什麼) 把這兩頁轉給工程師就好: * [交給工程師串接前的準備](/merchant/api-access/):你要先在後台幫他準備好的東西。 * [開發者快速開始](/developers/quickstart/):工程師從這裡開始串接。 ## 看不懂某個詞? [Section titled “看不懂某個詞?”](#看不懂某個詞) [名詞對照](/start/glossary/)用白話解釋文件裡常出現的詞,例如「結帳頁」「付款通知」「token」「3D 驗證」。 # 開通金流與付款方式 > 在 CRM 申請金流服務、看懂審核狀態,以及信用卡、Apple Pay、超商代碼、ATM、LINE Pay 各要怎麼開通。 收款前要先開通金流服務。申請在 CRM 後台完成,送出後由應援與銀行審核,通過後會收到 email 通知。 ## 登入 CRM [Section titled “登入 CRM”](#登入-crm) CRM 後台網址是 `https://{你的網域名稱}.oen.tw/crm`。網域名稱就是你申請時設定的應援頁子網域,例如應援頁是 `ming.oen.tw`,網域名稱就是 `ming`。 忘記網域名稱的話,可以在 CRM「總設定」第一個分頁最下方的「Oen 服務資訊 → 網域名稱」找到。之後工程師串接、WooCommerce 外掛都會用到這個名稱。 ## 申請流程 [Section titled “申請流程”](#申請流程) 1. **開始申請。** 還沒開通時,CRM 左側選單上方會顯示「開通金流服務」按鈕。也可以從「金流開通狀態」頁按「申請金流服務」。 2. **填寫申請表「金流服務申請」。** 依序填寫: * 基本資料:服務項目、公司或組織名稱、聯絡方式、統一編號等。 * 負責人資訊與帳務聯絡人。 * 付款方式及發票。 * 收款帳戶(戶名、銀行帳號、存摺封面影本)。 * 附件:依組織類型需要的登記文件等。檔案格式 PDF、JPG、JPEG、PNG,單檔 2 MB 以內;多頁文件請合併成一個 PDF。 * 勾選「應援金流服務使用條款」。 填到一半可以按「儲存草稿」,之後回來按「繼續完成申請」。 3. **送出申請。** 看到「成功送出金流服務申請!」就完成了。 4. **等待審核。** 進度可以在「金流開通狀態」頁查看,通過後會收到 email 通知。 「帳單顯示名稱」設定後無法更改 這個名稱會出現在消費者的信用卡帳單或交易明細上。請填消費者一看就認得的名稱,只能用中文、英文字母、空格、底線與連字號。 ## 審核狀態 [Section titled “審核狀態”](#審核狀態) 申請紀錄上的狀態: | 畫面上的狀態 | 意思 | 你要做什麼 | | ------- | --------- | ---------- | | 草稿 | 還沒送出 | 完成申請表後送出 | | 待應援收件 | 已送出,等應援確認 | 等待 | | 應援已收件 | 應援確認資料中 | 等待 | | 文件未通過 | 資料或文件需要補正 | 依通知補件後重新送出 | | 已送銀行審核 | 銀行審核中 | 等待 | | 銀行審核通過 | 銀行已核准 | 等應援完成設定 | | 銀行審核不通過 | 銀行未核准 | 聯絡應援客服 | | 已開通金流 | 可以開始收款 | 設定你的服務與價格 | 審核需要的時間依個案而定,有問題請[聯絡客服](#%E9%9C%80%E8%A6%81%E5%8D%94%E5%8A%A9)。 ## 各付款方式怎麼開通 [Section titled “各付款方式怎麼開通”](#各付款方式怎麼開通) | 付款方式 | 怎麼開通 | | --------- | ----------------------------------------------------------------- | | 信用卡 | 申請表的必選項目,隨金流一起開通。支援 VISA、Mastercard、JCB;American Express 與銀聯卡另外開通 | | Apple Pay | 在申請表勾選 | | 超商代碼 | 申請表沒有這個選項,請聯絡應援開通 | | ATM 虛擬帳號 | 申請表沒有這個選項,請聯絡應援開通 | | LINE Pay | 你要先有自己的 LINE Pay 商店帳號,再請應援開通。開通後依下方步驟設定 | 開通後,可以在「總設定」→「金流設定」分頁的「付款方式及費率」查看目前開通的付款方式與你的費率。顯示「未開通請聯絡應援」的,就是還沒開通。 ### LINE Pay 設定 [Section titled “LINE Pay 設定”](#line-pay-設定) 應援為你開通 LINE Pay 後,「總設定」→「開發者」分頁會出現「LINE Pay 設定」: 1. 把畫面上顯示的「Oen 正式環境 IP」設定到你自己的 LINE Pay 商家後台。 2. 輸入 LINE Pay 的 Channel ID 與 Channel Secret Key,按「驗證」。 3. 按「開啟 LINE Pay 收款」。看到「LINE Pay 收款已開啟」就完成了。 LINE Pay 的款項由 LINE Pay 直接撥給你,不會出現在應援的撥款裡。 ### 第三方服務 [Section titled “第三方服務”](#第三方服務) 「金流設定」分頁的「第三方服務」可以另外申請「應碰收 TWQR」與「藍新金流」。藍新金流主要提供 ATM 轉帳,款項由藍新撥款,也需要藍新審核。申請按鈕在主申請送出後才會出現。 ## 開通之後 [Section titled “開通之後”](#開通之後) * **要用程式串接收款**:請應援開啟 API 串接(開發者模式),再看[交給工程師串接前的準備](/merchant/api-access/)。 * **用 WooCommerce**:看 [WooCommerce 外掛](/merchant/woocommerce/)。 * **設定撥款頻率**:看[查詢交易與對帳](/merchant/transactions/)。 ## 需要協助 [Section titled “需要協助”](#需要協助) * 常見問題:[www.oen.tw/faq](https://www.oen.tw/faq) * 聯絡客服:CRM 右上角選單的「聯絡客服」(LINE 官方帳號 [lin.ee/zgKzwaZ](https://lin.ee/zgKzwaZ)) * 撥款與帳務: # 交給工程師串接前的準備 > 請應援開啟 API 串接、在 CRM 產生 token、設定付款通知網址、申請固定 IP,把工程師需要的東西一次準備好。 工程師串接 Payment API 之前,有幾件事要由商家在後台或透過應援完成。照這一頁準備好,再把清單交給工程師。 ## 準備清單 [Section titled “準備清單”](#準備清單) | 項目 | 誰做 | 在哪裡 | | ------------------- | --------------- | -------------------------------- | | 1. 金流已開通 | 商家申請、應援審核 | [開通金流與付款方式](/merchant/activate/) | | 2. 開啟 API 串接(開發者模式) | 請應援開啟 | 聯絡業務或客服 | | 3. 指派工程師的後台權限 | 商家 | CRM「團隊權限」 | | 4. 產生 token | 商家或工程師 | CRM「總設定」→「開發者」 | | 5. 設定付款通知網址 | 工程師提供網址 | CRM「總設定」→「開發者」 | | 6. 申請固定 IP | 工程師提供 IP,商家聯絡應援 | 聯絡客服或業務 | | 7. 測試環境帳號 | 請應援協助 | 聯絡業務或客服 | ## 1. 開啟 API 串接 [Section titled “1. 開啟 API 串接”](#1-開啟-api-串接) API 串接(開發者模式)由應援開啟,商家後台沒有開關。新申請的商家通常已經開啟。 開啟後,CRM 會出現: * 左側選單「金流管理」→「金流API」:連到 API 文件。 * 左側選單「金流管理」→「定期購買」:管理用 API 建立的定期扣款。 * 「總設定」→「開發者」分頁的「應援金流設定」:產生 token、設定通知網址。 ## 2. 指派權限 [Section titled “2. 指派權限”](#2-指派權限) 操作 token 與通知網址的人,要是後台管理員,或在「團隊權限」被指派「金流串接管理員」角色。權限不足時,「產生 Token」按鈕會是灰色的。 ## 3. 產生 token [Section titled “3. 產生 token”](#3-產生-token) 1. 進入 CRM「總設定」→「開發者」分頁,找到「應援金流設定」→「存取 Token」。 2. 按「產生 Token」。 3. 按「複製」,交給工程師。 重新產生 token 會讓舊的立刻失效 * 按下「重新產生 Token」**沒有確認視窗**,舊 token 會立刻失效,正在串接中的系統會馬上無法呼叫 API。確定要換之前,先跟工程師約好時間。 * 離開頁面後就看不到完整的 token 了。忘記複製的話,只能重新產生。 * token 就像密碼,只交給需要的工程師,不要貼在 email、聊天群組或 AI 工具裡。 測試環境和正式環境的 token 是分開的,不能共用。測試環境的 token 要在測試環境的後台產生,測試環境帳號請聯絡應援協助。 ## 4. 設定付款通知網址 [Section titled “4. 設定付款通知網址”](#4-設定付款通知網址) 付款完成時,應援會把結果送到你的系統。網址由工程師提供: 1. 在同一個「應援金流設定」區塊,找到「交易資料回傳網址位置」。 2. 貼上工程師提供的網址,必須是 `https://` 開頭。 3. 按「確定送出」,看到「設定成功」就完成了。 退款、取消定期購買與續扣重試的通知需要應援另外開啟,有需要請告訴業務人員。 ## 5. 申請固定 IP [Section titled “5. 申請固定 IP”](#5-申請固定-ip) 正式環境一定要先綁定固定 IP 正式環境的 API 只接受已綁定的 IP。還沒綁定時,工程師呼叫正式環境會被擋下,測試環境則不受影響。 1. 請工程師提供正式環境伺服器呼叫 API 時使用的**固定對外 IP**。 2. 聯絡應援客服或業務,提供你的網域名稱與這些 IP。 3. 應援完成設定後會通知你,工程師就可以開始呼叫正式環境。 透過合作平台(例如已和應援串接好的電商平台)收款的商家,不需要申請。 ## 6. 把這些交給工程師 [Section titled “6. 把這些交給工程師”](#6-把這些交給工程師) * 網域名稱(例如 `ming`) * 測試環境與正式環境的 token * 通知網址已設定好的確認 * 固定 IP 開通的確認 * 本站的[開發者快速開始](/developers/quickstart/) ## 常見問題 [Section titled “常見問題”](#常見問題) **在後台找不到「金流API」選單或「開發者」分頁?** API 串接還沒開啟,或你的帳號沒有權限。請應援開啟,或請管理員指派「金流串接管理員」角色。 **出現「產生存取 Token 失敗,請洽應援客服!」?** 請[聯絡客服](https://lin.ee/zgKzwaZ)並告知網域名稱。 **工程師說呼叫 API 一直回「未授權」?** 常見原因有三個:token 被重新產生過、測試與正式環境的 token 用錯了、正式環境還沒綁定固定 IP。 # 電子發票 > API 交易的電子發票怎麼自動開立、在哪裡查看、怎麼作廢,以及退款時的折讓。 ## API 交易會自動開發票 [Section titled “API 交易會自動開發票”](#api-交易會自動開發票) 開通「應援代開電子發票」後,用 API 收款的交易在**付款成功時自動開立發票**,不用另外操作。 * 開立發票需要交易帶有商品明細(品名、數量、單價)。工程師串接時一定會帶,詳見[單次付款](/developers/one-time/)。 * 消費者沒有提供發票資訊時,預設開立雲端發票。 * 由應援代開時,品名前面會加上你的商家名稱,後面加上「(受託代銷)」。 ### 怎麼開通 [Section titled “怎麼開通”](#怎麼開通) * 申請金流時勾選「使用應援代開電子發票(需付費)」,或事後聯絡應援開通。 * 還沒開通時,「金流管理」→「電子發票」頁會顯示「如需開立電子發票,請聯絡應援科技」。 * 想用自己公司的名義開發票,請聯絡應援人員。 ## 查看發票 [Section titled “查看發票”](#查看發票) 「金流管理」→「電子發票」有兩個分頁: * **電子發票明細**:類型有公司戶發票、捐贈發票、雲端發票、二聯式發票。狀態有開立中、已開立、開立失敗、作廢中、已作廢、作廢失敗。 * **折讓明細**:狀態有折讓中、已折讓、折讓失敗、作廢中、已作廢、作廢失敗。 ## 作廢與折讓 [Section titled “作廢與折讓”](#作廢與折讓) 後台沒有作廢按鈕 商家後台只能查看發票。發票開錯要作廢,或需要手動折讓時,請[聯絡客服](https://lin.ee/zgKzwaZ)。 退款產生的折讓是自動的,規則見[退款](/merchant/refunds/)。 # 消費者看到的付款流程 > 消費者從你的網站按下付款後,會在應援結帳頁看到什麼、各付款方式怎麼付、多久要付完,以及付款後回到哪裡。 用 Payment API 收款時,消費者會被帶到應援的結帳頁付款。這一頁說明消費者的體驗,方便你設計網站流程、回答消費者問題。 ## 單次付款 [Section titled “單次付款”](#單次付款) 1. 消費者在你的網站按下「付款」。 2. 畫面跳到應援結帳頁,網址是 `https://{你的網域名稱}.oen.tw/checkout/…`。 3. 消費者選擇付款方式並完成付款。 4. 付款成功後回到你的網站;失敗則回到你指定的失敗頁。 結帳頁 5 分鐘內要付款 結帳頁從你的網站建立起算 5 分鐘內有效。消費者停太久,會看到「付款時限已過」並被導回失敗頁;過期後才打開,會看到「此付款連結資訊已過期」。所以結帳連結**不能**事先建立好再用 email 或訊息寄給消費者。 ## 各付款方式怎麼付 [Section titled “各付款方式怎麼付”](#各付款方式怎麼付) | 付款方式 | 消費者的流程 | 多久要付完 | | --------- | --------------------------------------- | ------ | | 信用卡 | 輸入卡號、到期日、安全碼;可能跳到銀行頁面做 3D 驗證 | 當場完成 | | Apple Pay | 在支援的裝置(例如 iPhone 的 Safari)會自動出現按鈕,驗證後完成 | 當場完成 | | LINE Pay | 跳到 LINE Pay 付款 | 20 分鐘內 | | 超商代碼 | 取得繳費代碼,到超商繳費 | 48 小時內 | | ATM 虛擬帳號 | 取得虛擬帳號,用 ATM 或網銀轉帳 | 3 天內 | * **信用卡一定會出現**。要加開超商代碼、LINE Pay、ATM,需要先開通,並請工程師設定。 * 超商代碼、ATM 取號後,消費者會停在應援的繳費資訊頁,按「返回網站」才會回到你的網站。建議你的訂單頁也顯示繳費代碼與期限。 * 各付款方式有金額範圍,例如超商代碼是 80 到 20,000 元,超出範圍的方式不會顯示。詳見[付款方式](/developers/payment-methods/)。 ## 付款後回到哪裡 [Section titled “付款後回到哪裡”](#付款後回到哪裡) * **成功**:回到你的網站指定的成功頁。 * **失敗或逾時**:回到你的網站指定的失敗頁。 * 消費者可能付完款就關掉視窗,沒有回到你的網站。訂單是否付款,請以 CRM 的交易狀態或工程師系統收到的付款通知為準。 ## 定期定額 [Section titled “定期定額”](#定期定額) * 消費者在結帳頁輸入信用卡並同意定期扣款。 * 之後每期由應援自動扣款,**不需要消費者再操作**。 * 每次扣款前 3 天,應援會寄提醒信給消費者(有 Email 時)。 * 消費者要取消,請他聯絡你,由你在 CRM 或透過 API 取消。見[定期定額](/merchant/subscriptions/)。 ## 綁卡 [Section titled “綁卡”](#綁卡) 部分服務會請消費者先綁卡(例如儲值、隨用隨扣): * 消費者在綁卡頁輸入卡片並完成 3D 驗證,頁面 10 分鐘內有效。 * 部分銀行綁卡時會先刷 1 元再立即退回,頁面會事先告知。 ## 消費者的信用卡帳單 [Section titled “消費者的信用卡帳單”](#消費者的信用卡帳單) 消費者帳單上顯示的商家名稱,是你申請金流時填的「帳單顯示名稱」。消費者說「帳單上有看不懂的扣款」時,請先確認這個名稱。 ## 消費者問「我付款成功了嗎?」 [Section titled “消費者問「我付款成功了嗎?」”](#消費者問我付款成功了嗎) 到 CRM「金流管理」→「金流列表」,用消費者的姓名、Email、電話或你的訂單編號搜尋,看交易狀態。狀態說明見[查詢交易與對帳](/merchant/transactions/)。 # 退款 > 在 CRM 為 API 交易退款:全額或部分退款、超商與 ATM 要填的退款帳戶、已撥款交易怎麼扣回,以及發票折讓。 ## 在 CRM 退款 [Section titled “在 CRM 退款”](#在-crm-退款) 1. 到「金流管理」→「金流列表」,點進要退款的交易。 2. 按明細右上角的「退款」。 3. 在「退款申請」視窗填寫退款內容: * **只有一個品項,或交易沒有商品明細**:直接輸入退款金額,不能高於畫面提示的上限。 * **有多個品項**:先選「全額退款」或「部分退款」,部分退款要逐項選擇品項、數量與金額。 * **超商代碼或 ATM 的交易**:多一步「填寫退款帳號資訊」,填入消費者的銀行名稱與代碼、分行、帳號與戶名。 4. 按「確認送出」,再確認一次「確定退款?送出後無法取消操作」。 5. 看到「已送出退款申請!」就完成了。 退款作業時間依付款方式不同,需要 3 到 7 個工作天。退款完成後,系統會寄信通知你的帳務聯絡人。 ## 看不到「退款」按鈕? [Section titled “看不到「退款」按鈕?”](#看不到退款按鈕) 「退款」按鈕只在這些條件都成立時出現: * 交易狀態是「付款成功」或「已入帳」。 * 這筆交易還沒退過款。 * 你的帳號有「API 交易退款」權限。 每筆交易在 CRM 只能退一次 第一次退款後,交易狀態就會變成「退款中」或「已退款」,退款按鈕也會消失。要部分退款時,請一次決定好金額。 ## 各付款方式的差異 [Section titled “各付款方式的差異”](#各付款方式的差異) | 付款方式 | 要填退款帳戶嗎 | 退款後的狀態 | | ------------------------------- | ------------- | ----------------- | | 信用卡、Apple Pay、LINE Pay、應碰收 TWQR | 不用 | 立刻變成「全額退款」或「部分退款」 | | 超商代碼、ATM | 要,退款會匯到消費者的帳戶 | 「退款中」,匯款完成後更新 | * 超商代碼的交易如果**消費者還沒繳費**,工程師用 API 退款時會直接取消繳費代碼。 * LINE Pay 已經退過一次的交易,不能再退(畫面會顯示「LINE Pay 已退過款,無法退款。」)。 ## 已撥款的交易 [Section titled “已撥款的交易”](#已撥款的交易) 款項已經撥給你之後才退款,最後一步會顯示:「此金流已撥款至您的帳戶,將於下次撥款時扣除此筆退款金額及退款手續費。」交易狀態會變成「撥款後退款」,扣回的金額可以在「撥款列表」的「退款扣除」看到。 ## 發票折讓 [Section titled “發票折讓”](#發票折讓) 交易有開電子發票時: * 退款**一定要有商品明細**,品項金額合計要等於退款金額。交易本身缺少商品明細時,畫面會顯示「此筆金流缺少開立折讓所需的商品明細,目前無法於 CRM 進行退款。」請聯絡應援客服處理。 * 信用卡等即時退款成功後,系統會**自動開立折讓**。 * 發票還沒開出就全額退款時,系統會直接取消待開的發票,不開折讓。 * 折讓紀錄在「金流管理」→「電子發票」→「折讓明細」。 ## 用 API 退款 [Section titled “用 API 退款”](#用-api-退款) 工程師也可以用 API 退款,規則與 CRM 相同,見[開發者的退款說明](/developers/refunds/)。 # 定期定額 > 在 CRM 查看與終止用 API 建立的定期扣款(定期購買),看懂扣款失敗與自動重扣,以及 Subscription API 的訂閱管理。 CRM 裡有幾種「定期」功能,用 Payment API 建立的定期扣款在「**定期購買**」: | 選單 | 管理什麼 | | ------------------ | ------------------------------------------------------- | | 金流管理 →「定期購買」 | 用 Payment API 建立的定期定額(本頁主要說明) | | 訂閱管理 →「訂閱產品」「訂閱客戶」 | 用 [Subscription API](/products/subscription-api/) 建立的訂閱 | | 捐贈管理 →「定期定額」 | 應援捐款頁的定期捐款 | | 回饋品管理 →「訂閱制訂單」 | 回饋品的訂閱 | 「定期購買」選單要在 API 串接開啟後才會出現。 ## 查看定期購買 [Section titled “查看定期購買”](#查看定期購買) 列表可以依訂閱起始日期與狀態篩選,上方顯示定期購買人數與總金額。點進去是「定期購買明細」:付款人、起始日期、下次收取日期、每期金額、週期、已扣期數、狀態,以及每一期的扣款紀錄(金流編號、付款時間、金額、手續費、狀態)。 | 狀態 | 意思 | | -------- | -------------------------------------- | | 進行中 | 正常扣款中 | | 異常 - 待重扣 | 這一期扣款失敗,已排定重新扣款。明細會顯示「預計於某時間進行第 N 次重扣」 | | 異常 | 扣款失敗,沒有再排定重扣 | | 已取消 | 已終止 | | 已結束 | 期數已扣完 | ## 終止定期購買 [Section titled “終止定期購買”](#終止定期購買) 1. 在定期購買明細,狀態是「進行中」時,按狀態旁邊的「終止定期購買」。 2. 可以填寫終止原因。 3. 確認後就不會再扣款。 終止後無法回復 終止後這位消費者不會再被扣款,也不能恢復。消費者要重新訂閱,需要再走一次結帳流程。 消費者提出取消,就應該終止,不需要等消費者再確認。終止之後才扣到的款項,請退款給消費者。 ## 扣款失敗與自動重扣 [Section titled “扣款失敗與自動重扣”](#扣款失敗與自動重扣) * 扣款失敗時,狀態會變成「異常 - 待重扣」或「異常」,每一期的結果都在明細的扣款紀錄裡。 * 是否自動重扣,可以在「總設定」→「開發者」→「定期交易重試機制(Beta)」→「定期購買」設定開啟或關閉。 ## 消費者要換信用卡 [Section titled “消費者要換信用卡”](#消費者要換信用卡) Payment API 的定期購買**沒有給消費者自行換卡的頁面**。消費者要換卡時,常見做法是終止舊的定期購買,再請消費者重新訂閱一次。 換卡或綁卡時,金流列表可能出現一筆 1 元的「綁卡試刷」,這是驗證卡片用的,會立即退回。 ## Subscription API 的訂閱 [Section titled “Subscription API 的訂閱”](#subscription-api-的訂閱) 用 Subscription API 建立的訂閱在「訂閱管理」查看: * 訂閱明細是唯讀的,狀態有試用期中、訂閱中、暫停中、失敗、已取消、已終止。 * **消費者有自助管理頁**,可以用 Email 驗證碼登入後換卡、變更方案、取消。在訂閱明細按「複製管理連結」,把連結傳給消費者即可。 * 「訂閱管理」→「設定」可以管理 API 金鑰、Webhook 與 Email 通知。還沒開通時會顯示「此網域尚未開通 Subscription API 整合功能,如需使用請聯繫 OEN。」 # 查詢交易與對帳 > 在 CRM 找到 API 交易、看懂交易狀態、匯出資料,以及設定撥款頻率與查看撥款明細。 ## 找到交易 [Section titled “找到交易”](#找到交易) 所有交易都在 CRM「金流管理」→「金流列表」。 * **預設只顯示最近三個月**(三個月前的月初到今天)。要看更早的交易,請調整日期區間。日期可以依「建立日期」或「付款時間」篩選。 * **用 API 建立的交易,類型顯示為「現金購買」**。只想看 API 交易時,在「條件篩選」的類型選「現金購買」。 * 搜尋框可以用這些資料找 API 交易:金流編號、你的訂單編號、姓名、Email、電話。 * API 交易的金流編號是 `P` 開頭,後面接日期與亂數,例如 `P20260928AbCdEfGh`。 點進任一筆就是「金流明細」,可以看到付款人資料、交易類型(單次或定期)、金額、付款方式、手續費、金流狀態、期望撥款日期、訂單編號與商品明細。 ### 條件篩選 [Section titled “條件篩選”](#條件篩選) 可以依類型、付款方式、金額區間、金流狀態、是否由應援撥款、收款銀行、收據狀態等條件篩選。 ## 交易狀態 [Section titled “交易狀態”](#交易狀態) | 畫面上的狀態 | 意思 | | --------- | ------------------------------- | | 已建立付款意向 | 已建立結帳頁,消費者還沒付款 | | 尚未付款 | 付款處理中;超商代碼、ATM 等待消費者繳費時也是這個狀態 | | 付款成功 | 付款完成 | | 撥款在途 | 款項正在撥給你 | | 已入帳 | 已撥款給你,或款項由收單方直接撥給你(例如 LINE Pay) | | 退款中 | 退款處理中。超商代碼、ATM 的退款會停在這裡,直到匯款完成 | | 全額退款、部分退款 | 已退款。明細會註明退款金額 | | 撥款後退款 | 已撥款後才退款,退款金額會在下次撥款扣除 | | 扣款或授權失敗 | 付款失敗 | | 已失效 | 超商代碼或 ATM 逾期未繳 | | 已取消 | 交易已取消,例如超商代碼尚未繳費就被取消 | ## 匯出 [Section titled “匯出”](#匯出) 1. 在金流列表先篩選出要的資料並勾選。 2. 按「匯出金流資料」,選「一般格式 - Excel」或「一般格式 - CSV」,再按「匯出」。 3. 檔案不會立刻下載。到左側選單的「下載管理」取得檔案,完成後 1 小時內可以下載。 筆數很多時,建議先篩選類型,再點「選取全部」下載。匯出需要「下載金流」權限。 ## 撥款 [Section titled “撥款”](#撥款) ### 撥款頻率 [Section titled “撥款頻率”](#撥款頻率) 「總設定」→「金流設定」→「撥款方式設定」可以查看與編輯撥款頻率:暫不撥款、每週五、每週二與週五、雙週、每月。修改後從下一次撥款開始生效;如果系統正在撥款,會再下一次才生效。 這個區塊要應援為你啟用撥款設定後才會出現。沒有編輯權限時,請聯絡管理員。 ### 撥款列表 [Section titled “撥款列表”](#撥款列表) 「金流管理」→「撥款列表」列出每一次撥款:撥款日期、撥款編號、總金額、手續費、匯款金額、撥款狀態與發票狀態。有退款扣除時,會另外顯示「退款扣除」金額。點進去可以看到撥款細項,並匯出 CSV 或 Excel。 不在撥款裡的款項 LINE Pay、應碰收 TWQR、信用卡 - 藍新等由其他機構處理的款項,由該機構直接撥給你,不會出現在應援的撥款裡。 * 撥款帳戶可以在撥款列表的「撥款帳戶資訊」查看。要變更撥款帳戶,請寄信到 。 * 看到「撥款功能目前已暫停」時,請聯絡 。 ## 對帳建議 [Section titled “對帳建議”](#對帳建議) * 每天或每次撥款後,用「金流列表」匯出當期交易,和你自己系統的訂單比對。以「訂單編號」對應最方便。 * 工程師也可以用 API 自動對帳,見[查詢與對帳](/developers/querying/)。 # WooCommerce 外掛 > WordPress + WooCommerce 商家不用寫程式,安裝外掛、填入商店代碼與 Secret Key,就能收信用卡與超商繳費。 應援的 WooCommerce 外掛會在結帳時把消費者導到應援的結帳頁付款,付款完成後自動更新 WooCommerce 訂單狀態。 * 付款方式:信用卡、超商繳費 * 原始碼與下載:[github.com/OEN-Tech/woocommerce-oen-payment](https://github.com/OEN-Tech/woocommerce-oen-payment) * 網站需求:PHP 8.1 以上、WordPress 6.1 以上、WooCommerce 8.2 以上 ## 開始前 [Section titled “開始前”](#開始前) 1. **金流已開通。** 見[開通金流與付款方式](/merchant/activate/)。 2. **請應援開通「OenPay Embed」。** 外掛使用的金鑰要在這個分頁產生。開通後,CRM「總設定」會出現「OenPay Embed」分頁。 3. **要收超商繳費的話,請應援開通超商代碼。** 申請表上沒有這個選項。 外掛不需要開啟 API 串接(開發者模式)。 ## 1. 取得 Secret Key [Section titled “1. 取得 Secret Key”](#1-取得-secret-key) 1. 進入 CRM「總設定」→「OenPay Embed」分頁,找到「Embed API Key」。 2. 按「產生金鑰」。這是高風險操作,要輸入你的網域名稱才能確認。 3. 畫面會顯示 Publishable Key 與 Secret Key。**複製 Secret Key(`sk_` 開頭)**,先存在安全的地方。 4. 勾選「我已妥善保存 Secret Key」,按「立即清除」。 Secret Key 只會顯示一次 * 關閉後就看不到完整的 Secret Key,畫面 5 分鐘後也會自動清除。忘記複製的話,只能重新產生。 * 外掛要的是這把 Secret Key,**不是**「開發者」分頁的「存取 Token」。 * 正式環境後台產生的金鑰就是正式收款用的。外掛的測試模式要用測試環境後台產生的金鑰,測試環境帳號請聯絡應援。 * 同一分頁的「允許的 returnUrl 網域」是給 Embed 用的,只用 WooCommerce 外掛不需要填。 ## 2. 安裝外掛 [Section titled “2. 安裝外掛”](#2-安裝外掛) 1. 從 [GitHub 的 Releases 頁](https://github.com/OEN-Tech/woocommerce-oen-payment/releases)下載最新版本的原始碼壓縮檔並解壓縮。 2. 把資料夾上傳到網站的 `/wp-content/plugins/`。 3. 在 WordPress 後台「外掛」頁面啟用「WooCommerce OEN 金流付款」。 ## 3. 設定外掛 [Section titled “3. 設定外掛”](#3-設定外掛) 到「WooCommerce」→「設定」→「OEN」: | 設定項目 | 填什麼 | | --------------- | ----------------------------------------- | | 啟用 OEN 金流付款方式 | 勾選。這是總開關 | | 訂單編號前綴 | 選填。加在 WooCommerce 訂單編號前面 | | 顯示訂單商品名稱 | 選填。開啟後會把每個商品明細傳給應援 | | 在 Email 中顯示付款資訊 | 選填。在訂單通知信加上金流編號、繳費代碼等資訊 | | OEN 測試環境 | 測試時勾選;正式上線前要取消 | | 商店代碼 | 你的**網域名稱**。例如應援頁是 `ming.oen.tw`,就填 `ming` | | Secret Key | 貼上第 1 步的 Secret Key | | Webhook Secret | **留空** | 按「儲存變更」。外掛會自動向應援登記付款通知網址(`https://你的網站/?wc-api=oen_payment`),並把簽章密鑰自動填進「Webhook Secret」。 不用到 CRM 設定 webhook 外掛說明文件寫「至 OEN CRM 後台設定 Webhook 網址」,這一步**不需要做**。CRM「OenPay Embed」分頁新增的 webhook 收不到外掛需要的付款事件,外掛存檔時會自己完成登記。 如果「Webhook Secret」已經手動填過,外掛就不會自動登記。這時請勾選「Re-register webhook」再存一次。 ## 4. 啟用付款方式 [Section titled “4. 啟用付款方式”](#4-啟用付款方式) 到「WooCommerce」→「設定」→「付款方式」,啟用「OEN 信用卡」與(或)「OEN 超商繳費」。 ## 5. 測試後上線 [Section titled “5. 測試後上線”](#5-測試後上線) 1. 在測試模式下,用信用卡下一筆訂單,確認付款後 WooCommerce 訂單狀態會自動更新。 2. 換成正式環境後台產生的 Secret Key,取消勾選「OEN 測試環境」。 3. 勾選「Re-register webhook」後儲存,讓外掛重新登記正式環境的付款通知。 4. 用正式環境下一筆小額訂單確認,再從 WooCommerce 退款。 切換環境或網站網址之後,都要勾選「Re-register webhook」再存一次。 ## 退款 [Section titled “退款”](#退款) | 付款方式 | 怎麼退 | WooCommerce 會自動更新嗎 | | ---- | ------------------------------------------------- | ------------------ | | 信用卡 | 在 WooCommerce 訂單頁退款 | 會 | | 超商繳費 | 在 CRM 金流明細退款,要填消費者的退款帳戶,見[退款](/merchant/refunds/) | 不會,請手動更新訂單 | 信用卡訂單請從 WooCommerce 退款。從 CRM 退款時,WooCommerce 訂單不一定會自動變成已退款。 ## 發票 [Section titled “發票”](#發票) 開通「應援代開電子發票」後,付款完成時會自動開立電子發票,支援手機條碼、自然人憑證、統編與捐贈。見[電子發票](/merchant/invoices/)。 ## 常見問題 [Section titled “常見問題”](#常見問題) **找不到「OenPay Embed」分頁?** 請應援開通 OenPay Embed,並確認你的帳號有 API 串接設定的權限。 **儲存時沒有自動填入 Webhook Secret?** 確認已勾選「啟用 OEN 金流付款方式」,並填好商店代碼與 Secret Key,再勾選「Re-register webhook」儲存一次。成功時會顯示英文訊息「OEN webhook registered」。 **付款成功了,WooCommerce 訂單卻沒有更新?** 先確認網站可以從外部以 https 連線,以及 Webhook Secret 已經自動填入。仍有問題請[聯絡客服](https://lin.ee/zgKzwaZ),並提供網域名稱與訂單編號。 **WooCommerce 的交易在 CRM 哪裡看?** 在「金流管理」→「金流列表」,類型是「現金購買」。 # Embed 嵌入式付款 > 把信用卡付款表單嵌在你自己的頁面,消費者不用離開你的網站。需要先請應援開通。 Embed 把應援的信用卡表單以 iframe 嵌進你的結帳頁。卡號只在應援的 iframe 裡處理,不會經過你的頁面或伺服器。 開始前請確認 * **要先請應援開通**,商家不能自行開通。 * **沒有測試模式**:正式環境的金鑰打下去就是真的扣款。要測試請請應援另外準備測試環境的商家。 * 只支援**信用卡**、只收**新台幣**,金額是 1 到 200,000 的整數。 * 只支援付款後自動請款,不能存卡後再扣款,所以**做不了定期定額**;也**不開發票**。這些需求請用 [Payment API](/developers/quickstart/)。 ## 適合誰 [Section titled “適合誰”](#適合誰) | 適合 | 不適合 | | ----------------------------------- | ---------------------------- | | 有自己的結帳頁,希望消費者留在自己的網站完成信用卡付款,又不想處理卡號 | 需要定期扣款、存卡扣款、電子發票,或信用卡以外的付款方式 | ## 開通與後台設定 [Section titled “開通與後台設定”](#開通與後台設定) 1. **確認線上金流已開通。** Embed 建立在線上金流之上,還沒開通的話請先完成[開通金流](/merchant/activate/)。 2. **請應援開通 Embed。** 聯絡負責你的業務人員。開通後,CRM「總設定」會多出一個「OenPay Embed」分頁(網址 `/crm/setting/general?tab=embed`)。 看不到這個分頁時,確認兩件事:應援已經開通 Embed、你的帳號有 API 串接相關權限(例如「金流串接管理員」角色)。 3. **產生金鑰(Embed API Key)。** 按下產生後會同時拿到: * `pk_` 開頭的 Publishable Key:放在前端,公開沒關係。 * `sk_` 開頭的 Secret Key:**只會顯示一次**,5 分鐘後自動從畫面清除,請立刻交給工程師保存。之後畫面上只看得到開頭幾碼。 4. **登記允許的網域(OenPay Embed 允許的 returnUrl 網域)。** 把放付款表單的頁面與付款完成後導回頁的主機名稱都加進去。 5. **建立 webhook。** 填入 https 的接收網址。建立後會顯示簽章密鑰(`whsec_` 開頭),同樣只顯示一次。也可以由工程師呼叫 `POST /v1/webhooks` 建立。 允許網域是最常見的卡關點 * 清單是空的時候,**完全無法付款**。 * 只填主機名稱,例如 `shop.example.com`,不要加 `https://` 或路徑。 * 比對方式是完全相同,不支援萬用字元。`example.com` 與 `www.example.com` 要各加一筆。 * 不接受 IP、`localhost`、`.local`、`.internal`;至少要兩層網域;最多 25 筆。 * 移除網域會立即生效,請避開營業高峰。 ## 付款流程 [Section titled “付款流程”](#付款流程) 1. **後端**用 `sk_` 呼叫 `POST /v1/payment-intents`,拿到 `clientSecret`。 2. **前端**用 `pk_` 與 `clientSecret` 掛上付款表單。 3. **前端**呼叫 `confirmPayment()`,3D 驗證由 SDK 處理。成功後頁面會導向你的 `returnUrl`。 4. **後端**收到 `payment_intent.succeeded` webhook,驗證簽章後出貨。 ## 環境與網址 [Section titled “環境與網址”](#環境與網址) | | 正式環境 | | --- | -------------------------------------------------------------- | | API | `https://embed-api.oen.tw/v1` | | SDK | `https://static-assets.oen.tw/oenpay-sdk/v1.4.0/oenpay.umd.js` | * 金鑰沒有測試與正式之分,正式環境的金鑰一律真實扣款。 * 要測試時,請應援準備測試環境的商家與金鑰,並改用該環境的 API 與 SDK 網址。 ## 驗證 [Section titled “驗證”](#驗證) 每個請求都帶 `Authorization: Bearer <金鑰>`。 | 金鑰 | 格式 | 用在 | | --------------- | ------------------- | ---------------------- | | Publishable Key | `pk_` 加 24 碼 | 前端 SDK。只能打 SDK 用的四支端點 | | Secret Key | `sk_` 加 32 碼 | 後端。用 `pk_` 打後端端點會回 401 | | Webhook Secret | `whsec_` 開頭,共 54 字元 | 驗證 webhook 簽章 | * **重新產生金鑰**:舊金鑰會再保留 24 小時,讓你有時間部署新金鑰。 * **金鑰外洩**:先在後台**撤銷金鑰**讓它立即失效,再產生新的一組。只按重新產生的話,外洩的舊金鑰還能再用 24 小時。撤銷到新金鑰上線之間,付款會中斷。 * 以下任一項不成立,都會回 401 `AUTH_INVALID_KEY`,只有 message 不同:金鑰有效、商家啟用中、線上金流已開通、Embed 已開通。 ## 後端:建立 PaymentIntent [Section titled “後端:建立 PaymentIntent”](#後端建立-paymentintent) 金額請由後端依自己的訂單計算,不要相信前端傳來的數字。 | 欄位 | 必填 | 說明 | | ------------- | -- | -------------------------------------------------------- | | `amount` | 必填 | 整數,1 到 200,000 | | `orderId` | 必填 | 1 到 255 字元,不可含 `#` 與控制字元 | | `returnUrl` | 必填 | 付款成功後導回的網址。正式環境必須是 https,主機名稱要在允許網域清單內 | | `currency` | 選填 | 只能是 `TWD` | | `description` | 選填 | 最多 1,000 字元 | | `metadata` | 選填 | 最多 20 組;key 最多 40 字元,只能用英數與底線;value 最多 500 字元;總共不超過 8 KB | | `require3ds` | 選填 | `true` 時一律走 3D 驗證 | ```bash curl -X POST "https://embed-api.oen.tw/v1/payment-intents" \ -H "Authorization: Bearer $OEN_EMBED_SECRET_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order-0001" \ -d '{ "amount": 1200, "orderId": "order-0001", "returnUrl": "https://shop.example.com/payment/complete" }' ``` 回應格式是 `{ "data": { ... }, "requestId": "..." }`。`data` 就是 PaymentIntent: * `clientSecret`:**只在建立時回傳這一次**,交給前端使用。 * `id`:存進你的訂單,之後查詢、退款都用它。 ### PaymentIntent 的狀態 [Section titled “PaymentIntent 的狀態”](#paymentintent-的狀態) | 狀態 | 意思 | | --------------------------------- | ------------------------------------------------------------- | | `created`、`requiresPaymentMethod` | 等待付款者輸入卡片 | | `requiresAction` | 等待 3D 驗證。取消或逾時會回到 `requiresPaymentMethod`,可以用同一筆重新付款 | | `processing` | 付款處理中 | | `succeeded` | 付款成功(終態) | | `failed` | 付款失敗(終態)。原因在 `lastPaymentError.code`;要重新付款請建立新的 PaymentIntent | | `canceled` | 已取消(終態) | 停在 `processing` 超過 15 分鐘的付款,應援每 5 分鐘會自動對帳一次,所以大約 20 分鐘後再查通常就有結果。 ## 前端:載入 SDK 並付款 [Section titled “前端:載入 SDK 並付款”](#前端載入-sdk-並付款) SDK 還沒有發佈到 npm,請用 CDN 載入。正式環境建議鎖定版本並加上 SRI,`integrity` 的值取自同目錄的 `sri-hashes.json`。 ```html
``` * 想一直用最新版,可以改載 `https://static-assets.oen.tw/oenpay-sdk/v1/oenpay.umd.js`。這個網址每次發佈都會更新,**不能**加 `integrity`。 * 結帳頁必須是 https,付款 iframe 不允許嵌在 http 頁面裡。 ### 導回頁 [Section titled “導回頁”](#導回頁) 只有付款成功才會導向 `returnUrl`,網址會帶上 `payment_intent_client_secret` 與 `redirect_status`。`redirect_status` 只是提示,導回頁一定要再用 `oenpay.retrievePaymentIntent(clientSecret)` 查一次狀態。 出貨以 webhook 為準 前端查到的結果只用來給消費者即時回饋。出貨請以後端收到的 `payment_intent.succeeded` 為準,出貨前再用 `GET /v1/payment-intents/{id}` 交叉確認。 ## Webhook [Section titled “Webhook”](#webhook) ### 事件 [Section titled “事件”](#事件) | 事件 | 什麼時候 | | --------------------------------------------------- | ------------------------------------- | | `payment_intent.created` | 建立 PaymentIntent | | `payment_intent.requires_action` | 等待 3D 驗證 | | `payment_intent.succeeded` | 付款成功(出貨依據) | | `payment_intent.failed` | 付款失敗,原因在 `data.lastPaymentError.code` | | `payment_intent.canceled` | 已取消 | | `refund.created`、`refund.succeeded`、`refund.failed` | 退款 | ### 內容與 header [Section titled “內容與 header”](#內容與-header) ```json { "id": "evt_0123456789abcdef01234567", "type": "payment_intent.succeeded", "api_version": "2026-06-01", "created": 1790000000, "data": { "id": "…", "status": "succeeded", "amount": 1200, "orderId": "order-0001" } } ``` * `data` 是事件發生時的 PaymentIntent 或 Refund,不含 `clientSecret`。 * Header:`OenPay-Signature`、`OenPay-Event-Type`、`OenPay-Event-Id`。重試時另外帶 `OenPay-Retry-Count`,`OenPay-Event-Id` 不變。 ### 驗證簽章 [Section titled “驗證簽章”](#驗證簽章) `OenPay-Signature` 的格式是 `t=,v1=`: 1. 從 header 拆出 `t` 與所有 `v1=`。 2. 用完整的 `whsec_` 字串當 key,對 `{t}.{原始 body}` 計算 HMAC-SHA256,輸出 hex。 3. 用固定時間比較函式比對,任一個 `v1=` 相符就通過;`t` 與現在相差超過 300 秒就拒絕。 三個常見錯誤 * 先 `JSON.parse` 再轉回字串會改變 body,驗證簽章一定失敗。 * 時間要用 header 的 `t`,不是 body 的 `created`。重試時兩者不同。 * 不要用一般的字串比較。 - cURL ```bash # 在本機模擬一個帶簽章的通知,測試你的驗簽程式 BODY='{"id":"evt_test_0001","type":"payment_intent.succeeded","created":1790000000,"data":{}}' T=$(date +%s) SIG=$(printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$OEN_EMBED_WEBHOOK_SECRET" | sed 's/^.* //') curl -X POST "http://localhost:3000/webhooks/oen" \ -H "Content-Type: application/json" \ -H "OenPay-Signature: t=$T,v1=$SIG" \ -d "$BODY" ``` - Node.js ```js import crypto from "node:crypto"; import express from "express"; const app = express(); const TOLERANCE_SECONDS = 300; // OenPay-Signature: t=,v1=[,v1=],簽的是 `${t}.${原始 body}` function verifySignature(rawBody, header, secret) { const parts = (header ?? "").split(",").map((p) => p.trim()); const t = Number(parts.find((p) => p.startsWith("t="))?.slice(2)); const sigs = parts.filter((p) => p.startsWith("v1=")).map((p) => p.slice(3)); if (!Number.isFinite(t) || sigs.length === 0) return false; if (Math.abs(Date.now() / 1000 - t) > TOLERANCE_SECONDS) return false; const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest(); // 更換密鑰後 24 小時內會帶兩個 v1=,任一個相符就通過 return sigs.some((sig) => { const received = Buffer.from(sig, "hex"); return received.length === expected.length && crypto.timingSafeEqual(received, expected); }); } // 驗簽要用原始 body,不要先 JSON.parse app.post("/webhooks/oen", express.raw({ type: "application/json" }), (req, res) => { const rawBody = req.body.toString("utf8"); if (!verifySignature(rawBody, req.header("OenPay-Signature"), process.env.OEN_EMBED_WEBHOOK_SECRET)) { return res.status(400).send("invalid signature"); } const event = JSON.parse(rawBody); // 用 event.id(evt_ 開頭)判斷是否處理過;處理過就直接回 200 res.sendStatus(200); // 回應之後再處理出貨、寄信等耗時工作 }); ``` - PHP ```php ,v1=[,v1=],簽的是 "{t}.{原始 body}" $t = null; $sigs = []; foreach (explode(',', $header) as $part) { [$key, $value] = array_pad(explode('=', trim($part), 2), 2, ''); if ($key === 't') $t = (int) $value; if ($key === 'v1') $sigs[] = $value; } $expected = hash_hmac('sha256', $t . '.' . $payload, $secret); $matched = false; foreach ($sigs as $sig) { if (hash_equals($expected, $sig)) $matched = true; } if ($t === null || abs(time() - $t) > 300 || !$matched) { http_response_code(400); exit('invalid signature'); } $event = json_decode($payload, true); // 用 $event['id'](evt_ 開頭)判斷是否處理過;處理過就直接回 200 http_response_code(200); ``` - Python ```python import hashlib import hmac import os import time from flask import Flask, abort, request app = Flask(__name__) # OenPay-Signature: t=,v1=[,v1=],簽的是 f"{t}.{原始 body}" def verify_signature(payload: bytes, header: str, secret: str) -> bool: parts = [p.strip() for p in header.split(",")] t = next((p[2:] for p in parts if p.startswith("t=")), "") sigs = [p[3:] for p in parts if p.startswith("v1=")] if not t.isdigit() or not sigs: return False if abs(time.time() - int(t)) > 300: return False expected = hmac.new(secret.encode(), f"{t}.".encode() + payload, hashlib.sha256).hexdigest() return any(hmac.compare_digest(expected, s) for s in sigs) @app.post("/webhooks/oen") def oen_webhook(): payload = request.get_data() if not verify_signature(payload, request.headers.get("OenPay-Signature", ""), os.environ["OEN_EMBED_WEBHOOK_SECRET"]): abort(400) event = request.get_json() # 用 event["id"](evt_ 開頭)判斷是否處理過;處理過就直接回 200 return "", 200 ``` ### 投遞與重試 [Section titled “投遞與重試”](#投遞與重試) * 單次逾時 30 秒。請先回 2xx,再處理寄信、出貨等耗時工作。 * 沒有回 2xx 會重試,最多 5 次,間隔約 1 分鐘、5 分鐘、30 分鐘、2 小時、24 小時,各有約 ±20% 的隨機誤差。 * 同一個事件可能送達不只一次,請用事件 `id` 判斷是否處理過。 * 投遞紀錄保留 30 天,可用 `GET /v1/webhooks/{id}/logs` 查詢。 * 更換密鑰(`POST /v1/webhooks/{id}/rotate-secret`)後 24 小時內,header 會同時帶新舊兩個 `v1=`。 * 沒有「發送測試事件」的 API。要測試驗證簽章程式,請用上面的 cURL 範例在本機模擬。 ## 退款 [Section titled “退款”](#退款) Embed 的退款由後端呼叫 `POST /v1/refunds`: | 欄位 | 必填 | 說明 | | ----------------- | -- | -------------------------------------------------- | | `paymentIntentId` | 必填 | PaymentIntent 的 `id` | | `reason` | 必填 | `duplicate`、`fraudulent` 或 `requested_by_customer` | | `amount` | 選填 | 不帶就退剩餘全額。可以多次部分退款,上限是 `amountRefundable` | | `metadata` | 選填 | 同 PaymentIntent | * 付款成功後 180 天內可以退款。 * 請帶 `Idempotency-Key`,例如 `refund-訂單編號-1`。 HTTP 200 不代表退款成功 金流商拒絕時 HTTP 仍是 200,結果在回應 body 的 `status`(`succeeded` 或 `failed`)。收到 409 `REFUND_RECONCILIATION_REQUIRED` 或 500 時,退款可能已經送出:**不要換一把新的 Idempotency-Key 重送**,先用 `GET /v1/refunds?paymentIntentId=…` 查詢結果。退款沒有自動對帳,久未有結果請帶 `requestId` 聯絡應援。 ## 冪等性與頻率限制 [Section titled “冪等性與頻率限制”](#冪等性與頻率限制) * 支援 `Idempotency-Key` 的端點:建立 PaymentIntent、建立退款、capture,以及 SDK 代打的 confirm。key 是 1 到 255 個 ASCII 可見字元。 * 同一把 key 重送會拿到第一次的回應,header 帶 `Idempotency-Replayed: true`。 * 前一次處理超過 5 分鐘沒有結果,會重新處理;記錄保留 24 小時。 * 同一把 key 用在不同內容或端點,會回 422 `IDEMPOTENCY_KEY_REUSED`。 * 頻率限制: * 每把 Secret Key 每分鐘 500 次。超過回 429 `AUTH_RATE_LIMITED`,請依 `Retry-After` 等待。 * Publishable Key:每個 IP 每分鐘 30 次,每把每分鐘 60 次。 * 另外還有一層防火牆上限:**同一個 IP 每 5 分鐘最多 100 個請求**,每把 Publishable Key 每 5 分鐘最多 600 個請求。超過會直接回 403,body 不是一般的錯誤格式,也沒有 `Retry-After`。批次對帳或大量補查時,要自己把請求分散在時間上。 ## 錯誤 [Section titled “錯誤”](#錯誤) 錯誤格式: ```json { "error": { "code": "INVALID_AMOUNT", "message": "(錯誤說明)", "field": "amount" }, "requestId": "req_…" } ``` 程式判斷請比對 `code`,`message` 的文字可能調整。回報問題時附上 `requestId`,回應 header 的 `X-Request-Id` 也是同一個值。 | HTTP | 錯誤碼 | 意思與處理 | | ---- | ------------------------------------------------------------------------------- | ----------------------------------------- | | 400 | `INVALID_REQUEST`、`INVALID_AMOUNT`、`INVALID_CURRENCY`、`METADATA_LIMIT_EXCEEDED` | 參數不符規則,看 `field` 與 `message` 修正 | | 400 | `MERCHANT_NOT_CONFIGURED` | 商家的金流設定不完整,請聯絡應援。不是卡片問題 | | 400 | `PAYMENT_INTENT_ALREADY_SUCCEEDED`、`PAYMENT_INTENT_CANCELED` | 已付款或已取消,不能再做這個動作 | | 400 | `REFUND_AMOUNT_EXCEEDED`、`REFUND_WINDOW_EXPIRED` | 超過可退金額,或超過付款後 180 天 | | 401 | `AUTH_INVALID_KEY` | 金鑰缺少、錯誤或已撤銷;或用 `pk_` 打了後端端點;或商家尚未開通 Embed | | 402 | `CARD_*`、`3DS_*` | 付款失敗,見下表 | | 403 | `FORBIDDEN_ORIGIN`、`DOMAIN_NOT_REGISTERED` | 結帳頁的主機名稱不在允許網域清單內 | | 404 | `PAYMENT_INTENT_NOT_FOUND`、`REFUND_NOT_FOUND` 等 | 資源不存在,或不屬於這把金鑰的商家 | | 409 | `IDEMPOTENCY_KEY_IN_PROGRESS`、`REFUND_IN_PROGRESS` | 同一筆正在處理,稍後用同一把 key 重送 | | 409 | `REFUND_RECONCILIATION_REQUIRED` | 退款結果不明,不可換 key 重送,先查詢 | | 422 | `IDEMPOTENCY_KEY_REUSED` | 同一把 key 用在不同內容或端點 | | 429 | `AUTH_RATE_LIMITED` | 超過頻率限制,依 `Retry-After` 等待 | | 500 | `INTERNAL_ERROR` | 結果未知。付款或退款可能已經發生,先查狀態,不要直接重來 | ### 付款失敗的原因 [Section titled “付款失敗的原因”](#付款失敗的原因) 這些值會出現在前端 SDK 的錯誤、`payment_intent.failed` webhook 與 `lastPaymentError.code`。請不要把內部訊息原樣轉給付款者,可以參考右欄的文案。 | 錯誤碼 | 原因 | 建議對付款者顯示 | | ------------------------- | -------------- | ------------------------- | | `CARD_DECLINED` | 發卡銀行拒絕 | 發卡銀行拒絕這筆交易,請改用其他卡片或聯絡發卡銀行 | | `CARD_NOT_SUPPORTED` | 商家沒有開通國外卡或這種卡別 | 這張卡目前無法使用,請改用其他卡片 | | `CARD_INSUFFICIENT_FUNDS` | 額度不足 | 卡片額度不足,請改用其他卡片 | | `CARD_EXPIRED` | 卡片過期 | 卡片已過期,請改用其他卡片 | | `CARD_INVALID_NUMBER` | 卡號無效 | 卡號有誤,請重新確認 | | `CARD_INVALID_CVV` | 安全碼錯誤 | 安全碼有誤,請重新確認 | | `CARD_RISK_DECLINED` | 未通過風險控管 | 這筆交易無法完成,請改用其他卡片 | | `3DS_FAILED` | 3D 驗證未通過 | 銀行安全驗證未通過,請重新付款或改用其他卡片 | | `3DS_CANCELED` | 付款者在驗證頁按了取消 | 您已取消銀行安全驗證,要繼續請重新付款 | | `3DS_TIMEOUT` | 3D 驗證逾時 | 銀行安全驗證逾時,請重新付款 | ## 端點一覽 [Section titled “端點一覽”](#端點一覽) 後端用 Secret Key 呼叫: | Method | Path | 用途 | | ---------------------- | --------------------------------- | ----------------------------------------------------------- | | `POST` | `/v1/payment-intents` | 建立 PaymentIntent | | `GET` | `/v1/payment-intents` | 列表,可用 `status`、`createdGte`、`createdLte` 篩選,`limit` 1 到 100 | | `GET`、`PATCH` | `/v1/payment-intents/{id}` | 查詢;更新(只能改 `metadata`) | | `POST` | `/v1/payment-intents/{id}/cancel` | 取消 | | `POST` | `/v1/refunds` | 建立退款 | | `GET` | `/v1/refunds`、`/v1/refunds/{id}` | 查詢退款 | | `POST`、`GET` | `/v1/webhooks` | 建立、列出 webhook | | `GET`、`PATCH`、`DELETE` | `/v1/webhooks/{id}` | 管理 webhook | | `GET` | `/v1/webhooks/{id}/logs` | 投遞紀錄 | | `POST` | `/v1/webhooks/{id}/rotate-secret` | 更換簽章密鑰 | `/v1/tokens`、`/v1/confirm`、`/v1/payment-intents/{id}/status` 與 `/v1/3ds/*` 由 SDK 代為呼叫,後端不需要處理。 ## 測試 [Section titled “測試”](#測試) 在測試環境的商家上測試。到期日填未來任一日期,安全碼任意三碼: | 卡號 | 結果 | | ------------------ | -------------- | | `4000000000000002` | 直接成功 | | `4000010000000010` | 跳出 3D 驗證,驗證後成功 | * 要確定走到 3D 驗證,建立 PaymentIntent 時帶 `require3ds: true`。 * 測試環境另有 `POST /v1/3ds/simulate`,可以不經過驗證頁,直接把付款推到成功、失敗、取消或逾時,方便測試導回頁與 webhook。正式環境沒有這支 API。 ## 上線前檢查 [Section titled “上線前檢查”](#上線前檢查) * [ ] Secret Key 與 Webhook Secret 只放在後端環境變數或密鑰管理服務,沒有進版本控制。 * [ ] 正式站的主機名稱已加入允許網域清單。 * [ ] Webhook 接收端是 https、會驗證簽章、會用事件 `id` 判斷是否重複。 * [ ] 出貨以 `payment_intent.succeeded` 為準,出貨前再用 API 確認。 * [ ] 建立 PaymentIntent 與退款都帶 `Idempotency-Key`。 * [ ] 退款流程會檢查回應 body 的 `status`,不是只看 HTTP 200。 * [ ] 付款或退款收到 500 時,先查狀態,不直接重來。 * [ ] 在測試環境走過付款(含 3D 驗證成功與取消)、退款與 webhook。 # Subscription API > 建立產品與方案,管理每一筆訂閱從試用、扣款、變更方案到取消的完整生命週期。需要先開通。 Subscription API 用來經營訂閱制服務:你先建立**產品**與**方案**,把訂閱頁網址放在自己的網站,消費者在應援的訂閱頁輸入卡片完成訂閱。之後的續扣、試用、方案變更、取消與退款,都可以用 API 管理,並透過 webhook 同步狀態。 開始前請確認 * **需要開通**:網域要開啟 API 串接(開發者模式),操作的 CRM 帳號也要有 Subscription API 金鑰的權限。 * **收單限制**:訂閱的首期扣款目前只支援部分收單機構,不符合時訂閱頁會回 422 `SA047`。開通前請先跟業務人員確認。 * **沒有公開的測試環境**:目前只有正式環境可以使用。 ## 跟 Payment API 的定期定額差在哪 [Section titled “跟 Payment API 的定期定額差在哪”](#跟-payment-api-的定期定額差在哪) 只要「每月固定金額自動扣款」,用 [Payment API 的定期定額](/developers/subscriptions/)就夠了。需要以下功能時,再考慮 Subscription API: | | Payment API 定期定額 | Subscription API | | ------- | ------------------------- | ------------------------ | | 建立方式 | 每位訂閱人各建立一次結帳頁 | 先建立產品與方案,所有人共用同一個訂閱頁網址 | | 週期 | 每月,或每 1 到 12 個月 | 天、月、年,任意間隔 | | 試用期 | 不支援 | 支援試用天數與試用次數限制 | | 方案變更 | 不支援 | 支援;分級產品升級會產生補差額的付款連結 | | 取消 | 立即取消 | 期末取消、恢復、立即終止並按日計算退款 | | 扣款失敗 | 預設不重扣,可在 CRM 開啟重扣(最多 2 次) | 自動重試,另可產生補繳連結 | | 訂閱人自助管理 | 無 | 有,訂閱人以 email 驗證碼登入 | | 幣別 | 只支援 TWD | TWD | | 金鑰 | CRM 產生的 Payment API token | 獨立的 `sub_sk_` 金鑰,可限制權限範圍 | | Webhook | 沒有簽章 | 有簽章,17 種事件,可查投遞紀錄並手動重送 | ## 取得金鑰 [Section titled “取得金鑰”](#取得金鑰) 1. 確認網域已開啟 API 串接(開發者模式)。新申請的商家預設就是開啟的;舊商家請洽業務人員。沒開時產生金鑰會回 `M002`。 2. 進入 CRM「訂閱管理」→「設定」→「API 金鑰」(`/crm/subscription/settings/api-keys`)。你的帳號需要 Subscription API 金鑰的權限。 3. 建立金鑰並勾選需要的權限範圍(scope)。金鑰格式是 `sub_sk_` 加 32 碼,**只會顯示一次**。 4. 在同一區的「Webhook」頁(`/crm/subscription/settings/webhooks`)設定接收網址,取得簽章密鑰(`sub_whsec_` 開頭)。 | scope | 可以做的事 | | -------------------- | -------------------------- | | `read:*` | 所有查詢 | | `write:product` | 建立、更新產品 | | `write:plan` | 建立、更新方案 | | `write:subscription` | 變更方案、變更期間、取消、恢復、終止、退款、補繳連結 | | `write:customer` | 更新客戶資料 | | `ops:webhook` | 查詢投遞紀錄、手動重送 | * 更換金鑰後,舊金鑰會再有效 24 小時。 * 金鑰無效回 401 `X022`,權限範圍不足回 403 `X023`。 ## 共通規格 [Section titled “共通規格”](#共通規格) * Base URL:`https://subscription-api.oen.tw`,所有路徑都以 `/v1` 開頭。 * 每個請求帶 `Authorization: Bearer sub_sk_…`。 * 成功回應一律 HTTP 200,格式是 `{ "data": …, "paging"?: { "next": … } }`。 * 錯誤回應是 `{ "errno": "SA001", "message": "…" }`,HTTP 狀態碼依錯誤而定。程式判斷請比對 `errno`。 * 分頁:把上一頁的 `paging.next` 原封不動放進 `?page=`;沒有下一頁時是 `null`。列表每頁 50 筆。 * ID 前綴:訂閱 `sub_`、產品 `prod_`、方案 `plan_`、客戶 `cus_`、事件 `evt_`。 ### 訂閱狀態 [Section titled “訂閱狀態”](#訂閱狀態) | 狀態 | 意思 | | ------------ | ------------------------------- | | `trialing` | 試用中 | | `active` | 正常扣款中 | | `paused` | 扣款失敗,暫停中。`nextChargeAt` 是下次重試時間 | | `failed` | 扣款失敗且不再重試 | | `cancelled` | 已取消(期末取消) | | `terminated` | 已終止 | `nextChargeAt` 是實際扣款的時間,一律是台北時間上午 9 點。 ## 端點 [Section titled “端點”](#端點) | Method | Path | scope | 說明 | | ------ | -------------------------------------------- | -------------------- | --------------------------------------------------------- | | `POST` | `/v1/products` | `write:product` | 建立產品。`subscriptionType` 是 `fixedPeriod` 或 `tiered`,建立後不能改 | | `GET` | `/v1/products` | `read:*` | 產品列表 | | `GET` | `/v1/products/{productId}` | `read:*` | 查詢產品 | | `PUT` | `/v1/products/{productId}` | `write:product` | 更新產品 | | `GET` | `/v1/products/{productId}/subscription-url` | `read:*` | 取得訂閱頁網址 | | `POST` | `/v1/products/{productId}/plans` | `write:plan` | 建立方案。`billingPeriod` 建立後不能改 | | `GET` | `/v1/products/{productId}/plans` | `read:*` | 方案列表 | | `PUT` | `/v1/products/{productId}/plans/{planId}` | `write:plan` | 更新方案 | | `GET` | `/v1/products/{productId}/subscriptions` | `read:*` | 某產品的訂閱列表 | | `GET` | `/v1/subscriptions/{id}` | `read:*` | 查詢訂閱,含產品、方案、客戶、付款與最近 100 筆操作紀錄 | | `POST` | `/v1/subscriptions/{id}/plan-change` | `write:subscription` | 變更方案 | | `POST` | `/v1/subscriptions/{id}/period-change` | `write:subscription` | 變更期間結束日 | | `POST` | `/v1/subscriptions/{id}/cancel` | `write:subscription` | 期末取消 | | `POST` | `/v1/subscriptions/{id}/resume` | `write:subscription` | 恢復已取消的訂閱 | | `POST` | `/v1/subscriptions/{id}/terminate` | `write:subscription` | 立即終止,回應附退款試算 | | `POST` | `/v1/subscriptions/{id}/refunds` | `write:subscription` | 終止後退款 | | `POST` | `/v1/subscriptions/{id}/recovery-links` | `write:subscription` | 產生補繳連結,3 天內有效 | | `GET` | `/v1/customers` | `read:*` | 客戶列表 | | `GET` | `/v1/customers/{customerId}` | `read:*` | 查詢客戶 | | `PUT` | `/v1/customers/{customerId}` | `write:customer` | 更新客戶姓名、電話、地址(email 不能改) | | `GET` | `/v1/webhook-deliveries` | `ops:webhook` | 投遞紀錄 | | `GET` | `/v1/webhook-deliveries/{deliveryId}` | `ops:webhook` | 單筆投遞紀錄 | | `POST` | `/v1/webhook-deliveries/{deliveryId}/resend` | `ops:webhook` | 手動重送 | ### 主要欄位 [Section titled “主要欄位”](#主要欄位) **建立產品** `POST /v1/products` | 欄位 | 必填 | 說明 | | ------------------------------------------------------ | -- | ------------------------------------------------------ | | `name` | 必填 | 最多 50 字 | | `subscriptionType` | 必填 | `fixedPeriod` 或 `tiered` | | `status` | 選填 | 預設 `inactive` | | `summary` | 選填 | 最多 100 字 | | `description` | 選填 | 產品說明 | | `trialReuse` | 選填 | 試用可以用幾次:`unlimited`、`once_per_plan`、`once_per_product` | | `gracePeriodDays` | 選填 | 寬限天數 | | `basicInfoFields` | 選填 | 訂閱頁要不要收電話、地址,以及是否接受海外地址 | | `websiteUrl`、`successRedirectUrl`、`failureRedirectUrl` | 選填 | 你的網站與導回網址,必須是 https | | `customerServicePhone`、`customerServiceEmail` | 選填 | 顯示在訂閱頁的客服資訊 | **建立方案** `POST /v1/products/{productId}/plans` | 欄位 | 必填 | 說明 | | --------------- | -- | ---------------------------------------------------------- | | `name` | 必填 | 1 到 30 字 | | `price` | 必填 | 整數,0 以上 | | `billingPeriod` | 必填 | `{ "unit": "day" \| "month" \| "year", "interval": 1 以上 }` | | `trialDays` | 選填 | 試用天數;沒帶時沿用產品的設定 | | `description` | 選填 | 最多 1,000 字 | | `status` | 選填 | 預設 `active` | 消費者從訂閱頁回到你的網址時,會帶上 `?subscriptionId=sub_…&result=success|failure&action=subscription_create|recovery_payment|plan_upgrade`。導回只是提示,請以 webhook 或查詢結果為準。 ## Idempotency-Key [Section titled “Idempotency-Key”](#idempotency-key) 變更方案、變更期間、取消、恢復、終止、退款與 webhook 重送都**一定要帶** `Idempotency-Key`,沒帶會回 400 `SA034`。產品、方案、客戶的建立與更新,以及補繳連結,不需要帶。 * Header 名稱請寫成 `Idempotency-Key`(或全小寫)。 * 取消、恢復、終止:同一把 key 重送時,如果訂閱已經在目標狀態,會回原本的結果;內容不同則回 409 `SA044`。 * 退款: * 處理中回 409 `SA025`。 * 先前已失敗回 409 `SA026`,要再退請換一把新的 key。 * 結果未定回 503 `SA028`,**請用同一把 key 重試**,不要換新的。 ## Webhook [Section titled “Webhook”](#webhook) ### 事件 [Section titled “事件”](#事件) `subscription_created`、`subscription_renewed`、`subscription_failed`、`subscription_recovered`、`subscription_paused`、`subscription_cancelled`、`subscription_resumed`、`subscription_terminated`、`subscription_plan_changed`、`subscription_period_changed`、`subscription_payment_method_updated`、`subscription_payment_refunded`、`subscription_trial_started`、`subscription_trial_ending`、`subscription_trial_ended`、`subscription_upcoming_charge`、`customer_updated`。 設定 webhook 時沒有指定事件,就會收到全部事件。 ### 內容與 header [Section titled “內容與 header”](#內容與-header) ```json { "id": "evt_…", "type": "subscription_renewed", "created": 1790000000, "data": { "subscription": { "id": "sub_…", "status": "active" }, "paymentDetail": { "amount": 299, "currency": "twd" } } } ``` Header 包含 `OenPay-Signature`、`OenPay-Event-Type`、`OenPay-Event-Id` 與 `OenPay-Delivery-Attempt`。 ### 驗證簽章 [Section titled “驗證簽章”](#驗證簽章) 格式與算法跟 Embed 相同:`OenPay-Signature: t=,v1=`,用 `sub_whsec_` 密鑰對 `{t}.{原始 body}` 計算 HMAC-SHA256。伺服器端沒有規定時間差,建議拒絕超過 5 分鐘的通知。更換密鑰後 24 小時內會同時帶新舊兩個 `v1=`。 * cURL ```bash # 在本機模擬一個帶簽章的通知,測試你的驗簽程式 BODY='{"id":"evt_test_0001","type":"payment_intent.succeeded","created":1790000000,"data":{}}' T=$(date +%s) SIG=$(printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$OEN_SUBSCRIPTION_WEBHOOK_SECRET" | sed 's/^.* //') curl -X POST "http://localhost:3000/webhooks/oen" \ -H "Content-Type: application/json" \ -H "OenPay-Signature: t=$T,v1=$SIG" \ -d "$BODY" ``` * Node.js ```js import crypto from "node:crypto"; import express from "express"; const app = express(); const TOLERANCE_SECONDS = 300; // OenPay-Signature: t=,v1=[,v1=],簽的是 `${t}.${原始 body}` function verifySignature(rawBody, header, secret) { const parts = (header ?? "").split(",").map((p) => p.trim()); const t = Number(parts.find((p) => p.startsWith("t="))?.slice(2)); const sigs = parts.filter((p) => p.startsWith("v1=")).map((p) => p.slice(3)); if (!Number.isFinite(t) || sigs.length === 0) return false; if (Math.abs(Date.now() / 1000 - t) > TOLERANCE_SECONDS) return false; const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest(); // 更換密鑰後 24 小時內會帶兩個 v1=,任一個相符就通過 return sigs.some((sig) => { const received = Buffer.from(sig, "hex"); return received.length === expected.length && crypto.timingSafeEqual(received, expected); }); } // 驗簽要用原始 body,不要先 JSON.parse app.post("/webhooks/oen", express.raw({ type: "application/json" }), (req, res) => { const rawBody = req.body.toString("utf8"); if (!verifySignature(rawBody, req.header("OenPay-Signature"), process.env.OEN_SUBSCRIPTION_WEBHOOK_SECRET)) { return res.status(400).send("invalid signature"); } const event = JSON.parse(rawBody); // 用 event.id(evt_ 開頭)判斷是否處理過;處理過就直接回 200 res.sendStatus(200); // 回應之後再處理出貨、寄信等耗時工作 }); ``` * PHP ```php ,v1=[,v1=],簽的是 "{t}.{原始 body}" $t = null; $sigs = []; foreach (explode(',', $header) as $part) { [$key, $value] = array_pad(explode('=', trim($part), 2), 2, ''); if ($key === 't') $t = (int) $value; if ($key === 'v1') $sigs[] = $value; } $expected = hash_hmac('sha256', $t . '.' . $payload, $secret); $matched = false; foreach ($sigs as $sig) { if (hash_equals($expected, $sig)) $matched = true; } if ($t === null || abs(time() - $t) > 300 || !$matched) { http_response_code(400); exit('invalid signature'); } $event = json_decode($payload, true); // 用 $event['id'](evt_ 開頭)判斷是否處理過;處理過就直接回 200 http_response_code(200); ``` * Python ```python import hashlib import hmac import os import time from flask import Flask, abort, request app = Flask(__name__) # OenPay-Signature: t=,v1=[,v1=],簽的是 f"{t}.{原始 body}" def verify_signature(payload: bytes, header: str, secret: str) -> bool: parts = [p.strip() for p in header.split(",")] t = next((p[2:] for p in parts if p.startswith("t=")), "") sigs = [p[3:] for p in parts if p.startswith("v1=")] if not t.isdigit() or not sigs: return False if abs(time.time() - int(t)) > 300: return False expected = hmac.new(secret.encode(), f"{t}.".encode() + payload, hashlib.sha256).hexdigest() return any(hmac.compare_digest(expected, s) for s in sigs) @app.post("/webhooks/oen") def oen_webhook(): payload = request.get_data() if not verify_signature(payload, request.headers.get("OenPay-Signature", ""), os.environ["OEN_SUBSCRIPTION_WEBHOOK_SECRET"]): abort(400) event = request.get_json() # 用 event["id"](evt_ 開頭)判斷是否處理過;處理過就直接回 200 return "", 200 ``` ### 投遞與重試 [Section titled “投遞與重試”](#投遞與重試) * 單次逾時 15 秒,只有 2xx 算成功,**不跟隨 3xx 轉址**。 * 失敗會自動重試 3 次,間隔約 1 分鐘、5 分鐘、30 分鐘。 * 可用 `GET /v1/webhook-deliveries` 查詢,用 `POST /v1/webhook-deliveries/{id}/resend` 手動重送。 * 同一個事件可能送達不只一次,請用事件 `id` 判斷是否處理過。 ## 錯誤碼 [Section titled “錯誤碼”](#錯誤碼) ### 共通 [Section titled “共通”](#共通) | errno | HTTP | 意思 | | ------ | ---- | ------------------------------- | | `X006` | 400 | 欄位驗證失敗,`message` 會列出欄位與原因 | | `X019` | 400 | `page` 對不到這個列表的資料 | | `X028` | 400 | `page` 無法解析 | | `X016` | 400 | 目前狀態不能建立補繳連結(只有扣款失敗且排定重試中的訂閱可以) | | `X022` | 401 | 金鑰缺少、格式錯誤或無效 | | `X023` | 403 | 金鑰缺少這個操作需要的 scope | | `X011` | 404 | 路徑不存在 | ### 訂閱與方案 [Section titled “訂閱與方案”](#訂閱與方案) | errno | HTTP | 意思 | | ------------------------------- | ---- | ------------------------------------ | | `SA001` | 404 | 找不到訂閱,或不屬於這個商家 | | `SA002`、`SA003`、`SA004`、`SA006` | 409 | 目前狀態不能取消、終止、恢復或變更期間 | | `SA005` | 409 | 目前狀態不能變更方案 | | `SA008` | 409 | 讀取後狀態已改變,請重新查詢再操作 | | `SA009` | 409 | 因扣款失敗暫停的訂閱,請改用補繳連結 | | `SA010` | 503 | 暫時讀不到付款資料,可以用同一把 key 重試 | | `SA011` | 404 | 找不到方案,或不屬於這個產品 | | `SA012` | 400 | 已經是這個方案 | | `SA013` | 409 | 同一筆訂閱每天(台北時間)只能成功變更方案一次 | | `SA014` | 409 | 已有進行中的方案變更 | | `SA015` | 409 | 扣款仍在處理或結果未定。**不代表失敗**,請稍後查詢,不要重送 | | `SA016` | 409 | 這筆變更已經完成 | | `SA018` | 400 | 期間結束日格式錯誤 | | `SA019` | 422 | 期間結束日必須晚於今天 | | `SA045` | 404 | 找不到產品 | | `SA046` | 404 | 找不到客戶 | | `SA047` | 422 | 首期扣款的收單機構不支援 Subscription API,請洽業務人員 | ### 退款 [Section titled “退款”](#退款) | errno | HTTP | 意思 | | ------- | ---- | ----------------------------- | | `SA007` | 409 | 訂閱尚未終止,不能退款 | | `SA021` | 409 | 終止時沒有算出可退金額 | | `SA022` | 409 | 金額超過剩餘可退額度 | | `SA023` | 409 | 交易本身的可退餘額不足 | | `SA024` | 409 | 這筆交易另有處理中的扣款 | | `SA025` | 409 | 同一筆退款仍在處理中 | | `SA026` | 409 | 這把 key 先前的退款已失敗,要再退請換新的 key | | `SA027` | 502 | 金流端拒絕這筆退款。先確認交易狀態,要再退請換新的 key | | `SA028` | 503 | 退款結果未定,請用同一把 key 重試 | ### Idempotency 與 webhook [Section titled “Idempotency 與 webhook”](#idempotency-與-webhook) | errno | HTTP | 意思 | | ------- | ---- | -------------------------- | | `SA034` | 400 | 沒有帶 `Idempotency-Key` | | `SA035` | 409 | `Idempotency-Key` 已逾期,請換新的 | | `SA044` | 409 | 同一把 key 用在內容不同的請求 | | `SA030` | 404 | 找不到投遞紀錄 | | `SA031` | 409 | 這筆投遞正在處理中 | | `SA032` | 409 | 對應的 webhook 未啟用 | | `SA033` | 409 | 事件內容已無法取得,不能重送 | | `SA040` | 400 | 這次操作需要重新確認卡片資訊 | | `SA041` | 400 | 目前的付款方式不支援這次操作 |