VNSwitch can store a customer’s payment method so they do not have to enter it again, and so you
can charge it later without them, for subscriptions or instalments. You only ever handle a
reference to the stored method and masked details, never the full card number.
There are three patterns:
Pattern
Customer present?
What you send on the payment
Save a method for one-click checkout
Yes, now and later
setup_future_usage: "on_session" and customer_acceptance
Pay with a saved method
Yes
payment_token from the customer’s saved methods
Recurring, merchant-initiated charges
No
First mandate_data with setup_future_usage: "off_session", then recurring_details with off_session: true
Every pattern needs a
customer: pass the same customer_id on every payment.
The examples below send card details from a server to keep them short. Only do this if your
systems are certified to handle card data (PCI DSS). Otherwise collect the card in the browser
and confirm the payment there with the publishable key and the payment’s
client_secret (see Authentication). After the method
is saved, your server never needs the card number again.
Add setup_future_usage and customer_acceptance to a normal
payment. When the
payment succeeds, the card is saved to the customer and the response returns its
payment_method_id. If the payment needs 3-D Secure, the card is saved once the customer
completes it (see
Redirects and customer actions).
constres=awaitfetch("https://sandbox.api.vnswitch.nivc.ai/payments",{method:"POST",headers:{"Content-Type":"application/json","api-key":process.env.VNSWITCH_API_KEY,},body:JSON.stringify({amount:150000,currency:"VND",confirm:true,customer_id:"cus_demo_001",return_url:"https://merchant.example.vn/checkout/return",payment_method:"card",payment_method_data:{card:{card_number:"4242424242424242",card_exp_month:"12",card_exp_year:"2030",card_holder_name:"NGUYEN VAN AN",card_cvc:"123",},},setup_future_usage:"on_session",customer_acceptance:{acceptance_type:"online",accepted_at:"2026-11-01T08:20:00Z",online:{ip_address:"203.0.113.10",user_agent:"Mozilla/5.0 (Windows NT 10.0; Win64; x64)",},},}),});console.log(awaitres.json());
importosimportrequestsresp=requests.post("https://sandbox.api.vnswitch.nivc.ai/payments",headers={"api-key":os.environ["VNSWITCH_API_KEY"]},json={"amount":150000,"currency":"VND","confirm":True,"customer_id":"cus_demo_001","return_url":"https://merchant.example.vn/checkout/return","payment_method":"card","payment_method_data":{"card":{"card_number":"4242424242424242","card_exp_month":"12","card_exp_year":"2030","card_holder_name":"NGUYEN VAN AN","card_cvc":"123",}},"setup_future_usage":"on_session","customer_acceptance":{"acceptance_type":"online","accepted_at":"2026-11-01T08:20:00Z","online":{"ip_address":"203.0.113.10","user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64)",},},},)print(resp.json())
When the customer picks one, create the payment with that method’s payment_token and
payment_method. No card data is sent.
Tokens are short-lived. Fetch the list at the start of each checkout and do not store tokens.
If the saved method has requires_cvv: true, the customer must enter their CVC again. Collect
it in the browser and confirm the payment there with the publishable key and client_secret,
sending payment_method_data: { "card_token": { "card_holder_name": "…", "card_cvc": "…" } }
alongside the payment_token, so the CVC never reaches your server.
A mandate records the customer’s permission for you to charge them later. Create it on the
customer’s first payment, while they are present, by adding setup_future_usage: "off_session"
and mandate_data. The customer pays the first 150,000 VND now and may need to complete 3-D
Secure. The response returns the mandate_id; store it with the subscription.
constres=awaitfetch("https://sandbox.api.vnswitch.nivc.ai/payments",{method:"POST",headers:{"Content-Type":"application/json","api-key":process.env.VNSWITCH_API_KEY,},body:JSON.stringify({amount:150000,currency:"VND",confirm:true,customer_id:"cus_demo_001",description:"Gói thành viên tháng 11",return_url:"https://merchant.example.vn/checkout/return",payment_method:"card",payment_method_data:{card:{card_number:"4242424242424242",card_exp_month:"12",card_exp_year:"2030",card_holder_name:"NGUYEN VAN AN",card_cvc:"123",},},setup_future_usage:"off_session",mandate_data:{customer_acceptance:{acceptance_type:"online",accepted_at:"2026-11-01T08:20:00Z",online:{ip_address:"203.0.113.10",user_agent:"Mozilla/5.0 (Windows NT 10.0; Win64; x64)",},},mandate_type:{multi_use:{amount:1800000,currency:"VND",start_date:"2026-11-01T00:00:00Z",end_date:"2027-10-31T23:59:59Z",},},},}),});console.log(awaitres.json());
importosimportrequestsresp=requests.post("https://sandbox.api.vnswitch.nivc.ai/payments",headers={"api-key":os.environ["VNSWITCH_API_KEY"]},json={"amount":150000,"currency":"VND","confirm":True,"customer_id":"cus_demo_001","description":"Gói thành viên tháng 11","return_url":"https://merchant.example.vn/checkout/return","payment_method":"card","payment_method_data":{"card":{"card_number":"4242424242424242","card_exp_month":"12","card_exp_year":"2030","card_holder_name":"NGUYEN VAN AN","card_cvc":"123",}},"setup_future_usage":"off_session","mandate_data":{"customer_acceptance":{"acceptance_type":"online","accepted_at":"2026-11-01T08:20:00Z","online":{"ip_address":"203.0.113.10","user_agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64)",},},"mandate_type":{"multi_use":{"amount":1800000,"currency":"VND","start_date":"2026-11-01T00:00:00Z","end_date":"2027-10-31T23:59:59Z",}},},},)print(resp.json())
Run this from your billing job on each due date. The customer is not involved and no card data
is sent. The request must have:
confirm: true and off_session: true;
the same customer_id that set up the mandate;
recurring_details, which names what to charge:
recurring_details.type
recurring_details.data
Use it when
mandate_id
The mandate_id, for example man_7a4e2c9f1d
You set up a mandate with mandate_data. VNSwitch checks the mandate is active and the amount is within its limit.
payment_method_id
The payment_method_id, for example pm_5d1c9a7e3b
The method was saved with setup_future_usage: "off_session" and you track the agreement yourself.
Do not send recurring_details together with mandate_data or payment_token on the same
payment; the request is rejected.
Off-session charges can still fail, for example when the card has expired or the account has
no funds. Treat failed like any failed payment (see
Payment lifecycle),
listen for
webhooks, and ask the customer to update their method when needed.
curl -X POST https://sandbox.api.vnswitch.nivc.ai/payments \
-H "Content-Type: application/json"\
-H "api-key: <YOUR_API_KEY>"\
-d '{
"amount": 150000,
"currency": "VND",
"confirm": true,
"off_session": true,
"customer_id": "cus_demo_001",
"description": "Gói thành viên tháng 12",
"recurring_details": {
"type": "mandate_id",
"data": "man_7a4e2c9f1d"
}
}'
constres=awaitfetch("https://sandbox.api.vnswitch.nivc.ai/payments",{method:"POST",headers:{"Content-Type":"application/json","api-key":process.env.VNSWITCH_API_KEY,},body:JSON.stringify({amount:150000,currency:"VND",confirm:true,off_session:true,customer_id:"cus_demo_001",description:"Gói thành viên tháng 12",recurring_details:{type:"mandate_id",data:"man_7a4e2c9f1d"},}),});console.log(awaitres.json());
importosimportrequestsresp=requests.post("https://sandbox.api.vnswitch.nivc.ai/payments",headers={"api-key":os.environ["VNSWITCH_API_KEY"]},json={"amount":150000,"currency":"VND","confirm":True,"off_session":True,"customer_id":"cus_demo_001","description":"Gói thành viên tháng 12","recurring_details":{"type":"mandate_id","data":"man_7a4e2c9f1d"},},)print(resp.json())
importjava.net.URI;importjava.net.http.HttpClient;importjava.net.http.HttpRequest;importjava.net.http.HttpResponse;importjava.nio.charset.StandardCharsets;Stringbody="""
{
"amount": 150000,
"currency": "VND",
"confirm": true,
"off_session": true,
"customer_id": "cus_demo_001",
"description": "Gói thành viên tháng 12",
"recurring_details": { "type": "mandate_id", "data": "man_7a4e2c9f1d" }
}
""";HttpRequestrequest=HttpRequest.newBuilder(URI.create("https://sandbox.api.vnswitch.nivc.ai/payments")).header("Content-Type","application/json; charset=utf-8").header("api-key",System.getenv("VNSWITCH_API_KEY")).POST(HttpRequest.BodyPublishers.ofString(body,StandardCharsets.UTF_8)).build();HttpResponse<String>response=HttpClient.newHttpClient().send(request,HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));System.out.println(response.body());
Cards work as shown above. Linking a Vietnamese bank account or e-wallet as a saved method, and
charging it on a schedule, depends on the bank or provider supporting stored credentials and
on that provider being connected to VNSwitch.
Availability depends on the payment providers enabled for your account. Contact info@nivc.ai.
Before you build a bank-account subscription, call
List available payment methods with
recurring_enabled=true and check that the method you need is returned with
customer_acceptance_support set to supported.