Skip to content

Manual capture ​

Theo mặc định, Bahn capture số tiền đã ủy quyền ngay sau khi ủy quyền thành công. Manual capture tách hai bước này ra: thanh toán được ủy quyền (tiền được giữ) nhưng chưa được capture (tiền được thu) cho đến khi backend của bạn chủ động gọi CapturePaymentIntent. Điều này hữu ích khi bạn cần kiểm tra tồn kho, chạy kiểm tra gian lận, hoặc điều chỉnh đơn hàng trước khi thực hiện thu tiền.

Bạn không bắt buộc phải thu toàn bộ số tiền đã ủy quyền. Một lần capture có thể là một phần, và lần capture cuối cùng sẽ giải phóng phần còn lại đang được giữ — xem Capture một phần và capture cuối cùng.

Việc capture qua nhiều hơn một lần gọi là một tùy chọn đăng ký riêng: chỉ những PaymentIntent được tạo với payment_method_options.card.request_multicapture: if_available mới chấp nhận một lần capture với final_capture: false. Nếu không có tùy chọn này, bạn chỉ có đúng một lần capture, và đó phải là lần capture cuối cùng.

Nếu bạn quyết định không thu bất kỳ khoản nào, hãy gọi CancelPaymentIntent thay vào đó — Bahn sẽ đảo ngược việc ủy quyền trên mạng thẻ.

Luồng tổng quan ​

sequenceDiagram
    autonumber
    participant MerchantAPI as Merchant API<br/>(Backend)
    participant API as bn_api<br/>(Gateway)
    participant TXN as bn_transaction
    participant WHK as Webhook

    MerchantAPI ->> API: CreatePaymentIntent(amount, currency, capture_method=manual,<br/>payment_method_options.card.request_multicapture=if_available)
    API ->> TXN: CreatePaymentIntent(capture_method=manual)
    Note over TXN: PI status=requires_payment_method, capture_method=manual
    TXN -->> API: {payment_intent_id, client_secret}
    API -->> MerchantAPI: {payment_intent_id, client_secret}

    Note over MerchantAPI, TXN: Confirm + async authorization — same as standard flow

    Note over TXN: Authorization approved →<br/>Charge status=succeeded, captured=false<br/>PI status=requires_capture
    WHK ->> MerchantAPI: payment_intent.status.requires_capture

    MerchantAPI ->> API: CapturePaymentIntent(payment_intent_id)
    API ->> TXN: CapturePaymentIntent(payment_intent_id)
    Note over TXN: Creates local AUTHORIZATION.CLEARING transaction (no network call)<br/>Charge captured=true<br/>PI status=succeeded
    TXN -->> API: {payment_intent: {status=succeeded}}
    API -->> MerchantAPI: {payment_intent: {status=succeeded}}
    WHK ->> MerchantAPI: payment_intent.status.succeeded

    Note over TXN: AUTHORIZATION.CLEARING enters batch clearing

Cách hoạt động ​

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

Backend của bạn tạo một PaymentIntent với capture_method=manual. Điều này báo cho Bahn biết rằng bước ủy quyền và bước capture sẽ được tách riêng.

Nếu bạn dự định capture qua nhiều hơn một lần gọi, hãy đặt luôn payment_method_options.card.request_multicapture thành if_available ngay từ bước này — tính đủ điều kiện được gắn vào charge dựa trên các tùy chọn được lưu tại thời điểm tạo và không thể thêm vào sau đó.

Giai đoạn 2 — Confirm & ủy quyền ​

Các bước confirm và ủy quyền bất đồng bộ giống hệt luồng thanh toán tiêu chuẩn. SDK thu thập thông tin thẻ và xác nhận PaymentIntent; Bahn gửi yêu cầu ủy quyền đến mạng thẻ.

Giai đoạn 3 — Ủy quyền được chấp thuận (requires_capture) ​

Khi mạng thẻ chấp thuận việc ủy quyền, Bahn chuyển PaymentIntent sang trạng thái requires_capture thay vì succeeded. Charge ở trạng thái succeeded nhưng captured=false — tiền được giữ trên thẻ nhưng chưa được thu.

