電子發票號碼與狀態
網域開通「應援代開電子發票」後,API 交易在付款成功後會自動開立發票(建立交易時怎麼帶發票資訊,見單次付款)。你的系統可以用兩種方式取得發票號碼與狀態:
| 方式 | 怎麼取得 | 要先開啟嗎 |
|---|---|---|
| 查詢交易 | 查詢交易的回應裡,每筆交易都有 invoice 物件 |
不用 |
| 發票通知 | 發票開立、作廢時,應援 POST 一則通知到你的網址 |
要請應援開啟 |
下面三支 API 回傳的每一筆交易都多了 invoice 物件,不用多帶參數:
GET /transactions/{id}:查詢交易明細GET /transactions:查詢交易列表GET /order/{orderId}/transactions:用訂單編號查詢
curl "https://payment-api.oen.tw/transactions/2HhndgEquCbDzC5OyVxWSGZmd2l" \ -H "Authorization: Bearer $OEN_API_TOKEN"其他欄位省略,完整欄位見 Transaction 物件。
{ "code": "S0000", "data": { "id": "P20260928AB12CD34", "transactionId": "2HhndgEquCbDzC5OyVxWSGZmd2l", "amount": 1200, "status": "charged", "orderId": "A20260928001", "paidAt": "2026-09-28T02:41:03.118Z", "invoice": { "invoiceNumber": "ZZ00001234", "invoiceStatus": "issued", "invoiceDate": "2026-09-28T02:41:30.000Z", "randomNumber": "0000" } }, "message": ""}invoice 物件
Section titled “invoice 物件”| 欄位 | 說明 |
|---|---|
invoiceNumber |
發票號碼。只在 issued、voided 時有值,其他狀態是 null |
invoiceStatus |
發票狀態,見下表 |
invoiceDate |
開立時間(UTC,ISO 8601)。換算成台北時間就是發票日期。只在 issued、voided 時有值 |
randomNumber |
4 碼隨機碼。只在 issued、voided 時有值 |
| invoiceStatus | 意思 | 建議做法 |
|---|---|---|
issuing |
開立中,還沒拿到號碼 | 稍後再查 |
issued |
已開立,發票有效。作廢處理中、作廢失敗的發票也是這個狀態 | 記下號碼 |
failed |
開立失敗 | 聯絡應援客服 |
voided |
已作廢。invoiceNumber 是 null 時,表示發票還沒開出就隨全額退款取消 |
依你的流程更新訂單 |
unknown |
其他狀態 | 稍後再查,或聯絡應援 |
invoice一定會出現。交易沒有發票時是null:網域沒開通電子發票、交易還沒付款,或發票暫時讀不到。- 剛付款完成時查詢,
invoice可能還是null或issuing,稍後再查即可。 - 退款開立折讓不會改變發票狀態,部分退款後仍是
issued。 - 發票作廢後由應援重新開立、並對應到原交易時,查到的是新的那張發票。
- 回應不含任何買受人資料(統編、姓名、地址、電話、Email、載具、捐贈碼)。
- 請聯絡應援,為你的網域開啟「Webhook 新版事件」。測試環境與正式環境分別開啟。
- 通知送到你在 CRM 設定的「交易資料回傳網址位置」,和付款通知是同一個網址,不用另外設定。
- 這個開關也會開啟
refund、subscription_cancelled、subscription_retry三種通知。用不到的請依purpose略過,不要回錯誤。
開啟之前發生的開立、作廢,開啟後不會補送。
| purpose | 什麼時候送 |
|---|---|
invoice_issued |
發票開立成功、拿到發票號碼之後。應援在後台重試開立成功時也會送 |
invoice_voided |
發票作廢成功之後。作廢由應援客服處理,見電子發票 |
同一張發票,每一種通知只送一次。發票作廢後由應援重新開立、並對應到原交易時,舊發票送 invoice_voided,新發票送 invoice_issued,兩則的 chargeId 相同、id 不同。
不會送通知的情況:
- 不是 API 交易的發票,例如 Embed、商店訂單、POS 的發票。
- 退款開立的折讓。
- 發票還沒開出就隨全額退款取消(沒有發票號碼)。查詢交易會回
voided,invoiceNumber是null。 - 開立失敗的發票。查詢交易會回
failed。
POST /webhooks/oen HTTP/1.1Host: shop.example.comContent-Type: application/json
{ "purpose": "invoice_issued", "id": "invoice#2HhnqT4cWz0mR8bLxN3vYs6KdPa#issued", "transactionId": "invoice#2HhnqT4cWz0mR8bLxN3vYs6KdPa#issued", "merchantId": "ming", "chargeId": "2HhndgEquCbDzC5OyVxWSGZmd2l", "orderId": "A20260928001", "invoiceNumber": "ZZ00001234", "invoiceStatus": "issued", "invoiceDate": "2026-09-28T02:41:30.000Z", "randomNumber": "0000", "invoiceAmount": 1200, "createdAt": "2026-09-28T02:41:31.204Z", "message": ""}invoice_voided 的格式相同:purpose 是 invoice_voided、invoiceStatus 是 voided、id 結尾是 #voided,號碼、日期、隨機碼是被作廢的那張發票。
幾個容易弄錯的欄位(完整欄位見付款通知內容):
id:這則通知的事件編號,同一張發票的同一種通知固定不變,可以用來判斷重複。chargeId:發票所屬交易的內部 id,和charge通知的id相同。transactionId:和id相同,不是交易編號。沿用charge通知的程式時請特別注意。invoiceAmount:發票金額(含稅),是發票品項的加總;品項資料不完整時是null。createdAt:這則通知建立的時間,不是交易建立時間。
- 先回 HTTP 2xx,再到背景處理。應援每次最多等 10 秒。
- 用
id判斷是否處理過,處理過就略過。 - 有產生簽章密鑰時,先驗證
OenPay-Signature(見確認通知的真假);沒有密鑰時通知沒有簽章。不論哪一種,都可以用chargeId呼叫GET /transactions/{id},比對回應裡invoice的invoiceNumber與invoiceStatus。 - 依
invoiceStatus更新訂單的發票資料。
- 送出規則和付款通知相同:最多送 3 次,間隔約 2 秒、4 秒;不跟隨轉址;3 次都失敗就不會再送。見付款通知(webhook)。
- 重新開立時,
invoice_voided與invoice_issued可能順序顛倒送達。請以invoiceStatus與invoiceNumber判斷,不要依收到的順序。 - 發票狀態更新後如果系統中斷,這則通知不會補送。請定期用用訂單編號查詢或查詢交易列表對帳,補上還沒有發票號碼的訂單。
app.post("/webhooks/oen", express.json(), (req, res) => { res.sendStatus(200); // 先回 200
const event = req.body; if (event.purpose === "invoice_issued" || event.purpose === "invoice_voided") { handleInvoice(event).catch((err) => console.error("OEN invoice webhook", err)); } // charge 等其他 purpose 另外處理;不認得的 purpose 一律略過});
async function handleInvoice(event) { if (await alreadyProcessed(event.id)) return; // 用 id 去重
// 不相信通知內容,用 chargeId 回查 const res = await fetch(`https://payment-api.oen.tw/transactions/${encodeURIComponent(event.chargeId)}`, { 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 invoice = result.data.invoice; if (!invoice || invoice.invoiceNumber !== event.invoiceNumber) return; // 已被重新開立等情況,以回查為準
await saveInvoice(result.data.orderId, invoice); // 同一張重複寫入也要安全 await markProcessed(event.id);}導覽助手暫停服務中。請使用右上角的搜尋,或聯絡客服。
我可以幫你選擇收款方式、回答 API 串接與 CRM 操作問題,並附上文件出處。
