Xác nhận thanh toán
Xác nhận một payment intent mà backend của bạn đã tạo, bằng thẻ khách hàng nhập vào các trường thẻ của SDK. Đây là luồng xác nhận trực tiếp: số tiền được cố định khi tạo intent, và trình duyệt xác nhận nó bằng một lệnh gọi duy nhất.
Nếu muốn chốt số tiền trên server sau khi biết thẻ, hãy dùng Luồng dựa trên token. Để lưu thẻ mà không trừ tiền, xem Lưu thẻ.
Apple Pay và Google Pay dùng chính luồng này khi bạn truyền clientSecret vào bahn.mountApplePay()/bahn.mountGooglePay() — nút thanh toán tự xác nhận intent, nên bạn không cần gọi confirmPayment() cho chúng.
Ví dụ đầy đủ
Một trang thanh toán hoàn chỉnh cho luồng này: Ví dụ tích hợp → Xác nhận thanh toán, hoặc dưới dạng component React có kiểu: Xác nhận thanh toán với React.
1. Tạo payment intent trên backend
confirmPayment() xác nhận một payment intent đã tồn tại. Hãy tạo nó trên backend bằng một request API đã ký và chỉ trả client_secret về trình duyệt. Xem request phía server tại Thanh toán một lần.
// Backend của bạn: POST /api/create-payment-intent → { client_secret: "pi_xxx_secret_yyy" }
const { client_secret } = await fetch("/api/create-payment-intent", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ amount: 100000, currency: "vnd" }),
}).then((r) => r.json());2. Mount các trường thẻ
const bahn = new Bahn({ pk: "pk_test_...", environment: "sandbox" });
const card = bahn.mountCard("#card-container");confirmPayment() gửi request qua iframe số thẻ, nên thẻ phải được giữ mount cho đến khi lệnh gọi hoàn tất. Xem Thanh toán thẻ.
3. Xác nhận thanh toán
const { payment_intent, three_ds_payload } = await bahn.confirmPayment({
client_secret,
payment_method: {
billing_details: { name: "Nguyen Van A", email: "[email protected]" },
},
});Các trường thẻ không có tên chủ thẻ: hãy thu thập tên bằng ô nhập của riêng bạn và truyền vào billing_details.name như trên. Xem Thanh toán thẻ → Tên chủ thẻ.
4. Xử lý kết quả
Nếu ngân hàng phát hành yêu cầu 3D Secure, three_ds_payload sẽ có mặt: mount thử thách, rồi poll trạng thái cuối khi thử thách thành công. Ngược lại, hãy kiểm tra payment_intent.status: succeeded, requires_capture và requires_confirmation là thành công; failed, canceled và requires_payment_method là thất bại; các trạng thái khác (ví dụ processing) chưa phải trạng thái cuối, nên hãy poll cho đến khi đạt trạng thái cuối.
if (three_ds_payload) {
bahn.mount3DS(three_ds_payload.id, {
onSuccess: () => {
bahn.pollPaymentIntentStatus(client_secret, {
onSuccess: (paymentIntent) => redirectToSuccess(paymentIntent),
onFailure: (paymentIntent) => redirectToFailure(paymentIntent),
onTimeout: () => showTimeoutMessage(),
onError: (error) => console.error("Poll error:", error.message),
});
},
onFailure: (error) => showError(`3DS failed: ${error?.message ?? "Unknown error"}`),
});
} else if (["succeeded", "requires_capture", "requires_confirmation"].includes(payment_intent.status)) {
redirectToSuccess(payment_intent);
} else if (["failed", "canceled", "requires_payment_method"].includes(payment_intent.status)) {
redirectToFailure(payment_intent);
} else {
// ví dụ "processing" — poll cho đến khi đạt trạng thái cuối
bahn.pollPaymentIntentStatus(client_secret, {
onSuccess: (paymentIntent) => redirectToSuccess(paymentIntent),
onFailure: (paymentIntent) => redirectToFailure(paymentIntent),
onTimeout: () => showTimeoutMessage(),
});
}Để chạy thử thách trên một trang riêng thay vì overlay, xem Xác thực 3DS → Trang thử thách độc lập.
Tin vào webhook, không phải trình duyệt
Kết quả trên trình duyệt quyết định bạn hiển thị gì cho khách hàng. Chỉ đánh dấu đơn hàng đã thanh toán sau khi backend nhận được webhook cho trạng thái cuối. Xem Webhooks.
Cách hoạt động
sequenceDiagram
participant App as Your App
participant Backend as Your Backend
participant SDK as Bahn SDK
participant CardIframe as Card Iframe
participant API as Cake API
App->>Backend: Start checkout
Backend->>API: Create PaymentIntent (amount, currency)
API-->>Backend: PaymentIntent
Backend-->>App: { client_secret }
App->>SDK: mountCard("#container", options)
App->>SDK: confirmPayment({ client_secret })
SDK->>CardIframe: Delegate + merge card data
CardIframe->>API: POST /api/sdk/payment_intents/confirm
API-->>CardIframe: PaymentIntent + optional base64 3DS payload
CardIframe-->>SDK: Decoded result (no card data)
SDK-->>App: { payment_intent, three_ds_payload? }
opt three_ds_payload present
App->>SDK: mount3DS(three_ds_payload.id, callbacks)
SDK-->>App: callbacks.onSuccess()
App->>SDK: pollPaymentIntentStatus(client_secret, callbacks)
SDK-->>App: callbacks.onSuccess(paymentIntent) / onFailure(paymentIntent)
end
bahn.confirmPayment(payload)
Xác nhận một payment intent bằng thẻ trong các trường thẻ đã mount. Request được gửi từ bên trong iframe số thẻ — thông tin thẻ được ghép tại đó và không bao giờ đến trang của bạn.
confirmPayment(payload: Bahn.ConfirmPaymentPayload): Promise<{
payment_intent: Record<string, unknown>;
three_ds_payload?: Bahn.ThreeDSPayloadDecoded;
}>;Tham số — ConfirmPaymentPayload:
| Trường | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
client_secret | string | Có | Client secret của payment intent mà backend của bạn đã tạo |
payment_method.billing_details | ConfirmationTokenBillingDetails | Không | Tên, email, số điện thoại và địa chỉ của khách hàng. Dữ liệu thẻ do iframe thêm vào — không bao giờ truyền ở đây |
payment_method_options | Record<string, unknown> | Không | Các tùy chọn riêng của phương thức thanh toán |
confirmation_token | string | Không | Token từ createConfirmationToken(), để xác nhận bằng một thẻ đã token hóa trước đó |
amount | string | Không | Ghi đè số tiền, theo đơn vị tiền tệ nhỏ nhất |
Giá trị trả về:
| Trường | Kiểu | Mô tả |
|---|---|---|
payment_intent | Record<string, unknown> | Đối tượng PaymentIntent. Xem các trường trong Tham chiếu API |
three_ds_payload | ThreeDSPayloadDecoded | undefined | Chỉ có mặt khi cần xác thực 3DS. Truyền id của nó vào mount3DS() |
Reject với APIError:
| Nguyên nhân | status | isUserError |
|---|---|---|
Chưa gọi mountCard() | 0 | false |
| Các trường thẻ không hợp lệ hoặc chưa đầy đủ | 0 | true |
| Các trường thẻ không phản hồi trong 5 giây | 0 | true |
Request bị từ chối (400, 401, 403, 404, 409, 422) | Mã HTTP | true |
Giới hạn tần suất (429) hoặc lỗi server (5xx) | Mã HTTP | false |
| Không có phản hồi trong 30 giây, hoặc lỗi mạng | 0 | false |
Khi isUserError là true, có thể hiển thị message cho khách hàng. Các phương thức này không bao giờ gán APIError.code, nên hãy phân nhánh theo status và isUserError.
try {
const result = await bahn.confirmPayment({ client_secret });
// ...
} catch (error) {
if (error?.name === "APIError" && error.isUserError) {
showFieldError(error.message);
} else {
showGenericError();
}
}Xem mô hình lỗi đầy đủ tại Xử lý lỗi.
Bước tiếp theo
- Xác thực 3DS — tùy chọn thử thách, mã lỗi và luồng chuyển hướng
- Định nghĩa kiểu — mọi kiểu dùng trên trang này
- Ví dụ tích hợp — các trang thanh toán hoàn chỉnh