Create a Virtual Account via API

In this quickstart guide, you will create and pay into a virtual account created through the Payments API. This quickstart guide assumes a use case where you might generate a single use Virtual Account with a fixed payment amount. This use case is ideal for E-Commerce where you can generate a virtual account for each purchase.

Before you begin

  1. Register for a Xendit account by visiting our dashboard
  2. Generate a Secret API Key for development here or by visiting Settings > Developers > API Keys on the dashboard
  3. Make sure to set "Write" permissions to your API Key for Money-In products. For more details, please visit this guide

1. Create a Payment Request to generate a Virtual Account

Let's start by creating a Virtual Account for your customer to pay to. You can use the Payment Requests API provided by Xendit. This example uses the Axios library to perform a POST request to Xendit in JavaScript by defining a createVirtualAccountPayment function.

import axios from 'axios';

const authToken = Buffer.from('xnd_development_XXXX:').toString('base64');

async function createVirtualAccountPayment() {
    try {
        const { data, status } = await axios.post('https://api.xendit.co/payment_requests', 
            {
                currency: 'IDR',
                amount: 35000,
                reference_id: 'xendit_test_id_1',
                payment_method: {
                    type: 'VIRTUAL_ACCOUNT',
                    reusability: 'ONE_TIME_USE',
                    virtual_account: {
                        channel_code: 'BNI',
                        channel_properties: {
                            customer_name: 'Ahmad Gunawan',
                            expires_at: '2023-09-27 18:00:00.000'
                        }
                    }
                }
            },
            {
                headers: {
                    'Authorization': `Basic ${authToken}`
                }
            }
        )
}

The table below gives an explanation of each parameter in the request and the reason for specifying it. Make sure to replace the secret key in the example with your own secret key.

ParameterRequiredDescription
currencyYesThe currency of the virtual account. For IDR payment in Indonesia, we specify IDR
amountNoSpecifies the exact amount the virtual account can accept. For our E-Commerce use case, we would like our VA to accept a specific amount that equals the value of the product our customer is purchasing
reference_idNoAn ID set by you to uniquely identify each payment method. For example, "123"
payment_method.typeYesThe type of payment method. In this case, Virtual Account
payment_method.reusibilityYesWhether the Virtual Account can accept multiple payments or only one. For our E-Commerce use case, we will restrict usage to only a single payment for this virtual account
payment_method.virtual_account.channel_codeYesSpecifies the bank with which the Virtual Account is created. This will typically be the bank that the end customer would like to pay with. In this example, we use BNI.
payment_method.virtual_account.amountNoSpecifies the exact amount the virtual account can accept. For our E-Commerce use case, we would like our VA to accept a specific amount that equals the value of the product our customer is purchasing
payment_method.virtual_account.channel_properties.customer_nameYesThe name of the customer paying you. This must exactly match the name the customer has specified on their bank account. In this example, we used Ahmad Gunawan
payment_method.virtual_account.channel_properties.expires_atNoSpecify the time at which the virtual account will expire. This allows the virtual account number to be reused for other transactions

For each payment method creation, Xendit responds to you with the status of the payment and additional parameters. To view this, you can log the response returned from the axios POST request.

console.log(`Response returned with a status of ${status}`);

const { payment_method: { id, virtual_account: { channel_properties: { virtual_account_number } } }} = data;

console.log(`Virtual Account created with number ${virtual_account_number} and Payment Method ID ${id}`)

In the above snippet, the virtual_account_number parameter has been extracted. This is the virtual account which the end user should transfer money to. Along with this, the created payment_method.id field has also been extracted, which you will need to simulate payment in the next step. For further reading on the response returned from the Payment Requests API, please visit here.

In your checkout page, you should display this number to the end customer so that they can complete their payment.

Putting it all together, your final script should look like this:

import axios from 'axios';

const authToken = Buffer.from('xnd_development_XXXX:').toString('base64');

async function createVirtualAccountPayment() {
    try {
        const { data, status } = await axios.post('https://api.xendit.co/payment_requests', 
            {
                currency: 'IDR',
                amount: 35000,
                reference_id: 'xendit_test_id_1',
                payment_method: {
                    type: 'VIRTUAL_ACCOUNT',
                    reusability: 'ONE_TIME_USE',
                    virtual_account: {
                        channel_code: 'BCA',
                        channel_properties: {
                            customer_name: 'Ahmad Gunawan',                    
                        }
                    }
                }
            },
            {
                headers: {
                    'Authorization': `Basic ${authToken}`
                }
            }
        )

        console.log(`Response returned with a status of ${status}`);

        const { payment_method: { id, virtual_account: { channel_properties: { virtual_account_number } } }} = data;

        console.log(`Virtual Account created with number ${virtual_account_number} and Payment Method ID ${id}`)
    } catch (error) {
        console.log("Request failed")
    }
}

createVirtualAccountPayment()

After running this code (you may use NodeJS or Express to do this), you should see a successful created virtual account number being logged to the console.

> Response returned with a status of 201
> Virtual Account created with number 381659999480605 and Payment Method ID pm-442e7029-6622-4ef2-9eec-7a55b097ad00

2. Handle the Callback for Successful Payment

Once payment is completed by the customer, Xendit will send you a callback notifying successful payment. In this step, you will define a function to handle this callback sent from Xendit.

In the script below, a simple express server serves a POST endpoint /receive_callback. This is the endpoint Xendit will send the callback to.

import express, { json } from 'express';

const app = express();

app.use(json())

const PORT = process.env.PORT || 3000;

app.post('/receive_callback', async(req, res) => {
    const { body } = req;
    if (body.data.status === 'SUCCEEDED') {
        console.log(`Payment Successfull with status ${body.data.status} and id ${body.data.id}`)
    }
    res.sendStatus(200).end()
})

app.listen(PORT, () => console.log(`App listening at port ${PORT}`));
ParameterAlways PresentDescription
body.data.statusYesStatus of the payment. Will equal "SUCCEEDED" when payment is successful
body.data.idYesID of the payment request. Can be used to perform further actions such as Refunds

For a detailed list of parameters provided in the payment callback, please visit our API reference page. This page also provides additional details on how to handle failed payment callbacks.

3. Set the Callback URL on the Xendit Dashboard

Now that you have defined your callback handler, it is time to set the callback URL on Xendit's Dashboard. Ideally, this URL would lead to a production deployment of your server. For this example, it is possible to use the free tool ngrok to publicly host a localhost server.

> ngrok http 3000

Session Status                online                                                                                                                                                                      
Account                       abc@gmail.com (Plan: Free)                                                                                                                                            
Update                        update available (version 3.3.0-beta, Ctrl-U to update)                                                                                                                     
Version                       3.0.4                                                                                                                                                                       
Region                        Asia Pacific (ap)                                                                                                                                                           
Latency                       22ms                                                                                                                                                                        
Web Interface                 http://127.0.0.1:4040                                                                                                                                                       
Forwarding                    https://XXXX-121-6-44-41.ngrok-free.app -> http://localhost:3000     

You can then set the callback URL (https://XXXX-121-6-44-41.ngrok-free.app in this case) on the Xendit Dashboard. Click here and navigate to Payment Request > Payment Succeeded (New Payments API) to Test and Save this URL.

3. Simulate a Virtual Account Payment to your Payment Method

At this point, you would display the created virtual_account_number to your end user on your E-Commerce checkout page. Your customer would then continue on to payment through the BCA app or website. However, for testing, Xendit's simulation endpoint can be used to simulate payment.

> curl https://api.xendit.co/v2/payment_methods/pm-442e7029-6622-4ef2-9eec-7a55b097ad00/payments/simulate -X POST \
   --user xnd_development_XXXXXX: \
   --header 'Content-Type: application/json' \
   --data-raw '{
    "amount": 35000
}' \

Make sure to replace the user field with your API key and the path parameter with your newly created Payment Method ID from step 1. You should receive the response below

{
  "message": "We're processing payment for payment method ID 'pm-442e7029-6622-4ef2-9eec-7a55b097ad00' and will send you the result via callback. Please make sure you've set a callback URL in 'Payment Succeeded' section in Callback settings in Xendit Dashboard. If you don't receive the callback within the next 5 minutes, please contact us.",
  "status": "PENDING"
}

4. Verify that Callback was received

Now, if you go back to the server that we've been running (and perhaps hosting through ngrok), you should see a console log indicating a successful payment.

> Payment Successfull with status SUCCEEDED and id 64672654fe87ba392f68cb46

Wrapping up

You've managed to successfully integrate to Xendit by creating a Virtual Account and simulating payment. You'e also managed to verify successful payment by verifying a successful callback sent by Xendit.

Feel free to explore the following for further reading,

Last Updated on 2023-06-15