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áicanceled.
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 |
|---|---|
amount | Tổng số tiền đã ủy quyền — kích thước khoản giữ trên thẻ. |
amount_capturable | Số 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_received | Số 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_capture | Hiệu ứng | Yê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 |
false | Số 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:
{
"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ước | Request | amount_received | amount_capturable | Trạng thái | Tiền đã di chuyển |
|---|---|---|---|---|---|
| Đã ủy quyền | — | 0 | 100000 | requires_capture | — |
| Giao sản phẩm 1 | { "amount_to_capture": 50000, "final_capture": false } | 50000 | 50000 | requires_capture | chưa có |
| Giao sản phẩm 2 (đóng) | { "amount_to_capture": 20000, "final_capture": true } | 70000 | 0 | succeeded | 70000 đượ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ối | Lý 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ền | Bạ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: false | Mộ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 multicapture | Multiple 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:
POST /api/payment_intentsRequest
{
"amount": 100000,
"currency": "704",
"capture_method": "manual",
"payment_method_options": {
"card": {
"request_multicapture": "if_available"
}
},
"description": "Order #1234",
"metadata": {
"order_id": "1234"
}
}| Trường | Bắt buộc | Mô tả |
|---|---|---|
amount | Có | Số tiền cần ủy quyền, theo đơn vị tiền tệ nhỏ nhất (đồng nguyên cho VND). |
currency | Có | Mã tiền tệ số ISO 4217 (ví dụ "704" cho VND). |
capture_method | Có | Đặt thành "manual" để bật capture riêng biệt. Mặc định là "automatic". |
payment_method_options.card.request_multicapture | Khô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. |
description | Không | Mô tả dễ đọc hiển thị trên sao kê của khách hàng. |
metadata | Không | Các cặp key-value tùy ý để lưu thêm thông tin. |
Response
{
"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.
| Event | Mô 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:
POST /api/payment_intents/{id}/captureRequest — body là tùy chọn. Gửi {} (hoặc bỏ qua) để capture toàn bộ amount_capturable và đóng PaymentIntent.
{
"amount_to_capture": 50000,
"final_capture": false
}| Trường | Bắt buộc | Mô tả |
|---|---|---|
amount_to_capture | Không | Số 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_capture | Khô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:
{
"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:
{
"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:
POST /api/payment_intents/{id}/cancelRequest
{
"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
| Event | Mô tả |
|---|---|
payment_intent.status.succeeded | Capture 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. |