不能直接用老版 paypal-php-sdk(v1)对接 orders v2 api,因其仍调用已废弃的 /v1/payments/payment 路径、错误拼接 auth header、忽略 paypal-request-id 等必需头,导致 401/403/422 错误;必须改用 paypal/rest-api-sdk-php v1.14+ 或手写 http 请求调用 /v2/checkout/orders。

不能直接用老版 PayPal-PHP-SDK(v1)对接当前 PayPal Orders v2 API,会卡在 401 或 403 错误;必须用 paypal/rest-api-sdk-php v1.14+ 或手写 HTTP 请求调用 /v2/checkout/orders 接口。
为什么 PayPal\Rest\ApiContext 在 ThinkPHP6 中大概率失败
老 SDK 依赖已废弃的 /v1/payments/payment 路径,而 PayPal 自 2022 年起强制迁移至 Orders v2。即使你填对了 client_id 和 client_secret,ApiContext 内部仍会发错 endpoint、用错 auth header 格式(比如拼错 Basic base64)、或忽略 required headers(如 PayPal-Request-Id)。错误现象通常是:
-
HTTP 401 Unauthorized:token 没传、过期、或用了旧版 token endpoint(/v1/oauth2/token正确,但老 SDK 可能漏掉content-type) -
HTTP 403 Forbidden:App 权限没开 “payments:capture” 或 “orders:read”,沙箱 App 页面里要手动勾选 -
HTTP 422 Unprocessable Entity:purchase_units缺少amount.currency_code,或value是字符串而非数字(如"19.99"而非19.99)
ThinkPHP6 手动调用 Orders v2 的最小可行代码
不引入 SDK,用 think\facade\Http 直接发请求,可控性高、报错明确。关键点:
- token 必须走
https://api-m.sandbox.paypal.com/v1/oauth2/token(沙箱)或https://api-m.paypal.com/v1/oauth2/token(正式) - 创建订单用
POST /v2/checkout/orders,header 必须含Authorization: Bearer xxx和Content-Type: application/json -
purchase_units[0].payments.captures字段只在捕获后才存在,创建时不要写 - 前端 JS SDK 的
onApprove回调里拿到的orderID,需和服务端capture请求的 ID 完全一致(大小写敏感)
示例(简化版):
// 创建订单
$token = $this->getAccessToken(); // 先取 token,建议缓存到 Redis,有效期 9 小时
$orderData = [
'intent' => 'CAPTURE',
'purchase_units' => [[
'amount' => [
'currency_code' => 'USD',
'value' => 29.99 // 注意:是 float,不是 string
],
'description' => 'Premium subscription'
]]
];
$response = Http::withToken($token)
->withHeaders(['Content-Type' => 'application/json'])
->post('https://api-m.sandbox.paypal.com/v2/checkout/orders', $orderData);
$result = json_decode($response->getBody(), true);
if (!empty($result['id'])) {
return ['approve_link' => $result['links'][1]['href']]; // 取 rel=approve 的链接
}
capture 请求必须带 PayPal-Request-Id 防重放
PayPal 要求每次 capture 请求都带唯一 PayPal-Request-Id(UUID v4),否则返回 400 Bad Request,错误信息是 "The request ID is invalid."。这不是可选字段。
- 不能复用前端传来的
orderID当作PayPal-Request-Id - ThinkPHP6 可用
Str::uuid()生成,或bin2hex(random_bytes(16)) - 捕获地址是
/v2/checkout/orders/{order_id}/capture,不是/v2/payments/capture - 响应里的
status是COMPLETED,不是success—— 别按微信/支付宝逻辑硬套
捕获示例:
$requestId = Str::uuid();
$response = Http::withToken($token)
->withHeaders([
'Content-Type' => 'application/json',
'PayPal-Request-Id' => $requestId
])
->post("https://api-m.sandbox.paypal.com/v2/checkout/orders/{$orderId}/capture", []);
沙箱账号和 App 配置最容易被忽略的三个地方
很多问题其实卡在环境配置,而不是代码:
- 沙箱商家账号的邮箱必须是
@business.example.com后缀(开发者平台自动生成的),不能用自己的 Gmail;否则支付页显示 “Account not found” - App 的 “Live” 和 “Sandbox” credentials 绝对不能混用:沙箱用
api-m.sandbox.paypal.com+ 沙箱 client_id,切到线上必须全部替换 - Webhook 不是必须项,但如果你开了,必须在沙箱 App 页面里显式 “Add Webhook”,并验证 URL —— 否则 IPN 类通知不会触发
真正麻烦的从来不是签名或加密,而是 PayPal 把「环境隔离」做到了像素级:一个域名、一个邮箱、一个 endpoint 写错,整个流程就静默失败,连日志都不报具体原因。
大量免费API接口:立即使用
涵盖生活服务API、金融科技API、企业工商API、等相关的API接口服务。免费API接口可安全、合规地连接上下游,为数据API应用能力赋能!











