Shopify Plus 香港 FPS 轉數快支付整合教學

Key Takeaways
- Shopify Plus 無原生 FPS,需經 PSP App、Payments Apps API 或離線 QR
- FPS 為推送式付款,結算必須靠 webhook,不能前端輪詢
- 動態 HKQR 用 EMVCo TLV 加 CRC16-CCITT,Tag 01 須為 12
- 至少用三家香港銀行 App 實測掃碼,各家容錯度不同
- 抽象成通用介面後可延伸至 PayNow、DuitNow 等亞太軌道
Shopify Plus 沒有原生的轉數快(FPS)支付選項,實務上有三條路:接入支援 FPS 的香港收單機構 App、用 Payments Apps API 自建支付 App,或以離線付款方式配合動態 HKQR 收款並自動對帳。本文逐步示範三者的設定、程式碼與排錯方法。
為什麼 Shopify Plus 沒有現成的 FPS 按鈕?
轉數快(Faster Payment System, FPS)是由香港銀行同業結算有限公司(HKICL)營運、香港金融管理局(HKMA)監管的即時支付基建,2018 年 9 月投入服務,支援港元與人民幣的即時轉賬,並以手機號碼、電郵或 FPS ID 作為收款代理識別。根據 HKMA 公布的轉數快統計資料,FPS 登記數目已突破 1,500 萬個,遠超香港成年人口,代表大部分本地消費者手機內已有可用的 FPS 入口。
但 FPS 本身是「銀行間結算軌道」,不是一個發卡組織式的商戶收單網絡。它沒有像 Visa 那樣的統一授權/請款/退款 API 供全球平台直接對接,商戶端能力來自各家銀行與持牌支付服務供應商(PSP)各自包裝的介面。這是 Shopify Payments 在香港仍以信用卡為主、而 FPS 必須經第三方進入結帳流程的根本原因。
對跨境團隊來說,這一點值得放大理解:亞太區的即時支付軌道普遍是「本地基建 + 本地包裝」。ACI Worldwide 的 Prime Time for Real-Time 報告長期指出亞太是全球即時支付交易量最集中的區域,但印度 UPI、香港 FPS、新加坡 PayNow、馬來西亞 DuitNow、泰國 PromptPay 各有規格。你在香港做的 FPS 整合架構,若設計得好,就是日後接 PayNow、DuitNow 的樣板。
三條整合路線的取捨
路線 A:香港收單 App(最快上線)
透過 Shopify App Store 安裝支援 FPS 的本地 PSP,例如 QFPay、Yedpay、AsiaPay(PayDollar)等。優點是結帳頁直接出現 FPS 選項、退款走 PSP 後台、對帳有現成報表。缺點是抽成與月費由 PSP 決定,結帳體驗受 App 版面限制,且部分 App 對 Shopify Plus 的 Checkout Extensibility 支援程度不一。
路線 B:Payments Apps API 自建(最可控)
Shopify 的 Payments Apps API 讓你把自家或客戶自家的收單關係包成「離站付款方式」(offsite payment)。適合已有直接銀行/PSP 合約、需要多商店共用一套支付服務、或要把 FPS 與其他本地方式(八達通、支付寶香港、PayMe)收在同一個 App 內的集團。缺點是要自行處理 session 狀態機、退款、對帳與 App 審核。
路線 C:離線付款 + 動態 HKQR(最低成本)
用 Shopify 內建的 Manual Payment Method,結帳後在 Thank You 頁以 Checkout UI Extension 顯示動態 FPS QR,客戶付款後由自動化流程比對入賬並標記訂單為已付款。適合客單價高、訂單量中等、想避免抽成的品牌(例如珠寶、家具、B2B 補貨)。缺點是不即時、需要對帳機制,且退款要人手處理。
實務建議:訂單量高、SKU 多的 DTC 走 A 或 B;高客單、低頻的走 C。Branch8 在一個大中華區上市珠寶零售集團的 Shopify Plus 專案中,就同時保留了卡類收單與 FPS 兩條路,因為門店取貨訂單的客戶偏好本地即時轉賬。
Ready to Transform Your Ecommerce Operations?
Branch8 specializes in ecommerce platform implementation and AI-powered automation solutions. Contact us today to discuss your ecommerce automation strategy.
前置條件檢查清單
開始前先確認以下項目齊備,否則會在第 3 步卡住:
- Shopify Plus 商店,並已啟用 Checkout Extensibility(新版 Checkout)。舊版 checkout.liquid 無法安裝 Checkout UI Extension。
- 商業實體與商戶收款代理:以公司名義開立的香港銀行商戶戶口,並已登記 FPS 收款代理(FPS ID / 商戶識別)。個人 FPS 帳號不應用於商業收款。
- PSP 合約(路線 A/B):與支援 FPS 的持牌 PSP 或銀行完成 KYC,取得 sandbox 與 production 憑證。
- 開發環境:Node.js 20 LTS 以上、Shopify CLI 3.x、一個 Partner 帳號與 development store。
- HTTPS 可公開存取的服務端點(路線 B 必需),用於接收 payment session 請求與 PSP webhook。
- HKQR / EMVCo QR 規格文件(路線 C 必需):由你的銀行或 PSP 提供的 Merchant Presented Mode 欄位定義。EMVCo 公布的 QR Code 規格是共通基礎,但代理類型與 GUID 由 HKICL/銀行指定,務必以對方文件為準。
驗證 CLI 環境:
1node -v2# v20.11.13npm install -g @shopify/cli@latest4shopify version5# 3.x.x6shopify auth logout && shopify app dev --reset
路線 A:安裝香港 PSP App 並啟用 FPS
- 在 Shopify Admin 進入 Settings → Payments → Add payment methods,搜尋你的 PSP 名稱(例如 QFPay / Yedpay / AsiaPay),或直接從 Shopify App Store 安裝。
- 安裝後於 App 設定頁貼上 PSP 提供的
merchant_id、api_key、secret,並先選擇 Test / Sandbox mode。 - 在 PSP 後台的「支付方式」開關中勾選 FPS(部分後台稱 FPS QR 或 Faster Payment)。若沒有這個選項,通常代表你的商戶類別(MCC)或 KYC 尚未開通 FPS,需要向 PSP 申請。
- 回到 Shopify Settings → Payments,確認 FPS 出現在 Additional payment methods 清單並拖到理想的排序位置。香港市場建議放在信用卡之後、電子錢包之前。
- 用 Shopify 的 Bogus/Test 訂單走一次完整結帳,確認訂單狀態由
pending轉為paid。
預期輸出:訂單時間軸會出現 PSP 的 transaction reference,並可在 GraphQL Admin API 查到 gateway 名稱。
1curl -s -X POST "https://your-shop.myshopify.com/admin/api/2025-01/graphql.json" \2 -H "X-Shopify-Access-Token: $SHOPIFY_ADMIN_TOKEN" \3 -H "Content-Type: application/json" \4 -d '{"query":"{ orders(first:1, sortKey:CREATED_AT, reverse:true){ nodes { name displayFinancialStatus transactions { gateway status amountSet { shopMoney { amount currencyCode } } } } } }"}'
1{"data":{"orders":{"nodes":[{"name":"#1042","displayFinancialStatus":"PAID",2 "transactions":[{"gateway":"qfpay","status":"SUCCESS",3 "amountSet":{"shopMoney":{"amount":"1880.00","currencyCode":"HKD"}}}]}]}}}
若 displayFinancialStatus 停在 PENDING,先看第 8 節的排錯清單。
Ready to Transform Your Ecommerce Operations?
Branch8 specializes in ecommerce platform implementation and AI-powered automation solutions. Contact us today to discuss your ecommerce automation strategy.
路線 B:用 Payments Apps API 自建 FPS 支付 App
建立 App 骨架
1shopify app init --name fps-payments-app2cd fps-payments-app3shopify app generate extension --template payments_extension4# 選擇 Offsite payments
在 shopify.extension.toml 中定義付款方式與能力:
1[[extensions]]2type = "payments_extension"3name = "FPS (轉數快)"4handle = "fps-offsite"56 [extensions.payment_session]7 url = "https://pay.example.com/shopify/payment_sessions"89 [extensions.refund_session]10 url = "https://pay.example.com/shopify/refund_sessions"1112 [extensions.targeting]13 supported_countries = ["HK"]14 supported_currencies = ["HKD"]15 supports_deferred_payments = false16 supports_installments = false17 test_mode_available = true
處理 payment session 請求
Shopify 會在買家選擇 FPS 並按下付款時,POST 一個 payment session 到你的 payment_session.url。你的服務要向 PSP 建立一筆 FPS 交易,取得 QR 或跳轉頁,然後回傳 redirect_url。
1// server/routes/paymentSessions.js — Express 4, Node 202import crypto from "node:crypto";34export function verifyShopifyHmac(req) {5 const digest = crypto6 .createHmac("sha256", process.env.SHOPIFY_API_SECRET)7 .update(req.rawBody, "utf8")8 .digest("base64");9 const header = req.get("Shopify-Hmac-Sha256") || "";10 return crypto.timingSafeEqual(Buffer.from(digest), Buffer.from(header));11}1213export async function createPaymentSession(req, res) {14 if (!verifyShopifyHmac(req)) return res.status(401).send("bad hmac");1516 const { id, gid, amount, currency, test, payment_method } = req.body;17 if (currency !== "HKD") return res.status(422).send("unsupported currency");1819 // 向 PSP 建立 FPS 交易(此處以泛用 REST 介面示意)20 const psp = await fetch(`${process.env.PSP_BASE_URL}/v1/fps/charges`, {21 method: "POST",22 headers: {23 "Authorization": `Bearer ${test ? process.env.PSP_TEST_KEY : process.env.PSP_LIVE_KEY}`,24 "Content-Type": "application/json",25 "Idempotency-Key": id // 用 Shopify session id 做冪等鍵26 },27 body: JSON.stringify({28 amount_cents: Math.round(Number(amount) * 100),29 currency: "HKD",30 reference: gid,31 expires_in: 900 // FPS QR 建議 10–15 分鐘有效期32 })33 }).then(r => r.json());3435 await db.sessions.insert({ shopify_session_id: id, psp_charge_id: psp.id, gid });3637 return res.status(201).json({ redirect_url: psp.hosted_qr_url });38}
收到 PSP webhook 後結算 session
FPS 是推送式付款:客戶在自己的銀行 App 掃碼確認,PSP 才會回呼你。因此結算一定要走 webhook,不要靠前端輪詢。
1const RESOLVE = `2mutation PaymentSessionResolve($id: ID!) {3 paymentSessionResolve(id: $id) {4 paymentSession { id status { code } }5 userErrors { field message }6 }7}`;89const PENDING = `10mutation PaymentSessionPending($id: ID!, $pendingExpiresAt: DateTime!, $reason: PaymentSessionStatePendingReason!) {11 paymentSessionPending(id: $id, pendingExpiresAt: $pendingExpiresAt, reason: $reason) {12 paymentSession { id status { code } }13 userErrors { field message }14 }15}`;1617async function callPaymentsApi(shop, token, query, variables) {18 const r = await fetch(`https://${shop}/payments_apps/api/2025-01/graphql.json`, {19 method: "POST",20 headers: { "X-Shopify-Access-Token": token, "Content-Type": "application/json" },21 body: JSON.stringify({ query, variables })22 });23 return r.json();24}2526export async function pspWebhook(req, res) {27 // 1) 先驗 PSP 簽章(各家不同,通常是 HMAC-SHA256 over raw body)28 if (!verifyPspSignature(req)) return res.status(401).end();2930 const { charge_id, status } = req.body;31 const s = await db.sessions.findByPspChargeId(charge_id);32 if (!s) return res.status(202).end(); // 未知交易,別回 5xx3334 if (status === "SUCCEEDED") {35 await callPaymentsApi(s.shop, s.token, RESOLVE, { id: s.shopify_gid });36 } else if (status === "AWAITING_SETTLEMENT") {37 await callPaymentsApi(s.shop, s.token, PENDING, {38 id: s.shopify_gid,39 pendingExpiresAt: new Date(Date.now() + 24 * 3600 * 1000).toISOString(),40 reason: "BUYER_ACTION_REQUIRED"41 });42 }43 return res.status(200).end();44}
預期輸出(成功時):
1{"data":{"paymentSessionResolve":{"paymentSession":{2 "id":"gid://shopify/PaymentSession/1234567890",3 "status":{"code":"RESOLVED"}},"userErrors":[]}}}
退款同理,用 refundSessionResolve;FPS 退款在多數香港 PSP 是「新增一筆反向轉賬」而非撤銷授權,所以要在 App 內明確標示退款需時(通常 T+1 至 T+3 個工作日),避免客服誤導客戶。
路線 C:如何產生符合規格的動態 FPS QR?
FPS 商戶呈現式 QR 建立在 EMVCo Merchant-Presented QR 的 TLV(Tag-Length-Value)結構上:三位數長度、UTF-8 值、最後以 CRC16-CCITT(多項式 0x1021、初始值 0xFFFF)收尾。下面是可直接用的產生器骨架——代理類型、GUID 與商戶欄位一定要換成銀行/HKICL 文件上的實際值,不要照抄示意值。
1// fpsQr.js2const tlv = (tag, value) =>3 `${tag}${String(value.length).padStart(2, "0")}${value}`;45function crc16ccitt(str) {6 let crc = 0xffff;7 for (const byte of Buffer.from(str, "utf8")) {8 crc ^= byte << 8;9 for (let i = 0; i < 8; i++) {10 crc = crc & 0x8000 ? ((crc << 1) ^ 0x1021) & 0xffff : (crc << 1) & 0xffff;11 }12 }13 return crc.toString(16).toUpperCase().padStart(4, "0");14}1516export function buildFpsPayload({ guid, proxyType, proxyValue, amountHkd, reference, merchantName, mcc }) {17 const account =18 tlv("00", guid) + // 由 HKICL / 銀行指派19 tlv("01", proxyType) + // FPS ID / 手機 / 電郵20 tlv("02", proxyValue);2122 let payload =23 tlv("00", "01") + // Payload Format Indicator24 tlv("01", "12") + // 12 = 動態(單次使用)25 tlv("26", account) + // Merchant Account Information26 tlv("52", mcc) +27 tlv("53", "344") + // HKD ISO 4217 numeric28 tlv("54", amountHkd.toFixed(2)) +29 tlv("58", "HK") +30 tlv("59", merchantName.slice(0, 25)) +31 tlv("60", "Hong Kong") +32 tlv("62", tlv("05", reference)); // 訂單號,供對帳3334 payload += "6304";35 return payload + crc16ccitt(payload);36}
測試:
1node -e "import('./fpsQr.js').then(m=>console.log(m.buildFpsPayload({guid:'hk.example.psp',proxyType:'01',proxyValue:'1234567',amountHkd:1880,reference:'1042',merchantName:'DEMO STORE LTD',mcc:'5944'})))"2# 000201010212262600...5303344540719...62080504104263047A2B
驗收方法:把輸出丟進任何 QR 產生器,用你自己的銀行 App 掃描,確認金額、收款人與備註欄正確帶入。在真實環境上線前,一定要用至少三家不同銀行的 App(例如匯豐、中銀、恒生)各掃一次,因為各家對可選欄位的容錯度不同,這是我們在香港零售專案中最常抓到的問題。
然後在 Thank You 頁以 Checkout UI Extension 呈現:
1// extensions/fps-qr/src/Checkout.jsx2import { reactExtension, BlockStack, Text, Image, useOrder } from "@shopify/ui-extensions-react/checkout";34export default reactExtension("purchase.thank-you.block.render", () => <FpsBlock />);56function FpsBlock() {7 const order = useOrder();8 const src = `https://pay.example.com/fps-qr.png?order=${order?.id}`;9 return (10 <BlockStack border="base" padding="base" spacing="tight">11 <Text emphasis="bold">請用銀行 App 掃描以下轉數快 QR 完成付款</Text>12 <Image source={src} accessibilityDescription="FPS QR code" />13 <Text appearance="subdued">備註欄請保留訂單編號 {order?.name},QR 有效期 15 分鐘。</Text>14 </BlockStack>15 );16}
1shopify app deploy2# 於 Admin → Settings → Checkout → Customize 將 App block 加到 Thank you 頁
Ready to Transform Your Ecommerce Operations?
Branch8 specializes in ecommerce platform implementation and AI-powered automation solutions. Contact us today to discuss your ecommerce automation strategy.
對帳自動化:把人手核對交給流程與模型
路線 C 最大的營運成本不是開發,是每天核對銀行入賬。可行的自動化分三層:
- 結構化來源優先:若銀行或 PSP 提供入賬 webhook 或 API(部分香港銀行的商戶方案有提供),直接用金額 + 備註訂單號比對,命中即以 Admin API 標記為已付款。
- 半結構化補位:只能拿到 CSV/PDF 對帳單時,先用解析器抽出「日期、金額、付款人、備註」四欄,再交由規則引擎配對。
- 模型處理例外:客戶把訂單號打錯、金額拆單、備註寫成中文姓名等長尾情況,用 LLM 對「未配對入賬」與「未付款訂單」兩張清單做候選配對,輸出信心分數與理由,低於門檻的才進人工佇列。這是 2026 年比較務實的 AI 用法——不是取代對帳,而是把人手集中在真正模糊的 5%。
標記付款的 Admin API 呼叫:
1mutation MarkPaid($id: ID!) {2 orderMarkAsPaid(input: { id: $id }) {3 order { id displayFinancialStatus }4 userErrors { field message }5 }6}
配合 Shopify Flow 可以再加一層守門:Order marked as paid → 檢查 tag fps-auto-matched → 若無則通知財務 Slack 頻道複核。任何自動配對都應保留可稽核的比對紀錄,這在跨境集團的內部審計上是硬要求。
排錯清單:最常見的八個卡點
- 結帳頁看不到 FPS 選項:檢查 PSP 後台是否已開通 FPS、商店貨幣是否為 HKD、以及顧客的結帳國家/地區是否為香港。
supported_countries限制為 HK 時,海外地址不會顯示。 - 訂單長期停在 Pending:多數是 webhook 沒送到或被 401 擋下。用 PSP 後台的 webhook log 對照你的存取日誌,確認簽章驗證用的是 raw body 而非已 parse 的 JSON。
- HMAC 驗證永遠失敗:Express 預設會消耗 body,需保留原始字串:
app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf.toString("utf8"); } }))。 paymentSessionResolve回userErrors: session not found:session 已過期或你用了 payment sessionid而非gid。Payments Apps API 需要gid://shopify/PaymentSession/...。- QR 掃出來金額為零或需手動輸入:Tag 01 誤用
11(靜態)而非12(動態),或 Tag 54 格式不對(要1880.00,不是1880或HKD1880)。 - 部分銀行 App 掃描報「無效 QR」:CRC 計算錯誤,或 Tag 59 商戶名稱超過 25 字元。逐一移除可選欄位(60、62)二分定位。
- 重複付款:客戶重複掃碼或重試結帳。務必用 Shopify session id 作 PSP 的 idempotency key,並在對帳層對「同一訂單多筆入賬」設警示。
- 退款失敗:FPS 退款是反向轉賬,需要收款人資料;若原付款人帳戶資訊未被 PSP 保留,只能人手處理。政策頁與客服腳本要事先寫清楚。
Ready to Transform Your Ecommerce Operations?
Branch8 specializes in ecommerce platform implementation and AI-powered automation solutions. Contact us today to discuss your ecommerce automation strategy.
跨境延伸:一套架構、多個即時支付軌道
如果你的品牌在香港之外也有市場,不要為 FPS 寫一次性程式。把上面的路線 B 抽象成三個介面即可覆蓋大部分亞太即時支付:createLocalCharge()、onChargeWebhook()、buildMerchantQr()。
- 新加坡:PayNow,由 ABS/BCS 營運,同樣採 EMVCo QR(SGQR),代理為手機號或 UEN。
- 馬來西亞:DuitNow QR,由 PayNet 統一,跨錢包互通程度高。
- 台灣:以 TWQR 與各家電子支付為主,另有 ATM 虛擬帳號的深厚習慣,適用路線 C 的變體。
- 澳洲/紐西蘭:New Payments Platform 的 PayID 更多用於帳戶轉賬而非零售結帳,卡類與 BNPL 仍主導。
對美國、英國與歐洲品牌而言,這正是把亞太營運中心設在香港或新加坡的實質好處:一個團隊在同一時區內掌握多個本地支付軌道的規格差異與合規要求,而不是靠總部在深夜對著時差解釋為什麼結帳轉換率在香港特別低。PPRO 等本地支付研究機構長期指出,亞太多數市場的非卡類本地支付方式佔比高於卡類——不接本地方式,等於把結帳頁的一大部分流量交給對手。
在一個香港多品牌餐飲集團的 Shopify Plus 專案中,我們就把 FPS QR 與門店 QR 收銀共用同一個 payload 產生服務,前端分別由 Checkout UI Extension 與 POS 呈現,避免兩套邏輯各自漂移。
上線前的最終檢查
- 三家以上香港銀行 App 掃碼實測通過(金額、收款人、備註)。
- Webhook 重放測試:同一 payload 送兩次,訂單不會被標記兩次付款。
- QR 過期後的使用者路徑:明確的「重新產生 QR」按鈕與訂單狀態頁文案。
- 退款流程有 SOP 與客服話術,並在條款頁載明處理時間。
- 對帳報表可回溯至 Shopify 訂單號與 PSP transaction reference。
- 中文(繁體)文案與英文並存,FPS 一律標示為「轉數快 FPS」以提高辨識度。
把 FPS 當成「軌道」而非「按鈕」來設計,你的結帳層就同時具備了接下一個亞太市場的能力。
需要在 Shopify Plus 上接通轉數快、PayNow 或多市場本地支付,並把對帳自動化?Branch8 的香港與新加坡團隊長期處理跨境電商結帳與支付整合,歡迎聯絡我們討論你的架構與時程。
Ready to Transform Your Ecommerce Operations?
Branch8 specializes in ecommerce platform implementation and AI-powered automation solutions. Contact us today to discuss your ecommerce automation strategy.
Sources
FAQ
目前 Shopify Payments 在香港以信用卡與部分電子錢包為主,並未提供原生 FPS 選項。要在結帳頁出現轉數快,必須透過支援 FPS 的香港持牌支付服務供應商 App、以 Payments Apps API 自建付款方式,或用離線付款配合動態 HKQR。
About the Author
Matt Li
Co-Founder & CEO, Branch8 & Second Talent
Matt Li is Co-Founder and CEO of Branch8, a Y Combinator-backed (S15) Adobe Solution Partner and e-commerce consultancy headquartered in Hong Kong, and Co-Founder of Second Talent, a global tech hiring platform ranked #1 in Global Hiring on G2. With 12 years of experience in e-commerce strategy, platform implementation, and digital operations, he has led delivery of Adobe Commerce Cloud projects for enterprise clients including Chow Sang Sang, HomePlus (HKBN), Maxim's, Hong Kong International Airport, Hotai/Toyota, and Evisu. Prior to founding Branch8, Matt served as Vice President of Mid-Market Enterprises at HSBC. He serves as Vice Chairman of the Hong Kong E-Commerce Business Association (HKEBA). A self-taught software engineer, Matt graduated from the University of Toronto with a Bachelor of Commerce in Finance and Economics.