Skip to content

Thanh toán định kỳ & MIT không định kỳ ​

Một thẻ đã lưu có thể được tính phí mà không cần khách hàng có mặt (một MIT — giao dịch do merchant khởi tạo), nhưng các mạng thẻ phân biệt chủ thẻ đã đồng ý điều gì khi thẻ được lưu lại. Sự đồng ý đó là một mandate, và Bahn ghi nhận nó trên giao dịch đã thiết lập nó.

Có hai hình thức:

Thỏa thuậnChủ thẻ đã đồng ý điều gìTrường hợp sử dụng phổ biếnCách thiết lập
Không định kỳ (Unscheduled) (mặc định)Merchant có thể lưu thẻ và tính phí lại, không theo lịch cố định.Nạp tiền, trả tiền theo mức sử dụng, tính lại phí cho đơn hàng thất bại, mua lại chỉ với một cú nhấp.Một SetupIntent, hoặc một PaymentIntent với setup_future_usage=off_session — tùy chọn kèm setup_future_usage_cadence=unscheduled, cũng là giá trị mặc định
Định kỳ (Recurring)Merchant có thể tính phí theo một lịch cố định.Subscription, trả góp, phí thành viên.Một PaymentIntent với setup_future_usage=off_session và payment_method_options.card.setup_future_usage_cadence=recurring

Việc khai báo đúng loại thỏa thuận rất quan trọng đối với tranh chấp (dispute): các marker mà Bahn gửi cho Visa và Mastercard trên giao dịch lưu thẻ ban đầu sẽ được phát lại trên mọi MIT sau đó dựa theo mandate đó, và đây chính là bằng chứng cho sự đồng ý của chủ thẻ nếu giao dịch bị tranh chấp. Một khoản trả góp định kỳ được tính phí dưới marker không định kỳ sẽ là một vị thế yếu hơn so với việc tính phí dưới đúng thỏa thuận mà chủ thẻ thực sự đã chấp nhận.

WARNING

Thỏa thuận được cố định bởi giao dịch lưu thông tin thanh toán — nó không thể thay đổi sau đó. Nếu một khách hàng bắt đầu một subscription, hãy thiết lập một mandate định kỳ ngay tại thời điểm đó; bạn không thể nâng cấp một mandate không định kỳ thành định kỳ sau này. Hãy thiết lập một mandate mới thay vào đó.

Điều kiện tiên quyết ​

Luồng tổng quan ​

sequenceDiagram
    autonumber
    participant User as User
    participant SDK as SDK<br/>(Embedded)
    participant MerchantAPI as Merchant API<br/>(Backend)
    participant API as Bahn API

    Note over User, API: Step 1 — a CIT establishes the agreement
    User ->> MerchantAPI: Subscribe / accept terms
    MerchantAPI ->> API: CreatePaymentIntent(customer_id,<br/>setup_future_usage=off_session,<br/>payment_method_options.card.setup_future_usage_cadence=recurring)
    Note over API: both fields are creation-only

    alt Path A — SDK confirms with the client secret (recommended)
        API -->> MerchantAPI: {payment_intent, client_secret}
        MerchantAPI -->> SDK: {client_secret}
        SDK ->> API: ConfirmPaymentIntentWithClientSecret(card)
        Note over API: mandate_data captured from the<br/>customer's own request
    else Path B — server-side confirm at creation
        Note over MerchantAPI, API: same call carries confirm=true,<br/>off_session=false and mandate_data
        API -->> MerchantAPI: {payment_intent}
    end

    Note over API: storing authorization states the<br/>recurring agreement to the network
    API -->> SDK: 3DS challenge (customer present)
    User ->> SDK: Complete 3DS
    API ->> MerchantAPI: Webhook: payment_intent.status.succeeded
    Note over API: mandate created, recorded on the charge

    Note over MerchantAPI, API: Step 2 — read back the mandate_id
    MerchantAPI ->> API: GetPaymentIntent(id,<br/>expand=["payment_intent.latest_charge"])
    API -->> MerchantAPI: {latest_charge.payment_method_details.card.mandate_id}
    Note over MerchantAPI: store mandate_id with the subscription

    Note over MerchantAPI, API: Step 3 — each renewal is an MIT
    MerchantAPI ->> API: CreatePaymentIntent(confirm=true, off_session=true,<br/>payment_method_id, mandate_id)
    Note over API: replays the recurring agreement<br/>recorded on that mandate
    API ->> MerchantAPI: Webhook: payment_intent.status.succeeded