Bahn gửi một webhook event payment_intent.status.requires_capture đến endpoint đã đăng ký của bạn. Backend của bạn nên coi event này là tín hiệu để thực hiện logic tiền-capture (kiểm tra tồn kho, rà soát gian lận, điều chỉnh số tiền).

Giai đoạn 4 — Capture hoặc hủy ​

Sau khi hoàn tất logic tiền-capture, bạn có hai lựa chọn:

  • Capture — gọi CapturePaymentIntent, cho toàn bộ số tiền đã ủy quyền hoặc một phần của nó. Bahn tạo một giao dịch clearing cục bộ (không có lệnh gọi mạng đồng bộ). File clearing được gửi đến mạng thẻ trong đợt batch tiếp theo.
  • Hủy — gọi CancelPaymentIntent. Bahn đảo ngược việc ủy quyền trên mạng thẻ và chuyển PaymentIntent sang trạng thái canceled.

Việc giữ tiền ủy quyền có giới hạn thời gian do ngân hàng phát hành thẻ đặt ra (thường là 5–7 ngày). Nếu bạn không capture hoặc hủy trong khoảng thời gian đó, việc giữ tiền sẽ hết hạn và tiền được giải phóng về cho chủ thẻ. Bahn không tự động capture khi hết hạn.


Các trường số tiền trong capture ​

Ba trường trên PaymentIntent mô tả dòng tiền ở mỗi giai đoạn. Hãy đọc chúng từ response của capture thay vì tự theo dõi tổng số.

TrườngÝ nghĩa
amountTổng số tiền đã ủy quyền — kích thước khoản giữ trên thẻ.
amount_capturableSố tiền còn lại có thể capture từ khoản giữ đó. Bắt đầu bằng amount, giảm dần sau mỗi lần capture, và về 0 khi PaymentIntent đóng lại.
amount_receivedSố tiền bạn đã thu được cho đến nay, cộng dồn qua tất cả các lần capture.

Tại bất kỳ thời điểm nào trước lần capture cuối cùng, amount_capturable + amount_received == amount.

Capture một phần và capture cuối cùng ​

amount_to_capture có thể nhỏ hơn amount_capturable. Điều gì xảy ra với phần còn lại hoàn toàn do final_capture quyết định — không phải do bản thân số tiền.

final_captureHiệu ứngYêu cầu multicapture
true (mặc định)Toàn bộ số tiền đã thu qua tất cả các lần capture được thanh toán (settle) trong một lần duy nhất. Phần tiền ủy quyền còn lại được giải phóng, PaymentIntent chuyển thành succeeded, và không thể capture thêm.Không
falseSố tiền được ghi nhận là còn nợ nhưng chưa có gì được settle. amount_received tăng, amount_capturable giảm, phần còn lại vẫn được ủy quyền, và PaymentIntent vẫn ở trạng thái requires_capture để capture thêm.Có

final_capture: false yêu cầu multicapture

Một lần capture với final_capture: false chỉ được chấp nhận khi charge đã được bật multicapture — tức là PaymentIntent được tạo với payment_method_options.card.request_multicapture: if_available. Nếu không, request sẽ bị từ chối với thông báo multiple captures are not supported for this payment, và PaymentIntent vẫn giữ nguyên ở trạng thái requires_capture.

Một lần capture một phần đơn lẻ không cần đăng ký gì thêm: amount_to_capture thấp hơn amount_capturable với final_capture: true sẽ thu một phần số tiền ủy quyền và giải phóng phần còn lại, trong một lệnh gọi duy nhất. Điều mà multicapture mang lại cho bạn là lệnh gọi thứ hai.

Hãy kiểm tra trước khi phụ thuộc vào nó — đọc payment_method_details.card.multicapture.status trên charge (available hoặc không có) thay vì giả định rằng việc đăng ký đã được áp dụng.

WARNING

Việc capture ít hơn số tiền đã ủy quyền chỉ giải phóng phần chênh lệch ở lần capture cuối cùng. Một lệnh gọi final_capture: false thu ít hơn nhưng vẫn giữ nguyên phần còn lại — chủ thẻ vẫn thấy toàn bộ số tiền ủy quyền đang bị giữ.

