---
title: "串接 Paypal 筆記"
description: "完整的 PayPal REST API v2 串接筆記，涵蓋前端 React SDK 整合、後端 C# 建立訂單、授權與請款流程，以及 Braintree 進階付款功能說明。"
canonical_url: "https://blog.markkulab.net/post/integrate-paypal-checkout"
author: "Mark Ku"
author_url: "https://blog.markkulab.net/author/mark-ku"
site: "Mark Ku's Blog"
date_published: "2024-03-02 01:01:35 +0800"
category: "Payment"
tags: ["paypal", "payment", "checkout", "react", "csharp", "integration", "braintree"]
language: "zh-TW"
license: "CC BY 4.0"
license_url: "https://creativecommons.org/licenses/by/4.0/"
attribution: "轉載或引用請註明作者並附上原文連結"
---

# 串接 Paypal 筆記

## 時空背景
據[統計](https://www.rapyd.net/blog/ecommerce-and-payment-trends-germany/) Paypal 在德國擁有22%以上的市佔，並在歐盟也相的普及，但過去因為電商詐欺太嚴重，因此User 決定把Paypal 先關閉，藉由此次重新打造德國網站，把Paypal 重新串接，並搭配 [Riskified](https://www.riskified.com/) 保險服務，來分攤掉網路電商的詐欺的運營風險。
![Riskified 處理電商訂單的核准、拒絕與保證流程](https://blog.markkulab.net/content/markku/posts/integrate-paypal-checkout/images/riskified.png)

## Paypal 串接
Paypal 的Api 看起來昇級了，官方也建議用新版的 Rest api v2 版本串接。

### 相關文件
* [前端串接文件](https://developer.paypal.com/docs/checkout/standard/integrate/#link-integratefrontend)
* [取得Api Access Token](https://developer.paypal.com/api/rest/authentication/)
* [Api Request相關](https://developer.paypal.com/api/rest/requests/)
* [建立訂單及授權請款相關的api](https://developer.paypal.com/docs/api/orders/v2/#orders_create)

### Api Endpoint 
```
Sandbox. https://api-m.sandbox.paypal.com
Live. https://api-m.paypal.com
```
### 取得Api 金鑰
[連結](https://developer.paypal.com/dashboard/applications/sandbox)
![Get api key](https://blog.markkulab.net/content/markku/posts/integrate-paypal-checkout/images/get-api-key.png)

## 新版相當的方便，可以透過Event logs [偵錯](https://developer.paypal.com/dashboard/dashboard/sandbox)

![debug-tools](https://blog.markkulab.net/content/markku/posts/integrate-paypal-checkout/images/debug-tools.png)

## 前端的錯誤也可以透過瀏覽器偵錯
![console-debug.png](https://blog.markkulab.net/content/markku/posts/integrate-paypal-checkout/images/console-debug.png)
## 進階付款功能
進階模式多了，Venmo(社交支付)、Debit or Credit Card、Paypal Pay Later (先買後付)，但需要申請 PayPal 的 Braintree 付款閘道。

P.S. PayPal 的 Braintree 是一個全面的支付解決方案，類似於Payment gateway，主要是幫助商家接受、處理和分配支付，它解決了多個與接受線上支付相關的問題，提供了一個安全、靈活且易於集成的平台。

## 整合的流程圖
![Integration diagram](https://blog.markkulab.net/content/markku/posts/integrate-paypal-checkout/images/integration-diagram.png)

## 開始串接
首先，可以先參考官方的 [Integration builder](https://developer.paypal.com/integration-builder/) 的範例，其實寫的不錯，基本上照著界接就能做完了
![Integration-builder-1](https://blog.markkulab.net/content/markku/posts/integrate-paypal-checkout/images/integration-builder-1.png)
![Integration-builder-2](https://blog.markkulab.net/content/markku/posts/integrate-paypal-checkout/images/integration-builder-2.png)
![Integration-builder-3](https://blog.markkulab.net/content/markku/posts/integrate-paypal-checkout/images/integration-builder-3.png)


### 前端範例 ( Next js app route )
#### 安裝SDK
```
npm install @paypal/react-paypal-js --save
```

### 撰寫前端串接的程式

```
'use client'; // next js client component
import { PaymentOptions } from '@/const/payment/payment-option';
import { useCaptureMutation, useGetConfigQuery, useProcessMutation } from '@/redux/api/test-payment-apiSlice';
import { IGernalPaymentParams } from '@/typing/cart';
import { PayPalButtons, PayPalScriptProvider } from '@paypal/react-paypal-js';
import { v4 as uuidv4 } from 'uuid';

export default function TestPaypal() {
    const { data: paymentInitOption, isLoading: isPaymentInitLoading } = useGetConfigQuery(); // rtk query 取得 payal sdk 前端初始化的參數

    const [Process] = useProcessMutation(); // rtk query 用來呼叫建立建立授權訂單的api

    const [Capture] = useCaptureMutation(); // rtk query 用來呼叫提取信用請款的api

    const gernalPaymentParams: IGernalPaymentParams = {
        paymentTypeCode: PaymentOptions[PaymentOptions.Paypal],
        orderNo: uuidv4(),        
    } as IGernalPaymentParams;

    const createOrder = (): Promise<string> => {
        return Process(gernalPaymentParams)
            .unwrap()
            .then((res: ApiResponse<IGeneralPaymentResult>) => {
                if (res.isSuccess) {
                    const orderId = res.data.paymentReturnValue;

                    return orderId;
                }
                return '';
            });
    };

    const onApprove = (data: any) => {
        return Capture(gernalPaymentParams)
            .unwrap()
            .then((res: ApiResponse<IGeneralPaymentResult>) => {
                if (res.isSuccess) {
                    debugger;
                    paypalOrderId.current = res.data.paymentReturnValue;
                    alert('Payment success');
                }
            });
    };

    return (
        <>
            {!isPaymentInitLoading && (
                <PayPalScriptProvider options={paymentInitOption}>
                    <PayPalButtons
                        createOrder={createOrder}
                        onApprove={onApprove}
                        style={{ layout: 'horizontal', color: 'white', tagline: true }}
                    />
                </PayPalScriptProvider>
            )}
        </>
    );
}
```

## 後端範例 (C#)
### 定義後端環境設定 - Appsetting.json
```
  "Payment": {
    "PaymentOptions": [
      {
        "PaymentName": "Paypal",
        "IsSandbox": true,
        "ClientId": "Your paypal clientId",
        "Secret": "Your paypal secret",             
        "EndPoint": "https://api-m.sandbox.paypal.com" // sandbox		 
      }]
  }
```

### 從後端環境變數組出來SDK 初始化需要的參數給前端
```
     public async Task<Result<Dictionary<string, string>>> GetConfig()
     {
        var dic = new Dictionary<string, string>();
        dic.Add("client-id", GeneralPaymentConfig.ClientId);
        dic.Add("currency", HardCodeKey.BasedCurrency);
		// dic.Add("disable-funding", "credit,card"); // 關閉信用卡及借記卡

        var result = new Result<Dictionary<string, string>> { IsSuccess = true, Message = "", Data = dic };
        return result;
     }
```
### PAYPAL API 請求授權 - Authorization paypal 支援，兩種token，這邊採用 <client_id:secret> 當成登入的token 

依據官方文件，所述，我們可以呼叫 api 取得 Access-Token 或是使用 client_id:secret 當成 token 去呼叫paypal API

```
// To make REST API calls, include the bearer token in this header with the Bearer authentication scheme. The value is Bearer <Access-Token> or Basic <client_id:secret> 

public async Task<string> Authorization()
   {
      return Convert.ToBase64String(Encoding.ASCII.GetBytes($"{GeneralPaymentConfig.ClientId}:{GeneralPaymentConfig.Secret}")); ;
}
```
### 接著，我們實作 /api/orders 建立訂單及授權

```
public async Task<Result<GeneralPaymentResult>> ProcessAsync(GernalPaymentParameter paymentParameter)
{
   var result = new Result<GeneralPaymentResult>
   {
      IsSuccess = false,
      Message = "",
      Data = new GeneralPaymentResult
      {
         PaymentReturnType = PaymentReturnType.OrderId,
      }
   };

   try
   {
      var headers = new Dictionary<string, string>
    {
      { "Authorization", $"Basic  {Authorization()}" },
      { "PayPal-Request-Id",  HttpContext.Current.TraceIdentifier},
    };

      var paymentCapture = new PaymentCapture
      {
         Intent = "CAPTURE",
         PurchaseUnits = new List<PurchaseUnit>
   {
   new PurchaseUnit
   {
      ReferenceId = paymentParameter.OrderNo,
      Amount = new Model.ViewModels.Payment.Amount
      {
          CurrencyCode = "EUR",
          Value = "1.00"
      },
      Shipping = new PaypalShipping
      {
          Address =  new PaypalAddress
          {
              AddressLine1 = "2211 N First Street",
              AddressLine2 = "Building 17",
              AdminArea2 = "San Jose",
              AdminArea1 = "CA",
              PostalCode = "95131",
              CountryCode = "US"
          }
      }
      }
      },
         PaymentSource = new PaymentSource
         {
            PayPal = new PayPal
            {
               ExperienceContext = new ExperienceContext
               {
                  PaymentMethodPreference = "IMMEDIATE_PAYMENT_REQUIRED",
                  BrandName = "EXAMPLE INC",
                  Locale = "en-US",
                  LandingPage = "LOGIN",
                  ShippingPreference = "SET_PROVIDED_ADDRESS",
                  UserAction = "PAY_NOW",
                  ReturnUrl = "https://example.com/returnUrl",
                  CancelUrl = "https://example.com/cancelUrl"
               }
            }
         }
      };

      // Use Newtonsoft.Json to serialize the object to JSON (for demonstration)
      string body = JsonConvert.SerializeObject(paymentCapture, Formatting.Indented);

      var orderRsult = await HttpHelper.PostAsync<PaypalOrderResponse>(GeneralPaymentConfig.EndPoint + "/v2/checkout/orders", body, headers);
      result.Data.PaymentReturnValue = orderRsult.Id;
      result.Success();
   }
   catch (Exception ex)
   {
      result.Fail(ex.Message);
      NLogUtil.WriteSEQLog($"[Paypal][ProcessAsync]Error:{ex.Message},StackTrace:{ex.StackTrace}", NLog.LogLevel.Error);
   }

   return result;
}
```

### 最後，再來實作 /api/orders 請款 ( capture )
```
public virtual async Task<Result<GeneralPaymentResult>> CaptureAsync(GernalPaymentParameter paymentParameter)
{
   // Sandbox have some issue.cannot use capture
   var result = new Result<GeneralPaymentResult>
   {
      IsSuccess = false,
      Message = "Capture fail!",
      Data = new GeneralPaymentResult
      {
      }
   };

   try
   {
      var url = GeneralPaymentConfig.EndPoint + $"/v2/checkout/orders/{paymentParameter.PaymentGatewayOrderId}/capture";
      var requestBody = "";

      var headers = new Dictionary<string, string>
    {
      { "Authorization", $"Basic  {Authorization()}" },
      { "PayPal-Request-Id",  HttpContext.Current.TraceIdentifier},
    };

      var response = await HttpHelper.PostAsync<PaypalOrderResponse>(url, requestBody, headers);

      if (response.Status == "COMPLETED")
      {
         result.IsSuccess = true;
         result.Message = "Capture success";
      }

   }
   catch (Exception ex)
   {
      result.Fail(ex.Message);
      NLogUtil.WriteSEQLog($"[Paypal][CaptureAsync]Error:{ex.Message},StackTrace:{ex.StackTrace}", NLog.LogLevel.Error);
   }

   return result;
}
```
## Paypal 不登入也能使用信用卡
![paypal credit card](https://blog.markkulab.net/content/markku/posts/integrate-paypal-checkout/images/paypal-credit-card.png)

## 補充 - Upon Invoice
有趣的東西，Pyapl 的第三方支付，先出貨在付款，paypal 承諾商家一定收的到錢。

### 參考文章
* [美國電子商務實務筆記 - 信用卡授權(Authorize)及請款(Capture)](https://blog.markkulab.net/usa-ecommerce-note-credit-card-authorize-and-capture/)
* [Paypal express Checkout 整合指南](https://www.paypalobjects.com/webstatic/lvm/tw/zh/using-paypal/integration-guide.pdf)
* [Integrate Apple Pay with JS SDK for direct merchants](https://developer.paypal.com/docs/checkout/apm/apple-pay/#link-howitworks)
* [Integrate Google Pay with JS SDK for direct merchants](https://developer.paypal.com/docs/checkout/apm/google-pay/)

---

## 關於本文與作者

本文出自 [Mark Ku's Blog](https://blog.markkulab.net/post/integrate-paypal-checkout)

授權條款： [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) — 轉載或引用請註明作者並附上原文連結

### 關於作者

**[Mark Ku](https://blog.markkulab.net/author/mark-ku)** — Software Solution Provider

- 10+ 年資深軟體工程師，現為 AI 應用 Builder
- 專注大型平台架構設計，從北美電商到AI SaaS訂閱收費系統
- 結合 AI Agent 與自動化，打造高效可演進的產品技術基礎

### 作者開發的免費工具

以下工具皆可免費使用：

- [免費 PDF 簽名工具](https://blog.markkulab.net/tools/pdf-sign): 線上 PDF 簽名工具，瀏覽器內完成手繪、打字、上傳簽名，可拖曳放置、縮放、下載。所有處理都在你的裝置完成，檔案不會上傳。
- [VS Code Refactory](https://blog.markkulab.net/tools/refactory): Refactory 是一款 VS Code 重構擴充套件：34 個重構動作、37 條 code smell 檢查、Code Health 儀表板、18 種語言、534 支測試。懂你的專案慣例：介面放哪、DI 註冊寫在哪、'use client' 該不該加；還會用 git 修改頻率 × 複雜度排出「該先修哪個檔案」，並一鍵把壞味道交給你自己電腦上的 Claude Code 修。免費使用，原始碼不離開你的機器。
- [DB-Kit 資料庫管理工具](https://blog.markkulab.net/tools/db-kit): DB-Kit 是一個用 Tauri + Rust + React 打造的輕量跨平台資料庫管理工具，用單一一致的介面同時管理 MySQL、MariaDB、PostgreSQL、SQL Server、Oracle、SQLite、MongoDB、Redis、Kafka、Elasticsearch 與 RabbitMQ 十一種資料來源：連線密碼以 OS keychain 加密、SSH Tunnel、完整 CRUD、視覺化查詢建構器、多結果集同時顯示、跨連線資料傳輸與比對同步、Excel / CSV 匯入匯出、執行計畫視覺化、ER 圖、排程備份、SQL 壓力測試（p50～p99 延遲百分位）、15 條規則的 SQL 審查、Kafka 訊息瀏覽與監控告警；繁中 / 英文雙語介面，內建 AI 助手（自然語言生成 SQL、AI 審查與調校建議）與命令列工具 dbk。免費開源（MIT），提供 Windows / macOS / Linux 安裝檔。
- [VS Code Super Mermaid](https://blog.markkulab.net/tools/super-mermaid): Super Mermaid 是一款 VS Code 擴充套件：開箱即用的漂亮 Mermaid 圖表，自動上色、即時預覽、滑鼠平移縮放、PNG / SVG 高解析匯出，內建 21 種範本與多種主題。免費開源（MIT）。
- [React Super Mermaid](https://blog.markkulab.net/tools/react-super-mermaid): react-super-mermaid 是一個開源 React 元件庫：一行 <MermaidViewer> 即可渲染漂亮的 Mermaid 圖表，內建 colorful / sketch 主題、平移縮放、圖內搜尋、SVG / PNG 高解析匯出。輕量、SSR 安全、完整 TypeScript 型別。免費開源（MIT）。
- [Jira / Confluence Super Mermaid](https://blog.markkulab.net/tools/jira-super-mermaid): Atlassian Forge app：在 Jira issue 與 Confluence 內文直接寫 Mermaid 語法，畫流程圖、時序圖、狀態機與甘特圖。11 種圖表、SVG / PNG 匯出、明暗主題、完整中日韓文字支援。取得 Runs on Atlassian 資格：圖表存在你自己的站台，app 不呼叫任何第三方服務。免費，即將上架 Atlassian Marketplace。
- [Mermaid 線上預覽](https://blog.markkulab.net/tools/mermaid-preview): 在瀏覽器裡寫 Mermaid、即時看圖，整張圖表壓進網址就能分享。免註冊、不上傳伺服器，相容 mermaid.live 的分享連結。
- [React Intl Phone Number](https://blog.markkulab.net/tools/react-intl-phone-number): react-intl-phone-number 是一個開源 React 元件：framework-agnostic、不依賴 antd，提供 E.164 進出、可搜尋國旗 / 國碼下拉、可配置驗證等級（strict / mobile-strict / loose）、可主題化 CSS 與 i18n，電話邏輯由 google-libphonenumber 驅動。輕量、完整 TypeScript 型別。免費開源（MIT）。
- [Uptime Kuma Cluster](https://blog.markkulab.net/tools/uptime-kuma-cluster): 把單機版 Uptime Kuma 改造成高可用叢集：OpenResty + Lua 智慧負載平衡、MariaDB 共享狀態、健康檢查與自動 Failover，附叢集管理 REST API，一行 Docker Compose 啟動。免費開源（MIT）。
- [特教專案](https://blog.markkulab.net/education): 為特殊教育學生製作的學習教材

### 每日 Podcast

- [科技新鮮事](https://blog.markkulab.net/category/tech-news): 每日精選 AI 與科技趨勢，透過語音摘要快速掌握最新技術動態，涵蓋 AI 應用、軟體架構、DevOps 與工程實戰。 — RSS: https://blog.markkulab.net/feed.xml
- [AI股市蝦聊](https://blog.markkulab.net/category/ai-stock-chat): 每個交易日用 AI 分析台股盤勢，以雙人對話聊當天的盤中觀察與隔日預測。 — RSS: https://blog.markkulab.net/ai-stock-chat/feed.xml

### 電子報

[訂閱電子報](https://blog.markkulab.net/subscribe) — 第一時間收到新文章通知，無垃圾信、隨時可取消訂閱。
