# Apple Pay

> Apple Pay 在收银台、Web SDK 与 API 直连下的接入差异、域名验证与账号配置、API 直连的会话流程与 token 提交方式、钱包订阅，以及常见问题。

Apple Pay 让客户用 Wallet 中已添加的卡，通过 Face ID 或 Touch ID 完成支付。收银台与 Web SDK 由 Onerway 渲染 Apple Pay 按钮并处理 token，商户只需完成域名报备；API 直连由商户自建按钮、驱动 `ApplePaySession`，并把 Apple 返回的加密 payment token 提交给 Onerway。

## 接入方式选择

| 接入方式 | 按钮与会话 | token 处理 | 商户需要做的 |
| --- | --- | --- | --- |
| [收银台](/zh/payments/online-payments/checkout) | 收银台渲染并驱动 | 不经过商户系统 | 下单传 `productType=ALL`，或传 `lpmsInfo.lpmsType=ApplePay` 锁定 Apple Pay |
| [Web SDK](/zh/payments/online-payments/sdk) | SDK 在商户页面内渲染并驱动，外观见[配置钱包按钮](/zh/payments/online-payments/sdk#%E9%85%8D%E7%BD%AE%E9%92%B1%E5%8C%85%E6%8C%89%E9%92%AE) | 不经过商户系统 | 完成域名验证；结果通过 `payment_result` 接收，见 [SDK 自有按钮](/zh/payments/online-payments/sdk#apple-paygoogle-pay-%E7%AD%89-sdk-%E8%87%AA%E6%9C%89%E6%8C%89%E9%92%AE) |
| [API 直连](/zh/payments/online-payments/api) | 商户自建按钮与 `ApplePaySession` | Onerway 代解密或商户自解密 | 完成域名验证；本页「API 直连接入」全部步骤 |

收银台是 Onerway 托管页面，域名已由 Onerway 向 Apple 报备，商户无需再做域名验证。Web SDK 与 API 直连在商户自己的域名下展示 Apple Pay 按钮，Apple 要求该域名先通过验证，见「接入准备」。

三种接入方式都要求网站全站 HTTPS、TLS 1.2 及以上，且 Onerway 商户账号已开通 Apple Pay。沙盒测试前提与测试卡见 [Apple Pay 沙盒测试](/zh/payments/get-started/testing#apple-pay-%E6%B2%99%E7%9B%92%E6%B5%8B%E8%AF%95)。

### 浏览器与设备支持

Apple Pay on the Web 不再只限于 Safari：

- Safari，以及 iOS / iPadOS 上的第三方浏览器（同为 WebKit 内核）：在设备上直接完成支付。
- Mac、Windows 与其他设备上的兼容第三方浏览器：页面显示二维码，客户用 iOS 18 / iPadOS 18 及以上的 iPhone 或 iPad 扫码完成支付。API 直连商户需要使用 Apple Pay JS SDK 1.2.0 及以上提供的 `<apple-pay-button>` 元素，CSS 方式渲染的按钮不支持非 Safari 浏览器。
- 中国大陆：仅支持 iPhone / iPad 上的 Safari，第三方浏览器不可用。

收银台与 Web SDK 由 Onerway 处理以上差异；API 直连商户按本页示例使用官方 SDK 与按钮元素即可获得同样的覆盖范围。以 [Apple 官方说明](https://support.apple.com/en-us/120364)为准。

## 接入准备

Apple 要求每个展示 Apple Pay 按钮的商户域名都完成域名验证，验证依附于某个 Apple Developer 账号的 Merchant ID。按谁持有 Merchant ID，接入准备分三个层级：

| 层级 | 域名验证与商户验证 | token 解密 | 适用 |
| --- | --- | --- | --- |
| **默认** | 商户自有 Apple Developer 账号：自建 Merchant ID、自行完成域名验证，用自持 Merchant Identity 证书向 Apple 请求 merchant session | Onerway 代解密：支付处理证书由 Onerway 生成 CSR、商户上传到自己的 Merchant ID | 绝大多数商户 |
| **商户自解密** | 同默认 | 商户自解密：自持支付处理证书，解密结果放入 `cardInfo`，须满足 PCI DSS | 已具备 PCI DSS 与证书管理能力 |
| **Onerway 代理** | 域名注册在 Onerway 的 Apple Developer 账号下，商户调用 [Apple Pay 商户验证](/zh/payments/api-reference/endpoints/validate-apple-pay-merchant)换取 merchant session | Onerway 代解密 | 没有或不想维护 Apple Developer 账号的商户 |

Web SDK 只涉及域名验证，按默认层级或 Onerway 代理层级完成即可。不要把商户自有 Merchant ID 与 Onerway 代理验证混用。

商户自有账号与 Onerway 代理两种域名验证方式，验证文件都部署在网站的 `.well-known` 路径下（以 Apple Developer 后台或 Onerway 提供的路径为准），该地址不可位于代理或重定向之后，且须允许 Apple 验证服务器访问：

```text
https://your-store.com/.well-known/apple-developer-merchantid-domain-association
```

### 商户自有账号

1. **创建 Merchant ID**：登录 [Apple Developer](https://developer.apple.com/account/)，在 Certificates, Identifiers & Profiles 的 [Merchant IDs](https://developer.apple.com/account/resources/identifiers/list/merchant) 中新建，填写 Description 与 Identifier（如 `merchant.com.yourcompany.appname`）。已有可用 Merchant ID 可跳过。
2. **配置支付处理证书**：默认层级先把 Merchant ID 提供给 Onerway 技术支持，取得 Onerway 生成的 CSR。在该 Merchant ID 的 Apple Pay Payment Processing Certificate 中创建证书，「Will payments be processed exclusively in China mainland?」选 No，上传该 CSR。证书创建成功后下载 `.cer` 文件回传 Onerway：私钥由 Onerway 持有，收到证书后才能解密 Apple Pay token。商户自解密层级则由商户自行生成 CSR 并保管私钥。
3. **配置 Merchant Identity 证书**（仅 API 直连需要）：在 Apple Pay Merchant Identity Certificate 中按 [Apple 的 CSR 指南](https://developer.apple.com/help/account/certificates/create-a-certificate-signing-request)创建并下载，安全存放在商户验证服务端，用于[向 Apple 请求 merchant session](https://developer.apple.com/documentation/ApplePayontheWeb/requesting-an-apple-pay-payment-session)。
4. **域名验证**：在 Merchant Domains 中添加域名，下载验证文件并部署到上述路径，确认 HTTPS 可直接访问后在 Apple Developer 中点击 Verify。域名验证随网站 SSL 证书一起到期：Apple 会在证书到期前 30、15、7 天回查，提前续期 SSL 证书即可自动保持验证；若证书过期后才更换，需重新验证域名。支付处理证书与 Merchant Identity 证书各 25 个月到期，Merchant ID 不过期。

### Onerway 代理

1. 向 Onerway 技术支持提供需要展示 Apple Pay 按钮的全部域名，含子域名，沙盒与生产域名都要提供，格式 `https://your-store.com`。
2. Onerway 完成注册后返回验证文件，商户部署到上述路径并确认可访问：

```bash
curl -I https://your-store.com/.well-known/apple-developer-merchantid-domain-association
```

1. Onerway 通过 Apple 完成域名验证后通知商户。支付处理证书与 Merchant Identity 证书由 Onerway 集中持有并续期，商户无需管理。

无论哪个层级，证书都应存放在受控环境、最小权限访问，并监控 SSL 证书与 Apple 证书的到期时间。

## API 直连接入

商户自建按钮并驱动 `ApplePaySession`，Apple 返回的加密 payment token 按解密模式提交给 Onerway。两种模式二选一，`tokenInfo` 与 `cardInfo` 不可同时提交。

**Onerway 代解密**（推荐，默认与 Onerway 代理层级）：把 `event.payment.token` 整体序列化为字符串，原样放入 `tokenInfo.tokenId`，不要拆解其中的 `paymentData`、`paymentMethod` 与 `transactionIdentifier`。

```json
"tokenInfo": "{\"provider\":\"ApplePay\",\"tokenId\":\"<event.payment.token 序列化后的字符串>\"}"
```

**商户自解密**（自解密层级，须满足 PCI DSS）：不传 `tokenInfo`，把解密结果按下表放入 [`cardInfo`](/zh/payments/api-reference/endpoints/direct-create-transaction#request-cardInfo)。

| 解密后的 Apple 字段 | `cardInfo` 字段 |
| --- | --- |
| `applicationPrimaryAccountNumber` | `cardNumber` |
| `applicationExpirationDate`（`YYMMDD`） | `month` 取第 3、4 位；`year` 取前两位并补全为四位，如 `30` 对应 `2030` |
| `onlinePaymentCryptogram` | `cryptogram` |
| `eciIndicator` | `eci` |
| `paymentDataType` | `wallet.applePay.paymentDataType`，取 `3DSecure` 或 `EMV`，解密出哪种传哪种 |

完整报文见创建直连交易的「Onerway 代解密 Apple Pay 支付」与「商户自解密 Apple Pay 支付」示例。

前端先加载 Apple Pay JS SDK：推荐自动更新的 `1.latest` 地址；跨浏览器支持要求 1.2.0 及以上；固定版本时改用 `v1.x.y` 路径并加 `integrity`，`1.latest` 不支持 `integrity`。SDK 会在非 Safari 浏览器中注入 `ApplePaySession`。

<steps level="3">

### 能力检测并展示按钮

加载 Apple Pay JS SDK 后，仅当 `ApplePaySession` 存在且 `canMakePayments()` 为 true 时展示按钮。需要判断客户是否已有可用卡时用 `applePayCapabilities()`，它在非 Safari 浏览器只会返回 `paymentCredentialStatusUnknown`，此时仍应展示按钮；`canMakePaymentsWithActiveCard()` 已废弃。按钮使用 SDK 提供的 `<apple-pay-button>` 元素，样式属性见 [Apple Pay 按钮文档](https://developer.apple.com/documentation/applepayontheweb/applepaybutton)，品牌规范见 [Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/apple-pay)。

```html
<script src="https://applepay.cdn-apple.com/jsapi/1.latest/apple-pay-sdk.js"></script>
<apple-pay-button buttonstyle="black" type="buy" locale="zh-CN"></apple-pay-button>
```

### 获取 Onerway 配置

服务端调用[查询可用支付方式](/zh/payments/api-reference/endpoints/list-available-payment-methods)，取 `paymentMethod=ApplePay` 的记录，把 `countryCode` 与 `subCardTypes` 返回前端，分别作为支付请求的 `countryCode` 与 `supportedNetworks`；两者已按 Apple 要求的格式返回，原样透传。`applePayCapabilities()` 需要商户标识：自有账号层级用商户自己的 Merchant ID，Onerway 代理层级用查询可用支付方式返回的 `merchantId`。

### 创建支付会话

用 `supportsVersion()` 从高到低探测浏览器支持的最高版本，再创建 `ApplePaySession` 并调用 `begin()`。支付请求至少包含 `countryCode`、`currencyCode`、`supportedNetworks`、`merchantCapabilities`（含 `supports3DS`）与 `total`（`label`、`amount`、`type: 'final'`），`amount` 为字符串。

### 校验 validationURL 并换取 merchant session

在 `onvalidatemerchant` 事件中取 `validationURL`，校验其主机是 Apple 的验证网关域名（`apple-pay-gateway.apple.com`，中国大陆为 `cn-apple-pay-gateway.apple.com`，沙盒为对应的 `-cert` 域名），其他 URL 一律调用 `abort()`，再交给服务端。服务端转发前必须再次校验主机，不要只依赖前端校验，也不要硬编码验证地址。商户自有账号层级由服务端用 Merchant Identity 证书向 Apple 发起 mTLS 请求，请求体含 `merchantIdentifier`、`displayName`（稳定的店铺名，不要本地化或拼入订单号）、`initiative` 取 `web`、`initiativeContext` 取完整域名；Onerway 代理层级调用 [Apple Pay 商户验证](/zh/payments/api-reference/endpoints/validate-apple-pay-merchant)，并把响应中的 `data` 解析为对象。

### 完成商户验证

拿到 merchant session 后立即调用 `completeMerchantValidation()`。merchant session 只能使用一次，创建后 5 分钟过期，只在服务端即时请求，不要在客户端直接请求 Apple。

### 支付授权

在 `onpaymentauthorized` 事件中取 `event.payment.token` 交给服务端，服务端调用[创建直连交易](/zh/payments/api-reference/endpoints/direct-create-transaction)（`productType=CARD`、`subProductType=DIRECT`、`txnType=SALE`），按解密模式提交 `tokenInfo` 或 `cardInfo`。前端根据服务端结果调用 `completePayment()` 传入成功或失败状态，且必须恰好调用一次，否则 Apple Pay 面板会一直停留。

### 确认最终结果

同步响应 `status=S` 为成功、`P` 为处理中，最终状态以[支付结果通知](/zh/payments/api-reference/webhooks/payment-result)为准，钱包交易的通知带 `walletTypeName=ApplePay`。验签、应答与幂等处理见 [Webhook 通知](/zh/payments/get-started/webhooks)。

</steps>

## 前端示例

服务端三个内部接口由商户实现，分别对应上面的获取配置、商户验证与支付授权三步。

```html
<apple-pay-button id="applePayButton" buttonstyle="black" type="buy" locale="zh-CN" style="display:none;"></apple-pay-button>
<script src="https://applepay.cdn-apple.com/jsapi/1.latest/apple-pay-sdk.js"></script>
<script>
  const button = document.getElementById('applePayButton')

  // 服务端调用查询可用支付方式，返回 { countryCode, subCardTypes }
  const fetchConfig = () => fetch('/api/apple-pay/config').then(r => r.json())
  // 服务端按接入层级换取 merchant session：自有账号用 Merchant Identity 证书直连 Apple，Onerway 代理调用 Apple Pay 商户验证接口
  const validateMerchant = (validationURL, website) =>
    fetch('/api/apple-pay/validate-merchant', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ validationURL, website })
    }).then(r => r.json())
  // 服务端调用创建直连交易（tokenInfo.provider=ApplePay），返回 { success: boolean }
  const processPayment = (paymentToken) =>
    fetch('/api/apple-pay/process-payment', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ paymentToken })
    }).then(r => r.json())

  // 只接受 Apple 的验证网关主机，覆盖中国大陆与沙盒域名
  const APPLE_PAY_GATEWAY = /^(cn-)?apple-pay-gateway(-[a-z0-9-]+)?\.apple\.com$/

  const highestSupportedVersion = () => {
    for (let version = 14; version >= 3; version -= 1) {
      if (ApplePaySession.supportsVersion(version)) return version
    }
    return 3
  }

  function startSession(config) {
    const paymentRequest = {
      countryCode: config.countryCode,
      currencyCode: 'USD',
      supportedNetworks: config.subCardTypes,
      merchantCapabilities: ['supports3DS'],
      total: { label: 'Example Store', amount: '99.99', type: 'final' }
    }
    const session = new ApplePaySession(highestSupportedVersion(), paymentRequest)

    session.onvalidatemerchant = async (event) => {
      // 服务端必须再校验一次，不要只依赖这里
      if (!APPLE_PAY_GATEWAY.test(new URL(event.validationURL).hostname)) {
        session.abort()
        return
      }
      try {
        const merchantSession = await validateMerchant(event.validationURL, window.location.hostname)
        session.completeMerchantValidation(merchantSession)
      } catch {
        session.abort()
      }
    }

    session.onpaymentauthorized = async (event) => {
      try {
        const result = await processPayment(event.payment.token)
        session.completePayment(result.success ? ApplePaySession.STATUS_SUCCESS : ApplePaySession.STATUS_FAILURE)
      } catch {
        session.completePayment(ApplePaySession.STATUS_FAILURE)
      }
    }

    session.oncancel = () => {
      // 用户关闭了 Apple Pay 面板，恢复页面交互
    }

    session.begin()
  }

  async function init() {
    if (!window.ApplePaySession || !ApplePaySession.canMakePayments()) return
    const config = await fetchConfig()
    button.style.display = 'block'
    button.addEventListener('click', () => startSession(config))
  }
  init()
</script>
```

Apple 官方[交互式演示](https://applepaydemo.apple.com/)可体验完整流程。

## 钱包订阅

Apple Pay 可用于订阅：初始订阅时传 `subProductType=SUBSCRIBE` 与 `subscription`，`selfExecute=1`（Onerway 托管）与 `selfExecute=2`（商户自行发起每期扣款）均支持。收银台路径以 `lpmsInfo.lpmsType=ApplePay` 锁定 Apple Pay；API 直连路径以 `tokenInfo` 提交钱包加密 token 发起初始订阅。初始订阅成功后，[订阅扣款通知](/zh/payments/api-reference/webhooks/subscription-payment)返回 `contractId` 与订阅 `tokenId`，后续续费与升降级与卡订阅相同，见[订阅支付](/zh/payments/online-payments/scenarios/subscriptions)。

与卡订阅的差异：钱包加密 token 是一次性的，不能保存复用，订阅期间的扣款凭证只有订阅 token；钱包订阅不产生保存支付方式结果通知，也不返回卡 token。

## 常见问题

| 现象 | 常见原因 | 处理 |
| --- | --- | --- |
| 按钮不显示 | 未加载 Apple Pay JS SDK 或使用了 CSS 按钮、非 HTTPS、设备或浏览器不支持、所在国家或地区不支持 Apple Pay | 使用 SDK 的 `<apple-pay-button>`；只在 `canMakePayments()` 为 true 时展示；非 Safari 浏览器下 `applePayCapabilities()` 返回未知状态属正常；提供其他支付方式 |
| 点击后面板一闪而过 | 商户验证失败：`validationURL` 未通过校验、merchant session 超过 5 分钟或被复用、域名验证文件缺失或不可访问、网站 SSL 证书过期导致域名验证失效、`initiativeContext` 与验证域名不一致 | 确认 `completeMerchantValidation` 被调用；用 `curl -I` 确认验证文件返回 200；核对域名一致；证书问题联系 Onerway 重新生成；沙盒与生产的商户标识不要混用 |
| 确认支付后页面提示未完成 | 未调用或多次调用 `completePayment()`；`total.amount` 不是字符串或精度错误；支付处理证书状态异常 | 自检前端逻辑；仍失败时联系 Onerway 技术支持 |
| 沙盒测试卡被拒 | 未使用 sandbox 测试账户、设备地区与测试卡卡组织不匹配、卡未添加到 Wallet | 按 [Apple Pay 沙盒测试](/zh/payments/get-started/testing#apple-pay-%E6%B2%99%E7%9B%92%E6%B5%8B%E8%AF%95)完成前提 |

联系 Onerway 技术支持时请提供商户号与接入层级、发生时间与环境（沙盒或生产）、设备、操作系统与浏览器版本、完整的控制台与网络日志、复现步骤。

## 上线前检查

- 沙盒与生产域名已完成验证，验证文件可访问，SSL 证书续期已纳入监控。
- merchant session 只在服务端请求，`validationURL` 在前端与服务端都已校验。
- `completePayment()` 在成功与失败时都恰好调用一次。
- 不记录、不存储 Apple Pay payment token。
- Webhook 验签与幂等已实现。