Bước 1 — Thiết lập thỏa thuận bằng một CIT ​

Mandate được tạo bởi một giao dịch do khách hàng khởi tạo (customer-initiated), nơi khách hàng có mặt và xác thực. Cả hai trường mô tả thỏa thuận đều nằm trên lệnh gọi create:

POST /api/payment_intents
TrườngBắt buộcMô tả
setup_future_usageCóPhải là off_session — đây là thứ lưu lại một thông tin thanh toán mà bạn có thể tính phí sau này mà không cần khách hàng.
payment_method_options.card.setup_future_usage_cadenceKhôngrecurring biến thanh toán này thành giao dịch khởi đầu của một thỏa thuận định kỳ; unscheduled giữ thông tin thanh toán trên hồ sơ mà không theo lịch nào. Bỏ qua trường này nghĩa là unscheduled.
customer_idCóKhách hàng mà thông tin thanh toán được lưu gắn với. Không thể lưu thông tin thanh toán mà không có trường này.

Cả hai trường chỉ áp dụng lúc tạo

ConfirmPaymentIntent bỏ qua payment_method_options, và ConfirmPaymentIntentWithClientSecret hoàn toàn không chấp nhận trường này. Đặt setup_future_usage_cadence tại thời điểm confirm sẽ không có hiệu lực — thỏa thuận sẽ âm thầm quay về unscheduled. Hãy đặt nó khi bạn tạo intent.

Từ đó, xác nhận intent theo một trong hai cách.

Phương án A — SDK xác nhận bằng client secret ​

Tạo intent mà không có confirm, chuyển client_secret cho frontend của bạn, và để SDK xác nhận nó. Đây là luồng checkout thông thường (Thanh toán một lần) và là phương án được khuyến nghị.

Request

{
  "amount": "10000",
  "currency": "usd",
  "customer_id": "cus_xxxxxxxx",
  "setup_future_usage": "off_session",
  "payment_method_options": {
    "card": {
      "setup_future_usage_cadence": "recurring"
    }
  },
  "description": "Pro plan — first month"
}

Response

{
  "payment_intent": {
    "id": "pi_xxxxxxxx",
    "client_secret": "pi_xxxxxxxx_secret_xxxxxxxx",
    "status": "requires_payment_method",
    "setup_future_usage": "off_session",
    "customer_id": "cus_xxxxxxxx"
  }
}

Bạn không gửi mandate_data trên phương án này. Khi SDK xác nhận bằng client secret, Bahn ghi nhận sự chấp thuận của khách hàng từ chính request đó — IP thực của client, User-Agent, và thời điểm chấp thuận — những thứ mà SDK trên trình duyệt không thể tự báo cáo một cách đáng tin cậy và backend của bạn hoàn toàn không thể quan sát được. Đây là nguồn gốc tốt hơn cho một bản ghi đồng ý, đó là lý do phương án này được ưu tiên.

Phương án B — Xác nhận phía server tại thời điểm tạo ​

Tạo và xác nhận trong cùng một lệnh gọi với confirm=true. Vì không có request nào từ phía khách hàng liên quan, bạn phải tự cung cấp mandate_data.customer_acceptance, trong cùng lệnh gọi tạo đó.

Request

{
  "amount": "10000",
  "currency": "usd",
  "customer_id": "cus_xxxxxxxx",
  "payment_method_id": "pm_xxxxxxxx",
  "confirm": true,
  "off_session": false,
  "setup_future_usage": "off_session",
  "payment_method_options": {
    "card": {
      "setup_future_usage_cadence": "recurring"
    }
  },
  "mandate_data": {
    "customer_acceptance": {
      "type": "online",
      "accepted_at": "2026-09-07T10:15:30Z",
      "online": {
        "ip_address": "203.0.113.42",
        "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
      }
    }
  }
}
TrườngBắt buộcMô tả
confirmCótrue để tạo và xác nhận trong một lệnh gọi.
off_sessionCóPhải là false. Giao dịch thiết lập là do khách hàng khởi tạo; off_session=true kết hợp với setup_future_usage sẽ bị từ chối.
mandate_data.customer_acceptance.typeCóonline hoặc offline.
mandate_data.customer_acceptance.accepted_atCóThời điểm khách hàng chấp thuận thỏa thuận.
mandate_data.customer_acceptance.online.ip_addressBắt buộc với onlineĐịa chỉ IP của khách hàng tại thời điểm chấp thuận.
mandate_data.customer_acceptance.online.user_agentBắt buộc với onlineUser-Agent của khách hàng tại thời điểm chấp thuận.

