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
- Webhook
- Tích hợp API
- Thanh toán một lần — phiên bản tương đương dựa trên thẻ của luồng này
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 | Ở đâu | Ghi chú |
|---|---|---|
| Merchant ID | Apple Developer → Identifiers → Merchant IDs | ví dụ merchant.vn.cake.yourapp. Được dùng làm PKPaymentRequest.merchantIdentifier. |
| Payment Processing Certificate | Apple 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 entitlement | Xcode → Signing & Capabilities | Thê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
- 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.
- 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.
- Backend gọi
CreatePaymentIntent— Merchant API của bạn gọiCreatePaymentIntentvớiamountvàcurrency(mã số704cho VND). Bahn trả vềpayment_intentvàclient_secrettương ứng. - 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_idriêng của bạn và giữ cảpayment_intent.idlẫ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
App hiển thị payment sheet — Xây dựng một
PKPaymentRequestvà hiển thị nó bằngPKPaymentAuthorizationController.swiftlet 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()merchantCapabilitiesphả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.PassKit hiển thị sheet — iOS hiển thị bộ chọn thẻ, số tiền, và tên merchant từ request.
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.
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 blobDatachứa nội dung là payment token đã mã hóa dưới dạng JSON ({"data":…,"signature":…,"header":{…},"version":"EC_v1"}).swiftfunc 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
- 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. - 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. - 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ặcprocessing(đang xử lý). - 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ặcpayment_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
- 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.
- Backend trả về đơn hàng — Đã được cập nhật từ webhook event.
- 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.
POST /api/payment_intentsRequest
{
"amount": 8000,
"currency": "704",
"description": "Order #1234",
"metadata": { "order_id": "1234" }
}Response
{
"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
POST /api/payment_intents/{id}/confirmRequest
{
"payment_method_data": {
"apple_pay": {
"token": "{\"data\":\"WTP8BxZNUs…\",\"signature\":\"MIAGCSqGSIb3…\",\"header\":{\"publicKeyHash\":\"7c4ZFn…\",\"ephemeralPublicKey\":\"MFkwEwYH…\",\"transactionId\":\"31323334…\"},\"version\":\"EC_v1\"}"
}
}
}| Trường | Bắt buộc | Mô tả |
|---|---|---|
payment_method_data.apple_pay.token | Có | 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_options | Không | Cá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_method | Khô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.
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
{
"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:
{
"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
| Event | Mô tả |
|---|---|
payment_intent.status.succeeded | Thanh toán đã được capture. Đánh dấu đơn hàng đã thanh toán. |
payment_intent.status.processing | Thanh toán đang được xử lý. Giữ đơn hàng ở trạng thái chờ. |
payment_intent.status.requires_capture | Chỉ mới được ủy quyền — bạn đã dùng capture_method: manual. Gọi capture. |
payment_intent.payment_failed | Lầ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ứng | Nguyên nhân khả dĩ | Cách khắc phục |
|---|---|---|
| Nút Apple Pay không bao giờ xuất hiện | PKPaymentAuthorizationController.canMakePayments(usingNetworks:) trả về false | Thiế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ấp | Thự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ền | Gửi đúng chuỗi JSON UTF-8 thô như PassKit đã tạo ra. |
| Confirm thất bại khi retry | Token Apple Pay chỉ dùng được một lần | Gử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ị timeout | Confirm mất nhiều hơn ~30 giây | Hoà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.