Skip to content

Apple Pay (ứng dụng native) ​

Chấp nhận thanh toán từ một ứng dụng iOS native bằng Apple Pay. Người dùng ủy quyền bằng Face ID hoặc Touch ID, Apple trả cho app của bạn một payment token đã mã hóa, app của bạn chuyển tiếp token đó đến backend của riêng bạn, và backend của bạn xác nhận PaymentIntent với Bahn.

App của bạn không bao giờ nhìn thấy PAN. Apple trả về một thẻ đã được device-tokenize, được mã hóa bằng một chứng chỉ payment processing mà Bahn giữ private key tương ứng — Bahn giải mã nó phía server trong quá trình confirm.

WARNING

Trang này mô tả bề mặt ứng dụng native (PassKit). Apple Pay trên Web (Safari, Apple Pay JS) là một tích hợp riêng biệt — xem Apple Pay, hoặc Phụ lục bên dưới để biết mối liên hệ giữa hai luồng.

Điều kiện tiên quyết ​

Trước khi viết code, hãy hoàn tất thiết lập phía Apple trong tài khoản Apple Developer của bạn:

Yêu cầuỞ đâuGhi chú
Merchant IDApple Developer → Identifiers → Merchant IDsví dụ merchant.vn.cake.yourapp. Được dùng làm PKPaymentRequest.merchantIdentifier.
Payment Processing CertificateApple Developer → Merchant ID của bạnĐược tạo từ một CSR do Bahn cung cấp. Bahn giữ private key tương ứng và là bên duy nhất có thể giải mã token.
Apple Pay entitlementXcode → Signing & CapabilitiesThêm capability Apple Pay và chọn Merchant ID của bạn.

INFO

Gửi Merchant ID của bạn cho Bahn trong quá trình onboarding. Bahn sẽ trả về CSR để bạn upload lên Apple. Không tự tạo CSR — nếu Bahn không giữ private key tương ứng, Bahn sẽ không thể giải mã payment token và mọi lần confirm sẽ thất bại.

Luồng tổng quan ​

sequenceDiagram
    autonumber
    participant User as User
    participant PassKit as PassKit<br/>(iOS / Secure Element)
    participant App as Merchant App<br/>(iOS)
    participant MerchantAPI as Merchant API<br/>(Backend)
    participant API as Bahn API

    Note over User, API: Phase 1: Create PaymentIntent
    User ->> App: Tap "Buy with Apple Pay"
    App ->> MerchantAPI: Request checkout
    MerchantAPI ->> API: CreatePaymentIntent(amount, currency=704)
    API -->> MerchantAPI: {payment_intent, client_secret}
    MerchantAPI -->> App: {order_id}

    Note over User, API: Phase 2: Authorise in PassKit
    App ->> PassKit: Present PKPaymentAuthorizationController<br/>(PKPaymentRequest)
    PassKit -->> User: Show sheet (card, amount, merchant)
    User ->> PassKit: Approve with Face ID / Touch ID
    PassKit -->> App: PKPayment.token.paymentData<br/>(encrypted JSON)

    Note over User, API: Phase 3: Confirm
    App ->> MerchantAPI: POST /checkout/apple-pay {order_id, token}
    MerchantAPI ->> API: ConfirmPaymentIntent(id,<br/>payment_method_data.apple_pay.token)
    API ->> API: Decrypt token, submit to network
    API -->> MerchantAPI: payment_intent.status = "succeeded" | "processing"
    MerchantAPI -->> App: {status}
    App ->> PassKit: completion(.success)
    PassKit -->> User: Dismiss sheet with checkmark

    Note over MerchantAPI, API: Phase 4: Payment Result
    par Notify
        API ->> MerchantAPI: Webhook: payment_intent.status.succeeded
        MerchantAPI ->> MerchantAPI: Consume webhook event
    end

    Note over User, API: Phase 5: Complete
    App ->> MerchantAPI: GET Order status
    MerchantAPI -->> App: Order
    App -->> User: Show order status

Cách hoạt động ​