TIP

mandate_data được cung cấp lúc tạo sẽ được lưu trên intent, vì vậy nó cũng thỏa mãn cho một lần confirm sau đó không mang theo trường này. Việc cung cấp nó vô hại trên Phương án A — nhưng sự chấp thuận mà Bahn ghi nhận từ chính request của SDK có giá trị pháp lý cao hơn so với thứ backend của bạn tự dựng lại, vì vậy hãy ưu tiên việc ghi nhận tự động của Phương án A khi có thể.

3DS trên giao dịch thiết lập ​

3DS trên khoản thanh toán này là thứ mang tính xác thực đi tiếp đến mọi MIT sau này. Yêu cầu bỏ qua nó sẽ bị từ chối thẳng thừng thay vì bị bỏ qua: payment_method_options.card.request_three_d_secure=never kết hợp với setup_future_usage sẽ bị từ chối, vì vậy bạn sẽ không bao giờ nhầm tưởng rằng một thông tin thanh toán đã lưu được xác thực trong khi thực tế không phải vậy. Xem Kiểm soát 3DS.

Thiết lập thông qua một SetupIntent ​

Một SetupIntent cũng lưu một thông tin thanh toán off-session, và là lựa chọn phù hợp khi bạn lưu thẻ mà không tính phí ngay. Nó không có payment_method_options, vì vậy nó chỉ có thể tạo ra một mandate không định kỳ.

Để thiết lập một mandate định kỳ, hãy dùng một PaymentIntent thực sự tính phí cho kỳ đầu tiên — đây cũng chính là những gì các mạng thẻ mong đợi ở giao dịch khởi đầu của một thỏa thuận định kỳ.

INFO

setup_future_usage_cadence chỉ có thể là recurring hoặc unscheduled, và bỏ qua trường này nghĩa là unscheduled. Một giá trị mà phiên bản API này không nhận diện được sẽ hạ cấp về unscheduled thay vì bị từ chối — một thỏa thuận định kỳ sẽ không bao giờ vô tình được khai báo với mạng thẻ.


Bước 2 — Lấy mandate_id ​

Mandate được tạo bất đồng bộ, khi giao dịch ủy quyền lưu thẻ thành công. ID của nó được ghi nhận trên charge đã tạo ra nó, tại payment_method_details.card.mandate_id.

Charge không được bao gồm trong một PaymentIntent theo mặc định. Yêu cầu nó bằng expand:

GET /api/payment_intents/{id}?expand=payment_intent.latest_charge

expand là một danh sách chuỗi, không phải một giá trị đơn. Mỗi phần tử là một đường dẫn phân tách bằng dấu chấm, và bạn truyền một danh sách bằng cách lặp lại tham số một lần cho mỗi phần tử:

GET /api/payment_intents/{id}?expand=payment_intent.latest_charge&expand=some.other.path
Tham sốKiểuMô tả
expandstring[]Các đường dẫn sub-resource cần đưa vào response. Lặp lại tham số một lần cho mỗi đường dẫn. Hiện tại chỉ hỗ trợ payment_intent.latest_charge.

Mỗi đường dẫn được phân tách bằng dấu chấm tính từ resource mà nó thuộc về — payment_intent.latest_charge, không phải latest_charge — để các đường dẫn luôn rõ ràng khi có thêm sub-resource của sub-resource.

Một giá trị phân tách bằng dấu phẩy không phải là một danh sách

?expand=a,b được đọc là một đường dẫn duy nhất tên là a,b, không phải hai đường dẫn. Vì các đường dẫn không nhận diện được sẽ bị bỏ qua thay vì bị từ chối (xem bên dưới), request đó vẫn thành công nhưng âm thầm không expand được gì. Hãy lặp lại tham số thay vào đó.

Response

{
  "payment_intent": {
    "id": "pi_xxxxxxxx",
    "status": "succeeded",
    "customer_id": "cus_xxxxxxxx",
    "payment_method_id": "pm_xxxxxxxx",
    "latest_charge_id": "ch_xxxxxxxx",
    "latest_charge": {
      "id": "ch_xxxxxxxx",
      "status": "succeeded",
      "payment_method_details": {
        "type": "card",
        "card": {
          "brand": "visa",
          "last4": "4242",
          "network_transaction_id": "123456789012345",
          "mandate_id": "mand_xxxxxxxx"
        }
      }
    }
  }
}