Vì vậy, một lần capture chia nhỏ là một chuỗi các lệnh gọi final_capture: false — mỗi lệnh cần multicapture — theo sau bởi một lệnh gọi đóng cuối cùng với final_capture: true. Lệnh gọi cuối đó mới thực sự di chuyển tiền: nó settle tổng của tất cả các lần capture thành một lần thanh toán duy nhất với một balance transaction.

Giải phóng phần còn lại mà không thu thêm ​

Để đóng một PaymentIntent đã được capture một phần khi bạn đã thu đủ những gì mình muốn, hãy gửi:

json
{
  "amount_to_capture": 0,
  "final_capture": true
}

Phần chưa được capture sẽ được giải phóng và PaymentIntent chuyển thành succeeded.

INFO

Điều này yêu cầu ít nhất một lần capture trước đó. Một PaymentIntent chưa thu được gì phải được hủy thay vào đó, việc này đảo ngược ủy quyền thay vì đóng nó lại.

Ví dụ minh họa ​

Một đơn hàng được ủy quyền 100.000, được tạo với request_multicapture: if_available. Một sản phẩm được giao ngay, sản phẩm thứ hai giao sau, và phần còn lại của đơn hàng bị hủy.

BướcRequestamount_receivedamount_capturableTrạng tháiTiền đã di chuyển
Đã ủy quyền—0100000requires_capture—
Giao sản phẩm 1{ "amount_to_capture": 50000, "final_capture": false }5000050000requires_capturechưa có
Giao sản phẩm 2 (đóng){ "amount_to_capture": 20000, "final_capture": true }700000succeeded70000 được settle

Lệnh gọi đóng settle 70.000 thành một giao dịch duy nhất và giải phóng 30.000 còn lại của khoản giữ. Số dư khả dụng của chủ thẻ sẽ được khôi phục lại 30.000 đó sau khi ngân hàng phát hành xử lý việc giải phóng.

Nếu lệnh gọi thứ hai dùng final_capture: false, sẽ không có gì được settle và toàn bộ 100.000 vẫn còn bị giữ.

Lỗi ​

Request bị từ chốiLý do
amount_to_capture cao hơn amount_capturable, hoặc cao hơn phần còn lại của tổng số đã ủy quyềnBạn không thể capture nhiều hơn số đã được ủy quyền. Sử dụng tăng số tiền ủy quyền để tăng tổng số trước.
amount_to_capture: 0 với final_capture: falseMột lần capture không phải cuối cùng phải thu một số tiền dương. Để giải phóng phần còn lại, đặt final_capture: true.
final_capture: false trên một charge chưa bật multicaptureMultiple captures are not supported for this payment. PaymentIntent không được tạo với payment_method_options.card.request_multicapture: if_available, và không thể thêm tùy chọn này sau khi tạo. Hãy capture một lần với final_capture: true.
Một PaymentIntent không ở trạng thái requires_capture, không được tạo với capture_method: manual, hoặc đã được capture đầy đủKhông còn gì để capture.

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

1. Tạo PaymentIntent với manual capture ​

Truyền capture_method=manual khi tạo PaymentIntent:

http
POST /api/payment_intents

Request

json
{
  "amount": 100000,
  "currency": "704",
  "capture_method": "manual",
  "payment_method_options": {
    "card": {
      "request_multicapture": "if_available"
    }
  },
  "description": "Order #1234",
  "metadata": {
    "order_id": "1234"
  }
}
TrườngBắt buộcMô tả
amountCóSố tiền cần ủy quyền, theo đơn vị tiền tệ nhỏ nhất (đồng nguyên cho VND).
currencyCóMã tiền tệ số ISO 4217 (ví dụ "704" cho VND).
capture_methodCóĐặt thành "manual" để bật capture riêng biệt. Mặc định là "automatic".
payment_method_options.card.request_multicaptureKhông"if_available" để cho phép capture qua nhiều hơn một lệnh gọi, tức là chấp nhận final_capture: false sau này. Mặc định là "never". Chỉ áp dụng lúc tạo — endpoint confirm chấp nhận payment_method_options nhưng không áp dụng nó, vì vậy không thể thêm tùy chọn này sau.
descriptionKhôngMô tả dễ đọc hiển thị trên sao kê của khách hàng.
metadataKhôngCác cặp key-value tùy ý để lưu thêm thông tin.

