Skip to content

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.

typescript
// 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ẻ ​

typescript
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 ​

typescript
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.

typescript
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.

typescript
confirmPayment(payload: Bahn.ConfirmPaymentPayload): Promise<{
  payment_intent: Record<string, unknown>;
  three_ds_payload?: Bahn.ThreeDSPayloadDecoded;
}>;

Tham số — ConfirmPaymentPayload:

TrườngKiểuBắt buộcMô tả
client_secretstringCóClient secret của payment intent mà backend của bạn đã tạo
payment_method.billing_detailsConfirmationTokenBillingDetailsKhôngTê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_optionsRecord<string, unknown>KhôngCác tùy chọn riêng của phương thức thanh toán
confirmation_tokenstringKhôngToken từ createConfirmationToken(), để xác nhận bằng một thẻ đã token hóa trước đó
amountstringKhôngGhi đè số tiền, theo đơn vị tiền tệ nhỏ nhất

Giá trị trả về:

TrườngKiểuMô tả
payment_intentRecord<string, unknown>Đối tượng PaymentIntent. Xem các trường trong Tham chiếu API
three_ds_payloadThreeDSPayloadDecoded | undefinedChỉ 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ânstatusisUserError
Chưa gọi mountCard()0false
Các trường thẻ không hợp lệ hoặc chưa đầy đủ0true
Các trường thẻ không phản hồi trong 5 giây0true
Request bị từ chối (400, 401, 403, 404, 409, 422)Mã HTTPtrue
Giới hạn tần suất (429) hoặc lỗi server (5xx)Mã HTTPfalse
Không có phản hồi trong 30 giây, hoặc lỗi mạng0false

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.

typescript
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 ​

Developers