Giai đoạn 1 — Tạo PaymentIntent ​

  1. Người dùng nhấn nút Apple Pay — App của bạn nhận thao tác nhấn và yêu cầu checkout từ backend của bạn.
  2. App yêu cầu checkout — App gửi giỏ hàng hoặc mã đơn hàng đến Merchant API của bạn.
  3. Backend gọi CreatePaymentIntent — Merchant API của bạn gọi CreatePaymentIntent với amount và currency (mã số 704 cho VND). Bahn trả về payment_intent và client_secret tương ứng.
  4. Backend chỉ trả về mã đơn hàng — Khác với luồng Web SDK, app không cần client_secret: nó không bao giờ giao tiếp trực tiếp với Bahn. Trả về order_id riêng của bạn và giữ cả payment_intent.id lẫn API key ở phía backend.

INFO

Trong luồng native, lệnh gọi confirm được thực hiện server-to-server, vì vậy không có bất kỳ thông tin xác thực nào của Bahn được đóng gói trong app binary.

Giai đoạn 2 — Ủy quyền trong PassKit ​

  1. App hiển thị payment sheet — Xây dựng một PKPaymentRequest và hiển thị nó bằng PKPaymentAuthorizationController.

    swift
    let request = PKPaymentRequest()
    request.merchantIdentifier = "merchant.vn.cake.yourapp"
    request.countryCode = "VN"
    request.currencyCode = "VND"
    request.supportedNetworks = [.visa, .masterCard]
    request.merchantCapabilities = .capability3DS
    request.paymentSummaryItems = [
      PKPaymentSummaryItem(label: "Order #1234", amount: NSDecimalNumber(value: 8000))
    ]
    
    let controller = PKPaymentAuthorizationController(paymentRequest: request)
    controller.delegate = self
    controller.present()

    merchantCapabilities phải bao gồm .capability3DS — điều này bắt buộc cho mọi mạng thẻ và không liên quan đến việc có hiển thị thử thách 3DS cho người dùng hay không.

  2. PassKit hiển thị sheet — iOS hiển thị bộ chọn thẻ, số tiền, và tên merchant từ request.

  3. Người dùng chấp thuận — Người dùng xác thực bằng Face ID, Touch ID, hoặc mã passcode.

  4. PassKit trả về token — Delegate của bạn nhận một PKPayment. Trường bạn cần là payment.token.paymentData: một blob Data chứa nội dung là payment token đã mã hóa dưới dạng JSON ({"data":…,"signature":…,"header":{…},"version":"EC_v1"}).

    swift
    func paymentAuthorizationController(
      _ controller: PKPaymentAuthorizationController,
      didAuthorizePayment payment: PKPayment,
      handler: @escaping (PKPaymentAuthorizationResult) -> Void
    ) {
      guard let token = String(data: payment.token.paymentData, encoding: .utf8) else { return }
      // gửi `token` đến backend của bạn, sau đó gọi handler(...)
    }

WARNING

Chuyển tiếp paymentData dưới dạng chuỗi JSON UTF-8 mà PassKit tạo ra. Không bọc thêm base64, không serialize lại, và không sắp xếp lại các key — Bahn xác minh chữ ký của Apple dựa trên chính xác các byte gốc trước khi giải mã, và bất kỳ thay đổi nào cũng sẽ làm mất hiệu lực.

Giai đoạn 3 — Confirm ​

  1. App gửi token đến backend của bạn — Gửi { order_id, token } đến endpoint của riêng bạn qua TLS. Token chỉ dùng được một lần và có thời hạn ngắn; hãy chuyển tiếp nó ngay lập tức.
  2. Backend xác nhận PaymentIntent — Merchant API của bạn gọi endpoint confirm của Bahn với token trong payment_method_data.apple_pay.token. Xem Trách nhiệm của merchant API bên dưới để biết payload chính xác.
  3. Bahn giải mã và trả về kết quả — Bahn xác minh chữ ký, giải mã token bằng private key của chứng chỉ payment processing, gửi giao dịch đến mạng thẻ, và trả về PaymentIntent đã cập nhật. Các trạng thái thường gặp là succeeded (đã capture) hoặc processing (đang xử lý).
  4. App hoàn tất sheet — Trả kết quả về cho app của bạn và gọi handler(PKPaymentAuthorizationResult(status: .success, errors: nil)). PassKit hiển thị dấu tick và đóng lại.

WARNING

Bạn chỉ có khoảng 30 giây kể từ didAuthorizePayment để gọi completion handler trước khi iOS timeout sheet. Nếu lệnh gọi confirm của bạn có thể mất lâu hơn, hãy hoàn tất sheet với .success ngay khi Bahn trả về processing và đối chiếu kết quả cuối cùng qua webhook.