Response

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

2. Xử lý webhook requires_capture ​

Khi ủy quyền được chấp thuận, Bahn gửi một event payment_intent.status.requires_capture. Sử dụng event này để chạy logic nghiệp vụ tiền-capture của bạn.

EventMô tả
payment_intent.status.requires_captureỦy quyền đã được chấp thuận. Tiền đang được giữ. Gọi CapturePaymentIntent hoặc CancelPaymentIntent.

3. Capture thanh toán ​

Sau khi logic tiền-capture của bạn hoàn tất và bạn sẵn sàng thu tiền, hãy gọi CapturePaymentIntent:

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

Request — body là tùy chọn. Gửi {} (hoặc bỏ qua) để capture toàn bộ amount_capturable và đóng PaymentIntent.

json
{
  "amount_to_capture": 50000,
  "final_capture": false
}
TrườngBắt buộcMô tả
amount_to_captureKhôngSố tiền cần capture theo đơn vị tiền tệ nhỏ nhất, ≤ amount_capturable. Mặc định là toàn bộ amount_capturable. 0 chỉ được phép khi đi kèm final_capture: true.
final_captureKhôngĐây có phải là lần capture cuối cùng hay không. Mặc định là true, việc này sẽ settle tất cả số tiền đã thu cho đến nay và giải phóng phần tiền ủy quyền còn lại. Đặt thành false để giữ phần còn lại ở trạng thái ủy quyền cho một lần capture tiếp theo — chỉ được chấp nhận trên một charge đã bật multicapture.

Response — một lần capture không phải cuối cùng để PaymentIntent ở trạng thái requires_capture với các trường số tiền đã cập nhật:

json
{
  "payment_intent": {
    "id": "pi_01HZ3X9YQ7M8KH8M5JYV9X4F2B",
    "amount": 100000,
    "amount_received": 50000,
    "amount_capturable": 50000,
    "status": "requires_capture"
  }
}

Một lần capture cuối cùng sẽ đóng nó lại:

json
{
  "payment_intent": {
    "id": "pi_01HZ3X9YQ7M8KH8M5JYV9X4F2B",
    "amount": 100000,
    "amount_received": 70000,
    "amount_capturable": 0,
    "status": "succeeded"
  }
}

Bahn gửi một webhook event payment_intent.status.succeeded khi PaymentIntent đóng lại. Không có webhook nào được gửi cho một lần capture không phải cuối cùng — vì PaymentIntent chưa đổi trạng thái — vì vậy hãy tự quản lý sổ sách dựa trên response của capture.

4. Hủy thay vì capture ​

Để giải phóng khoản giữ ủy quyền mà không thu bất kỳ khoản nào, hãy gọi CancelPaymentIntent:

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

Request

json
{
  "cancellation_reason": "abandoned"
}

Bahn đảo ngược việc ủy quyền trên mạng thẻ và gửi một webhook event payment_intent.status.canceled.

Việc hủy chỉ hoạt động khi chưa có gì được thu. Khi PaymentIntent đã có amount_received khác 0, hãy đóng nó lại bằng amount_to_capture: 0 và final_capture: true thay vào đó, và thực hiện Refund nếu bạn cần hoàn trả những gì đã được capture.

5. Xử lý các webhook trạng thái cuối ​

EventMô tả
payment_intent.status.succeededCapture hoàn tất và PaymentIntent đã đóng. Đánh dấu đơn hàng đã thanh toán với amount_received — có thể thấp hơn amount.
payment_intent.status.canceledỦy quyền đã bị đảo ngược. Giải phóng đơn hàng hoặc đề nghị thử lại.
payment_intent.payment_failedỦy quyền bị mạng thẻ từ chối.

Developers