Lưu mandate_id cùng với payment_method_id trên bản ghi subscription của bạn. Cả hai đều cần thiết để tính phí sau này.

Chờ thành công trước khi đọc

mandate_id chỉ được điền khi thanh toán đạt trạng thái succeeded. Hãy đọc PaymentIntent sau webhook payment_intent.status.succeeded thay vì ngay sau khi confirm — một intent ở trạng thái processing có thể chưa mang trường này.

Lưu ý về expand:

  • Một đường dẫn expand mà phiên bản API này không biết sẽ bị bỏ qua, không bị từ chối — một client được viết cho một bề mặt API mới hơn vẫn tiếp tục hoạt động.
  • latest_charge sẽ không xuất hiện nếu intent chưa có charge nào.
  • Cùng một card.mandate_id xuất hiện trên các charge MIT sau này, ở đó nó báo cáo mandate mà charge đó thực thi (exercised) thay vì mandate mà nó tạo ra — hữu ích để đối chiếu một lần gia hạn với thỏa thuận gốc của nó.

Bước 3 — Tính phí theo mandate (MIT) ​

Mỗi lần gia hạn hoặc tính phí lại không định kỳ là một PaymentIntent được xác nhận với off_session=true, chỉ rõ mandate mà nó thực thi.

POST /api/payment_intents

Request

{
  "amount": "10000",
  "currency": "usd",
  "customer_id": "cus_xxxxxxxx",
  "payment_method_id": "pm_xxxxxxxx",
  "mandate_id": "mand_xxxxxxxx",
  "confirm": true,
  "off_session": true,
  "description": "Pro plan — renewal"
}
TrườngBắt buộcMô tả
payment_method_idCóThông tin thanh toán đã lưu. Dữ liệu thẻ inline và confirmation token bị từ chối đối với thanh toán off-session.
off_sessionCótrue — không có khách hàng nào có mặt.
mandate_idKhôngMandate mà thanh toán này thực thi. Nếu bỏ qua, server sẽ coi charge này là không định kỳ và tự chọn một mandate — xem bên dưới.
confirmCótrue. mandate_id chỉ được đọc tại thời điểm confirm.

Một MIT không mang setup_future_usage — nó thực thi một thỏa thuận đã có sẵn thay vì tạo mới, và việc kết hợp cả hai sẽ bị từ chối. Nó cũng không cần mandate_data: sự đồng ý đã được ghi nhận một lần, bởi CIT ở Bước 1.

Bahn đọc mandate, phát lại thỏa thuận đã ghi nhận trên đó cho mạng thẻ, và thanh toán tiếp tục như bình thường — xem Thanh toán một lần để biết về ủy quyền, capture, và webhook.

Khi bạn bỏ qua mandate_id ​

Server sẽ coi charge này là không định kỳ và tự chọn một mandate theo chiến lược riêng của nó, dựa trên các thỏa thuận không định kỳ đang hoạt động của phương thức thanh toán đó. Các thỏa thuận định kỳ cố tình bị loại khỏi việc tìm kiếm này.

Mandate nào được chọn không phải là một phần của hợp đồng giao tiếp (API contract). Đừng phụ thuộc vào lựa chọn này: nếu có nhiều hơn một thỏa thuận không định kỳ tồn tại trên cùng một thẻ, mandate bạn nhận được có thể không phải là mandate bạn muốn.

Tình huốngCó cần truyền mandate_id?
Tính phí định kỳ / subscription / trả gópCó — bắt buộc trong thực tế. Cơ chế fallback không bao giờ chọn một thỏa thuận định kỳ, vì vậy một subscription bị bỏ mặc cho giá trị mặc định sẽ bị tính phí như không định kỳ, dưới sai thỏa thuận.
Nhiều thỏa thuận trên cùng một thẻ (ví dụ một subscription cộng với các lần nạp tiền tùy ý)Có. Chỉ rõ mandate bạn đang tính phí — cơ chế fallback không thể phân biệt chúng.
Tính phí thẻ lưu sẵn không định kỳ, chỉ đúng một thỏa thuận cho mỗi thẻTùy chọn. Chỉ có một ứng viên duy nhất, nên cơ chế fallback sẽ chọn đúng nó.

Việc chỉ rõ mandate luôn an toàn, và là lựa chọn mặc định được khuyến nghị.

