在线支付系列(四):PayPal——一位海外买家的安全支付之旅
在线支付系列(四):PayPal——一位海外买家的安全支付之旅
Emma 为什么不敢在陌生网站输入卡号?
伦敦的设计师 Emma 在 Instagram 上刷到了一个小众手工包品牌。她点进链接,一个设计简洁的独立站,标价 $89 的手工皮包看起来很不错。
她心动了,但手指悬在「Buy Now」按钮上方犹豫了三秒。
这个网站她从没听说过。页面底部没有什么知名品牌的 logo,About Us 写得语焉不详。如果她输入了信用卡号,万一是诈骗网站怎么办?被盗刷了怎么办?
然后她看到了支付选项里的 PayPal 按钮。
"PayPal 我信得过。就算有问题,PayPal 会帮我追回来。"
她点击 PayPal 按钮,跳转到 PayPal 登录页面,输入邮箱和密码,确认支付。全程没有在这个陌生网站上输入任何银行卡信息。
这就是 PayPal 存在的核心价值:在商户和买家之间建立信任层。对 Emma 来说,PayPal 是"安全感";对商户来说,PayPal 意味着能转化那些"不敢在陌生网站输卡号"的用户。
这篇文章,就从 Emma 的这笔 $89 说起。
一、PayPal 的商业逻辑
在讲技术之前,先理解 PayPal 在支付生态中的位置。
1.1 PayPal 不是信用卡,也不是银行
PayPal 的角色有点像支付宝——它是一个数字钱包。用户在 PayPal 绑定信用卡或银行账户,购物时只需要登录 PayPal 确认,不需要向商户暴露任何银行信息。
关键数据:
- 4.31 亿活跃账户,主要在北美和欧洲
- 覆盖 200+ 个国家/地区
- 支持 25+ 种货币
- 注册门槛极低——有个邮箱就能开始收款
1.2 PayPal 的费用结构
但安全感和低门槛是有代价的。PayPal 是四大支付渠道中费率最高的:
Emma 这笔 $89 的交易:
- 手续费:$89 × 3.49% + $0.49 = $3.60
- 商户到账:$85.40
如果 Emma 后来申请了退款呢?
- PayPal 不退原交易手续费——商户白亏 $3.59
- 这是和 Stripe 最大的区别。Stripe 退款时会退手续费,PayPal 不会。
如果 Emma 发起了争议呢?
- PayPal 收取 $15 标准争议费
- 高争议量账户(争议率过高):升至 $30/笔
- 如果交易未通过 PayPal 账户而直接走信用卡网络,退单费为 $20/笔
一句话总结 PayPal 的成本特点:费率最高、退款最贵(不退手续费),但门槛最低、买家信任度最高。适合需要转化"谨慎型"海外用户的场景。
二、PayPal 的技术架构
PayPal 的支付流程和 Stripe 不同。Stripe 的核心交互在前端(Stripe.js),而 PayPal 是一种"前端触发 + 后端完成"的模式。
2.1 整体流程
让我们跟着 Emma 的 $89 走一遍:
Emma 的浏览器 商户后端 PayPal 服务器
│ │ │
│ ① 点击 PayPal 按钮 │ │
│──────────────────────→│ │
│ │ ② 创建 Order │
│ │─────────────────────→│
│ │ ③ 返回 Order ID │
│ │←─────────────────────│
│ ④ 返回 Order ID │ │
│←──────────────────────│ │
│ │ │
│ ⑤ PayPal SDK 弹出登录窗口 │
│─────────────────────────────────────────────→│
│ ⑥ Emma 登录 PayPal 确认支付 │
│─────────────────────────────────────────────→│
│ │ │
│ ⑦ PayPal 返回 "APPROVED" │
│←─────────────────────────────────────────────│
│ │ │
│ ⑧ 告诉后端去 Capture │ │
│──────────────────────→│ │
│ │ ⑨ Capture Order │
│ │─────────────────────→│
│ │ ⑩ 返回 "COMPLETED" │
│ │←─────────────────────│
│ │ ⑪ 更新订单状态 │
注意第 ⑨ 步:和 Stripe 不同,PayPal 的扣款需要后端主动调用 Capture。用户在 PayPal 弹窗中确认后,钱还没有真正扣——你需要再调一次 API 来"捕获"这笔款。
这其实就是信用卡的"授权-捕获"分离思想——PayPal 也借鉴了这个设计。
2.2 认证方式:OAuth 2.0
PayPal API 使用 OAuth 2.0 认证。每次调用 API 前,你需要用 Client ID 和 Secret 换取一个 Access Token。
这和支付宝/微信的签名机制完全不同。支付宝/微信是每个请求都签名,PayPal 是先拿 token 再带着 token 请求。
三、动手:PayPal 对接全流程
3.1 接入准备
第一步:访问 developer.paypal.com 登录
▼
第二步:创建 App(My Apps & Credentials → Create App)
▼
第三步:获取 Client ID 和 Secret
▼
第四步:创建沙盒测试账号(一个买家账号 + 一个商家账号)
▼
第五步:配置 Webhook Endpoint
| 密钥 | 用途 | 安全级别 |
|---|---|---|
| Client ID | 标识应用,前端 SDK + 后端 OAuth | 可公开 |
| Secret | 后端 API 认证 | 🔴 绝密 |
| Webhook ID | 验证 Webhook 来源 | 🔴 绝密 |
3.2 OAuth 2.0:先拿 Token
每次调用 PayPal API 之前,你都需要先获取 Access Token。Token 有效期通常是几小时,过期后重新获取即可。
import base64, requests, os
PAYPAL_CLIENT_ID = os.getenv("PAYPAL_CLIENT_ID")
PAYPAL_SECRET = os.getenv("PAYPAL_SECRET")
PAYPAL_BASE = "https://api-m.sandbox.paypal.com" # 沙盒环境
def get_access_token():
"""用 Client ID + Secret 换取 Access Token"""
auth = base64.b64encode(
f"{PAYPAL_CLIENT_ID}:{PAYPAL_SECRET}".encode()
).decode()
resp = requests.post(
f"{PAYPAL_BASE}/v1/oauth2/token",
headers={
"Authorization": f"Basic {auth}",
"Content-Type": "application/x-www-form-urlencoded",
},
data={"grant_type": "client_credentials"}
)
return resp.json()["access_token"]
和支付宝/微信的区别:支付宝和微信是"每个请求都用私钥签名",PayPal 是"先用凭证换 token,然后带着 token 请求"。两种方式都安全,但 OAuth 的方式更符合 REST API 的风格。
3.3 前端:Smart Payment Buttons
PayPal 提供了一套官方的前端 SDK——Smart Payment Buttons。它会根据用户所在地区自动显示最合适的 PayPal 按钮样式。
<!-- 引入 PayPal SDK -->
<script src="https://www.paypal.com/sdk/js?client-id=YOUR_CLIENT_ID¤cy=USD"></script>
<!-- PayPal 按钮容器 -->
<div id="paypal-button-container"></div>
paypal.Buttons({
// ① 用户点击按钮后,调用你的后端创建订单
createOrder: async () => {
const res = await fetch('/api/pay/paypal/create-order', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
amount: '89.00',
currency: 'USD',
description: 'Handmade Leather Bag',
order_id: 'ORD_20260404002',
})
});
const data = await res.json();
return data.paypal_order_id; // 返回给 PayPal SDK
},
// ② Emma 在 PayPal 弹窗中确认后,调用后端去 Capture
onApprove: async (data) => {
const res = await fetch('/api/pay/paypal/capture-order', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ paypal_order_id: data.orderID })
});
const result = await res.json();
if (result.status === 'COMPLETED') {
alert('支付成功!🎉');
}
},
// ③ 出错了
onError: (err) => {
console.error('PayPal Error:', err);
alert('支付出现问题,请重试');
},
// ④ Emma 在 PayPal 弹窗中取消了
onCancel: () => {
alert('支付已取消');
},
}).render('#paypal-button-container');
3.4 后端:创建订单 + 捕获
from fastapi import FastAPI, Request, HTTPException
import requests as http_requests
import os
app = FastAPI()
PAYPAL_BASE = os.getenv("PAYPAL_BASE_URL",
"https://api-m.sandbox.paypal.com")
@app.post("/api/pay/paypal/create-order")
async def create_order(request: Request):
"""Emma 点击 PayPal 按钮后,后端创建一个 PayPal 订单"""
body = await request.json()
token = get_access_token()
order_data = {
"intent": "CAPTURE", # 意图:直接捕获(不是先授权再捕获)
"purchase_units": [{
"reference_id": body["order_id"], # 你的订单号
"description": body.get("description", ""),
"amount": {
"currency_code": body.get("currency", "USD"),
"value": body["amount"], # 金额——注意是元(字符串),不是分!
}
}],
"application_context": {
"brand_name": "Your Store",
"user_action": "PAY_NOW",
}
}
resp = http_requests.post(
f"{PAYPAL_BASE}/v2/checkout/orders",
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
json=order_data
)
result = resp.json()
if resp.status_code == 201:
# 保存你的订单号和 PayPal 订单号的映射关系
await save_order_mapping(body["order_id"], result["id"])
return {
"paypal_order_id": result["id"],
"status": result["status"], # 此时应该是 "CREATED"
}
raise HTTPException(400, detail=result)
@app.post("/api/pay/paypal/capture-order")
async def capture_order(request: Request):
"""Emma 在 PayPal 弹窗中确认后,后端来捕获这笔款"""
body = await request.json()
token = get_access_token()
resp = http_requests.post(
f"{PAYPAL_BASE}/v2/checkout/orders/"
f"{body['paypal_order_id']}/capture",
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
)
result = resp.json()
if result["status"] == "COMPLETED":
# 提取支付详情
capture = result["purchase_units"][0]["payments"]["captures"][0]
await update_order_status(
order_id=result["purchase_units"][0]["reference_id"],
status="PAID",
transaction_id=capture["id"],
amount=capture["amount"]["value"],
currency=capture["amount"]["currency_code"],
)
return {"status": result["status"]}
注意金额单位:PayPal 和支付宝一样用元(字符串),不是分。
"89.00"而不是8900。这一点和 Stripe/微信相反。
3.5 Webhook 验签
PayPal 的 Webhook 验签方式比较独特——它不是你自己算签名比对,而是调用 PayPal 的 API 来验证。
@app.post("/api/pay/paypal/webhook")
async def paypal_webhook(request: Request):
"""PayPal Webhook 处理"""
body = await request.json()
headers = dict(request.headers)
# 1. 验签——调用 PayPal 官方 API 验证
token = get_access_token()
verify_payload = {
"auth_algo": headers.get("paypal-auth-algo"),
"cert_url": headers.get("paypal-cert-url"),
"transmission_id": headers.get("paypal-transmission-id"),
"transmission_sig": headers.get("paypal-transmission-sig"),
"transmission_time": headers.get("paypal-transmission-time"),
"webhook_id": os.getenv("PAYPAL_WEBHOOK_ID"),
"webhook_event": body,
}
verify_resp = http_requests.post(
f"{PAYPAL_BASE}/v1/notifications/verify-webhook-signature",
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
},
json=verify_payload
)
if verify_resp.json().get("verification_status") != "SUCCESS":
raise HTTPException(400, "Invalid webhook signature")
# 2. 业务处理
event_type = body["event_type"]
resource = body["resource"]
if event_type == "PAYMENT.CAPTURE.COMPLETED":
# 幂等检查 + 更新订单状态
await handle_capture_completed(resource)
elif event_type == "PAYMENT.CAPTURE.REFUNDED":
await handle_refund(resource)
elif event_type == "CUSTOMER.DISPUTE.CREATED":
# 收到争议通知!需要及时处理
await handle_dispute_created(resource)
return {"status": "ok"}
四大渠道的验签方式对比:
| 渠道 | 验签方式 | 复杂度 |
|---|---|---|
| 支付宝 | 用支付宝公钥证书验证 RSA 签名 | ★★★☆ |
| 微信支付 | 用平台证书验 RSA 签名 + AES-GCM 解密 | ★★★★ |
| Stripe | 用 Webhook Secret 算 HMAC-SHA256 比对 | ★★☆☆ |
| PayPal | 调用 PayPal API 验证(把签名信息转发给 PayPal) | ★★★☆ |
四、PayPal 的争议保护:Emma 的安全网
Emma 之所以敢在陌生网站用 PayPal 付款,核心原因是 PayPal 的买家保护计划(Buyer Protection)。
4.1 买家保护怎么运作?
如果 Emma 收到的手工包和描述不符(比如图片是真皮,收到的是人造革),或者根本没收到包裹,她可以在 PayPal 发起争议。
Emma 发起争议 → PayPal 冻结 $89 → 商户有 20 天响应
│
├── 商户和 Emma 协商解决 → 结案
│
└── 协商失败 → PayPal 介入裁决
│
├── 判商户赢 → 解冻 $89,退还给商户
└── 判 Emma 赢 → $89 退给 Emma,商户承担损失 + $15 争议费
4.2 对商户的影响
作为商户,PayPal 的争议保护是一把双刃剑:
好处:它让更多"谨慎型"用户愿意下单。没有 PayPal,Emma 可能直接关掉页面——你连成交的机会都没有。
风险:PayPal 的裁决总体上倾向于保护买家。如果你没有充分的发货证明和交易记录,很可能败诉。
建议:
- 每笔订单都保存发货证明和物流追踪号
- 使用 PayPal 的"卖家保护"功能——寄送到 PayPal 交易详情中的地址
- 收到争议后尽快响应(20 天内),不要拖
- 争议率控制在 1% 以下,否则 PayPal 会采取限制措施
五、PayPal 的特殊之处
对接过支付宝、微信、Stripe 之后,PayPal 有一些独特的特点值得单独说明。
5.1 退款政策:手续费不退
这是 PayPal 和其他三个渠道最大的区别。当你退款时:
| 渠道 | 退款后原交易手续费 |
|---|---|
| 支付宝 | ✅ 退还 |
| 微信支付 | ✅ 退还 |
| Stripe | ✅ 退还 |
| PayPal | ❌ 不退还 |
也就是说,如果 Emma 退了那笔 $89 的订单,商户不仅拿不到钱,还要亏 $3.59 的手续费。如果你的退货率很高,这笔成本会非常可观。
5.2 PayPal 支持部分退款
好消息是 PayPal 支持部分退款,最多 10 次。如果 Emma 对包的颜色不满意但不想退货,你可以退 $20 表示歉意,而不是全额退款。
5.3 汇率加价
如果你的结算币种和用户的支付币种不同,PayPal 会自动做货币转换——但它会在中间汇率上加 3~4% 的差价。这是跨境使用 PayPal 时一个经常被忽略的隐性成本。
5.4 退款时限
PayPal 的退款时限是 180 天(6 个月)。相比之下支付宝是 3 个月,微信是 1 年,Stripe 没有限制。
六、沙盒测试
PayPal 的沙盒环境和生产环境完全隔离。你需要在 developer.paypal.com 创建测试账号。
6.1 测试账号
PayPal 沙盒会为你自动创建两个测试账号:
- Business 账号:模拟商户,用来收款
- Personal 账号:模拟买家(就像 Emma),用来付款
6.2 测试卡号
如果你需要测试"用信用卡支付而不是 PayPal 余额"的场景:
| 卡组织 | 卡号 | 用途 |
|---|---|---|
| Visa | 4032039317984658 | 正常支付 |
| Mastercard | 5425233430109903 | 正常支付 |
| Visa | 4687380000000002 | 触发拒绝 |
6.3 沙盒 vs 生产
切换到生产环境只需要做两件事:
- 把 API 地址从
api-m.sandbox.paypal.com改成api-m.paypal.com - 把沙盒的 Client ID / Secret 换成生产的
七、什么时候该选 PayPal?
经过这篇文章的学习,让我们总结一下 PayPal 的适用场景。
强烈推荐接入 PayPal 的场景:
- 你的目标用户在北美或欧洲
- 你是一个新品牌/小品牌,用户对你的网站缺乏信任
- 你卖的是数字商品或服务(PayPal 的即时交付验证机制成熟)
- 你需要最快速度上线收款(注册即用,不需要企业审核)
可以不接 PayPal 的场景:
- 你只做中国市场(用户几乎不用 PayPal)
- 你的退款率很高(手续费不退的成本太大)
- 你已经有了知名品牌背书,用户信任度足够高
- 你的客单价很低(固定费用 $0.49 的比例太高)
本系列文章导读
| 篇目 | 标题 | 你将学到 |
|---|---|---|
| 第 1 篇 | 概览篇——一笔订单触发的支付之旅 | 四方模型、支付生命周期、市场格局、成本分析、选型决策 |
| 第 2 篇 | 支付宝 & 微信支付——一杯咖啡的扫码之旅 | 签名机制、回调处理、AES-GCM 解密、完整对接代码 |
| 第 3 篇 | Stripe & 信用卡——一件跨境商品的卡支付之旅 | Payment Intents、3D Secure、PCI DSS、前后端代码 |
| 第 4 篇 | PayPal——一位海外买家的安全支付之旅(本文) | OAuth 认证、Smart Buttons、争议保护、Webhook |
| 第 5 篇 | 统一支付网关——当四条河流汇入一片海 | 三层架构、策略模式、幂等设计、对账补偿 |
参考来源
- PayPal Developer Documentation
- PayPal Orders V2 API
- PayPal Webhooks
- PayPal Buyer Protection
- PayPal Standard Transaction Fees
- PayPal Seller Protection
欢迎关注公众号 coft,获取更多深度技术文章。下一篇也是最后一篇——当你同时接了支付宝、微信、Stripe、PayPal 之后,四套签名机制、四种回调格式、四个退款接口……维护成本开始指数增长。怎么办?答案是搭一个统一支付网关。

浙公网安备 33010602011771号