This guide takes you from a sandbox key to a successful payment, a refund and a verified webhook.
It takes about 15 minutes. Run the steps in order in one language; each step uses the ID returned
by the one before.
You need:
A VNSwitch sandbox account and its secret API key (dashboard → API keys).
A webhook URL set on your business profile and its payment response hash key
(dashboard → Webhooks). For local testing, use any HTTPS tunnel to your machine.
cURL, Node.js 18+, Python 3 with requests, or Java 17+.
exportVNSWITCH_API_KEY="<YOUR_API_KEY>"
// export VNSWITCH_API_KEY="<YOUR_API_KEY>" before running
constBASE_URL="https://sandbox.api.vnswitch.nivc.ai";constheaders={"Content-Type":"application/json","api-key":process.env.VNSWITCH_API_KEY,};
// export VNSWITCH_API_KEY="<YOUR_API_KEY>" before runningimportjava.net.URI;importjava.net.http.HttpClient;importjava.net.http.HttpRequest;importjava.net.http.HttpResponse;staticfinalStringBASE_URL="https://sandbox.api.vnswitch.nivc.ai";staticfinalHttpClientCLIENT=HttpClient.newHttpClient();staticStringsend(Stringmethod,Stringpath,Stringjson)throwsException{HttpRequest.Builderb=HttpRequest.newBuilder(URI.create(BASE_URL+path)).header("Content-Type","application/json").header("api-key",System.getenv("VNSWITCH_API_KEY"));b=json==null?b.method(method,HttpRequest.BodyPublishers.noBody()):b.method(method,HttpRequest.BodyPublishers.ofString(json));returnCLIENT.send(b.build(),HttpResponse.BodyHandlers.ofString()).body();}
Create a payment for 150,000 VND. VND has no minor unit, so amount is 150000
(
Amounts and currencies). With confirm: false the payment waits for a
payment method. Keep the returned payment_id: you use it in every later step.
If customer_id does not exist yet, VNSwitch creates the customer for you.
Attach the test card and authorise the payment. With capture_method: "automatic" the money is
captured straight away, so the status becomes succeeded.
Raw card numbers in server requests are fine in the sandbox. In production, sending card data through your servers puts them in PCI DSS scope. Talk to us about hosted card entry before you go live.
If the status is requires_customer_action, the card needs 3-D Secure: send the customer to the
URL in next_action (
Redirects and customer actions).
constconfirmRes=awaitfetch(`${BASE_URL}/payments/${payment.payment_id}/confirm`,{method:"POST",headers,body:JSON.stringify({payment_method:"card",payment_method_type:"credit",payment_method_data:{card:{card_number:"4242424242424242",card_exp_month:"12",card_exp_year:"2030",card_holder_name:"NGUYEN VAN AN",card_cvc:"123",},},}),});console.log((awaitconfirmRes.json()).status);
resp=requests.post(f"{BASE_URL}/payments/{payment['payment_id']}/confirm",headers=HEADERS,json={"payment_method":"card","payment_method_type":"credit","payment_method_data":{"card":{"card_number":"4242424242424242","card_exp_month":"12","card_exp_year":"2030","card_holder_name":"NGUYEN VAN AN","card_cvc":"123",}},})print(resp.json()["status"])
Your server decides whether an order is paid from this call or from a webhook, never from what
the browser says. See
Payment lifecycle for every status.
Refund 50,000 VND of the 150,000 VND payment. If you leave out amount, VNSwitch refunds the
full captured amount, so always send it for partial refunds. Refunds can finish later: the status starts as pending and you get a
refund_succeeded or refund_failed webhook. See
Refunds.
curl -X POST https://sandbox.api.vnswitch.nivc.ai/refunds \
-H "Content-Type: application/json"\
-H "api-key: $VNSWITCH_API_KEY"\
-d '{
"payment_id": "pay_8f2c1d9e0a",
"amount": 50000,
"reason": "Customer returned one item"
}'
constrefundRes=awaitfetch(`${BASE_URL}/refunds`,{method:"POST",headers,body:JSON.stringify({payment_id:payment.payment_id,amount:50000,reason:"Customer returned one item",}),});console.log(awaitrefundRes.json());
resp=requests.post(f"{BASE_URL}/refunds",headers=HEADERS,json={"payment_id":payment["payment_id"],"amount":50000,"reason":"Customer returned one item",})print(resp.json())
Stringrefund=send("POST","/refunds","""
{
"payment_id": "pay_8f2c1d9e0a",
"amount": 50000,
"reason": "Customer returned one item"
}
""");System.out.println(refund);
Response
{"refund_id":"ref_3b7e5a1c4d","payment_id":"pay_8f2c1d9e0a","amount":50000,"currency":"VND","status":"pending","reason":"Customer returned one item"}
When the payment succeeded in Step 2, VNSwitch sent a payment_succeeded event to your webhook
URL. Each webhook carries an X-Webhook-Signature-512 header: the lowercase hex HMAC-SHA512 of
the raw body, keyed with your payment response hash key.
Read the raw body before parsing it.
Compute the HMAC and compare it in constant time. Reject the request if it does not match.
Reply with a 2xx status quickly, then process the event. Use event_id to ignore duplicates.
// npm install express
importcryptofrom"node:crypto";importexpressfrom"express";constapp=express();constHASH_KEY=process.env.VNSWITCH_PAYMENT_RESPONSE_HASH_KEY;// Keep the raw body: the signature covers the exact bytes received.
app.post("/webhooks/vnswitch",express.raw({type:"application/json"}),(req,res)=>{constexpected=crypto.createHmac("sha512",HASH_KEY).update(req.body).digest("hex");constreceived=req.get("X-Webhook-Signature-512")||"";constok=received.length===expected.length&&crypto.timingSafeEqual(Buffer.from(received),Buffer.from(expected));if(!ok)returnres.status(401).end();constevent=JSON.parse(req.body);console.log(event.event_type,event.content.object.payment_id);res.status(200).end();// answer fast, process afterwards
});app.listen(3000);
importjava.nio.charset.StandardCharsets;importjava.security.MessageDigest;importjavax.crypto.Mac;importjavax.crypto.spec.SecretKeySpec;// Call this with the raw request body bytes and the X-Webhook-Signature-512 header.staticbooleanisValidSignature(byte[]rawBody,Stringreceived)throwsException{Stringkey=System.getenv("VNSWITCH_PAYMENT_RESPONSE_HASH_KEY");Macmac=Mac.getInstance("HmacSHA512");mac.init(newSecretKeySpec(key.getBytes(StandardCharsets.UTF_8),"HmacSHA512"));StringBuilderexpected=newStringBuilder();for(byteb:mac.doFinal(rawBody)){expected.append(String.format("%02x",b));// lowercase hex}returnreceived!=null&&MessageDigest.isEqual(expected.toString().getBytes(StandardCharsets.UTF_8),received.getBytes(StandardCharsets.UTF_8));}