Các mandate được tạo trước khi có tính năng này

Một mandate không ghi nhận marker nào cho mạng thẻ — mọi mandate được tạo trước khi thỏa thuận định kỳ được hỗ trợ — sẽ tiếp tục được coi là không định kỳ. Đây là hướng an toàn (một marker không định kỳ trên một khoản trả góp định kỳ là một sự hạ cấp mà các mạng thẻ chấp nhận; chiều ngược lại là một tranh chấp bạn sẽ thua), nhưng điều này có nghĩa là các subscription hiện có sẽ tiếp tục được tính phí như không định kỳ cho đến khi bạn thiết lập một mandate định kỳ mới.


Quy tắc & giới hạn ​

Thiết lập thỏa thuận (Bước 1):

  • setup_future_usage và payment_method_options.card.setup_future_usage_cadence chỉ áp dụng lúc tạo. ConfirmPaymentIntent bỏ qua payment_method_options; ConfirmPaymentIntentWithClientSecret không chấp nhận trường này.
  • setup_future_usage=off_session yêu cầu mandate_data.customer_acceptance — do bạn cung cấp lúc tạo, hoặc được ghi nhận tự động khi SDK xác nhận bằng client secret.
  • setup_future_usage yêu cầu một customer_id.
  • setup_future_usage không thể kết hợp với off_session=true, cũng không thể kết hợp với request_three_d_secure=never.
  • Một SetupIntent chỉ có thể tạo ra một mandate không định kỳ.
  • setup_future_usage=on_session kèm setup_future_usage_cadence=recurring khai báo thỏa thuận trên giao dịch ủy quyền nhưng không tạo mandate nào — việc tạo mandate chỉ diễn ra ở off-session. Dùng off_session khi bạn có ý định tính phí mà không có khách hàng.

Tính phí theo mandate (Bước 3):

  • mandate_id chỉ có thể được cung cấp trên lệnh gọi create-with-confirm (POST /api/payment_intents với confirm=true). ConfirmPaymentIntent không chấp nhận trường này.
  • mandate_id được gửi kèm confirm=false sẽ bị bỏ qua.
  • mandate_id trên một thanh toán on-session (off_session=false) sẽ bị từ chối — một mandate chỉ được thực thi khi khách hàng vắng mặt.
  • Bỏ qua mandate_id trên một thanh toán off-session sẽ không gây lỗi: server sẽ tự chọn một mandate không định kỳ theo chiến lược riêng, và mandate nào được chọn không được xác định trước.
  • Một mandate được chỉ rõ phải thuộc về payment_method_id đang được tính phí và phải ở trạng thái active, nếu không request sẽ bị từ chối.
  • Không có endpoint công khai nào để liệt kê hoặc đọc mandate. mandate_id chỉ đến với bạn qua charge, như ở Bước 2 — hãy lưu lại nó.

Lỗi ​

Điều kiệnResponse
setup_future_usage=off_session và không có mandate_data từ cả lệnh gọi create lẫn confirmmandate_data is required for setup_future_usage=off-session
setup_future_usage kèm off_session=truesetup_future_usage is not allowed for off-session payment
setup_future_usage kèm request_three_d_secure=neverrequest_three_d_secure=never is not allowed with setup_future_usage
setup_future_usage mà không có customer_idCustomer is required to save a payment method
Mandate được chỉ rõ không tồn tại, thuộc về một phương thức thanh toán khác, hoặc không activeno active mandate found for this payment method
Không có mandate_id nào được truyền và phương thức thanh toán không có mandate không định kỳ nào đang hoạt độngno active mandate found for this payment method
mandate_id được truyền kèm off_session=falsecannot provide mandate in on-session payment
off_session=true mà không có payment_method_idpayment_method_id is required for off-session

Xem Xử lý lỗi để biết về cấu trúc lỗi và hướng dẫn retry.

Webhook ​

Các charge MIT phát ra cùng các event như bất kỳ thanh toán nào khác — hãy coi webhook là kết quả chính thức.

EventMô tả
payment_intent.status.succeededCharge đã được capture. Đánh dấu lần gia hạn đã thanh toán.
payment_intent.payment_failedBị từ chối. Thử lại theo chính sách dunning của bạn, hoặc yêu cầu khách hàng cập nhật thẻ — điều này đồng nghĩa với việc thiết lập một mandate mới.
charge.capturedCharge đã được capture.

Developers