Giai đoạn 4 — Kết quả thanh toán ​

Bahn thông báo cho backend của bạn độc lập với app:

  • Thông báo qua webhook — Bahn gửi payment_intent.status.succeeded (hoặc payment_intent.payment_failed) đến webhook endpoint đã đăng ký của bạn. Webhook là nguồn sự thật chính thức (authoritative) cho kết quả thanh toán cuối cùng. Người dùng có thể tắt app giữa lúc chấp thuận và lúc response của bạn đến.

Giai đoạn 5 — Hoàn tất ​

  1. App lấy thông tin đơn hàng — App của bạn gọi Merchant API để lấy trạng thái đơn hàng mới nhất.
  2. Backend trả về đơn hàng — Đã được cập nhật từ webhook event.
  3. Người dùng thấy kết quả — App hiển thị màn hình thành công hoặc thất bại.

Trách nhiệm của merchant API ​

1. Tạo một PaymentIntent ​

Giống hệt Thanh toán một lần — không có gì đặc thù cho Apple ở giai đoạn này.

http
POST /api/payment_intents

Request

json
{
  "amount": 8000,
  "currency": "704",
  "description": "Order #1234",
  "metadata": { "order_id": "1234" }
}

Response

json
{
  "payment_intent": {
    "id": "pi_01HZ3X9YQ7M8KH8M5JYV9X4F2B",
    "client_secret": "pi_secret_ilAA93E16NV3uic90rq--tz-7FOTeupgN5DH6GYwji0",
    "amount": 8000,
    "currency": "704",
    "status": "requires_payment_method"
  }
}

2. Xác nhận với Apple Pay token ​

http
POST /api/payment_intents/{id}/confirm

Request

json
{
  "payment_method_data": {
    "apple_pay": {
      "token": "{\"data\":\"WTP8BxZNUs…\",\"signature\":\"MIAGCSqGSIb3…\",\"header\":{\"publicKeyHash\":\"7c4ZFn…\",\"ephemeralPublicKey\":\"MFkwEwYH…\",\"transactionId\":\"31323334…\"},\"version\":\"EC_v1\"}"
    }
  }
}
TrườngBắt buộcMô tả
payment_method_data.apple_pay.tokenCóPKPaymentToken đã mã hóa của Apple Pay dưới dạng chuỗi JSON — nội dung UTF-8 của PKPaymentToken.paymentData, không sửa đổi. Loại trừ lẫn nhau với card, napas_qr, card_present, và google_pay.
payment_method_optionsKhôngCác tùy chọn riêng cho từng lần xác nhận. Bỏ qua trừ khi Bahn đã thông báo khác cho tài khoản của bạn.
capture_methodKhôngĐặt thành manual để ủy quyền ngay và capture sau. Xem Manual capture.

Bahn giải mã token phía server. Không có biến thể merchant-tự-giải-mã ở đây — luôn gửi token đã mã hóa thô.

Xác thực lệnh gọi này như bất kỳ request server-to-server nào khác: Bn-Api-Key cùng với một body đã ký JWS. Xem Thực hiện request đầu tiên.

bash
curl -X POST 'https://bn.cakedigibank.tech/api/payment_intents/pi_01HZ3X9YQ7M8KH8M5JYV9X4F2B/confirm' \
  -H 'Content-Type: application/jose' \
  -H 'Bn-Api-Key: api_key-ab318df034' \
  -H 'Bn-Jose-Method: jws' \
  -H 'Bn-Request-Id: 7f3e9c1a-4b2d-4e8f-a1c0-123456789abc' \
  -H 'Bn-Idempotency-Key: order-1234-applepay' \
  -d '<compact JWS of the JSON body above>'

Response

json
{
  "payment_intent": {
    "id": "pi_01HZ3X9YQ7M8KH8M5JYV9X4F2B",
    "amount": 8000,
    "currency": "704",
    "status": "succeeded",
    "payment_method_id": "pm_01HZ3XA2K4T7QJ0M9C5N8E1D3F",
    "latest_charge_id": "ch_01HZ3XA2K4T7QJ0M9C5N8E1D3G"
  }
}

PaymentMethod kết quả mang theo nguồn gốc ví điện tử (wallet provenance), hữu ích cho việc đối chiếu và xác định liability shift:

json
{
  "type": "card",
  "card": {
    "brand": "visa",
    "last4": "1234",
    "wallet": {
      "type": "apple_pay",
      "assurance_details": {
        "account_verified": true,
        "card_holder_authenticated": true
      }
    },
    "network_token": {
      "dynamic_last4": "4242",
      "token_requestor_id": "40010030273"
    }
  }
}

Apple Pay luôn tạo ra một thẻ đã device-tokenize (DPAN) — không có cách nào để lấy được PAN gốc của nguồn tiền. card.last4 là bốn chữ số cuối của thẻ nguồn tiền (đây là số mà người dùng thấy trong app Wallet, nên hiển thị số này); network_token.dynamic_last4 thuộc về device token.

INFO

Luôn đặt Bn-Idempotency-Key cho lệnh gọi này, gắn theo order của bạn. Nếu app retry vì response bị mất, idempotency sẽ phát lại response gốc — token của Apple chỉ dùng được một lần, nên nếu không có idempotency, một lần thử lại thực sự sẽ thất bại một cách khó hiểu.

3. Xử lý webhook ​

EventMô tả
payment_intent.status.succeededThanh toán đã được capture. Đánh dấu đơn hàng đã thanh toán.
payment_intent.status.processingThanh toán đang được xử lý. Giữ đơn hàng ở trạng thái chờ.
payment_intent.status.requires_captureChỉ mới được ủy quyền — bạn đã dùng capture_method: manual. Gọi capture.
payment_intent.payment_failedLần thử thất bại. Đọc last_payment_error để biết lý do.

INFO

Không coi dấu tick trên PassKit là bằng chứng thanh toán thành công. Nó chỉ có nghĩa là người dùng đã ủy quyền và handler của bạn đã báo cáo thành công. Hãy xử lý đơn hàng dựa trên webhook.

Xử lý lỗi ​

Triệu chứngNguyên nhân khả dĩCách khắc phục
Nút Apple Pay không bao giờ xuất hiệnPKPaymentAuthorizationController.canMakePayments(usingNetworks:) trả về falseThiết bị không có thẻ hợp lệ, hoặc supportedNetworks không bao gồm loại thẻ người dùng đang có. Chuyển sang form nhập thẻ.
Confirm thất bại với lỗi giải mãChứng chỉ payment processing được tạo từ một CSR không do Bahn cấpThực hiện lại bước tạo chứng chỉ với CSR của Bahn. Bahn phải giữ private key tương ứng.
Confirm thất bại với lỗi chữ ký hoặc token không hợp lệpaymentData bị bọc base64, serialize lại, hoặc bị cắt xén trong quá trình truyềnGửi đúng chuỗi JSON UTF-8 thô như PassKit đã tạo ra.
Confirm thất bại khi retryToken Apple Pay chỉ dùng được một lầnGửi cùng Bn-Idempotency-Key để response gốc được phát lại thay vì một lần ủy quyền mới được thử.
Sheet bị timeoutConfirm mất nhiều hơn ~30 giâyHoàn tất với processing và đối chiếu kết quả qua webhook.

Kiểm thử ​

Apple Pay không thể được kiểm thử trên iOS Simulator với thẻ thật. Sử dụng một thiết bị vật lý đã đăng nhập vào một Apple ID sandbox với một thẻ test Apple Pay Sandbox đã thêm vào Wallet, trỏ đến host sandbox của Bahn https://bn.cakedigibank.tech.


Phụ lục — Apple Pay trên web ​

Nếu bạn cũng chấp nhận Apple Pay trên Safari (thay vì, hoặc song song với, một app native), đó là một tích hợp riêng biệt so với trang này — xem Apple Pay để biết đầy đủ luồng web.

bahn.mountApplePay() hiển thị nút và xử lý việc xác thực merchant cho bạn — không có endpoint merchant-session riêng nào cần gọi từ trang của bạn. Yêu cầu đăng ký domain vẫn áp dụng giống hệt như với native (xem Apple Pay → Yêu cầu): domain production của bạn phải được đăng ký với Apple và phục vụ /.well-known/apple-developer-merchantid-domain-association, nếu không sheet sẽ đóng lại mà không có lỗi hữu ích nào hiển thị trên trình duyệt.

Developers