Direct API Integration

This section is intended for merchants that would like to create their own integration (which we assume not using the provided SDK that Indodana has provided).

Overview

Indodana provides a simple end-to-end checkout method for your e-commerce site. This checkout method allows your customer to make payment for their purchase with easy and secure verification installment process. By only inputting their mobile number and security PIN, they can finalize the purchase by choosing the tenure options provided for this service.

This is how Indodana Checkout process works:

Preparation

To get started, please make sure that you have the MERCHANT_API_KEY and MERCHANT_SECRET_KEY ready.

You also need to make sure that you have access to our sandbox & production API URLs. The sandbox API URL is located in https://sandbox01-api.indodana.com.

Here are preparation list you might required during this integration:

Authorizing Your Request

The important thing that you need to know when you want to perform a direct integration is the API authentication. By default, all of Indodana Merchant API endpoints are secured and you will need to authenticate your request. To be able to execute the API successfully, you will need to authorize yourself by presenting a valid Authorization header in your HTTP request.

To generate the Authorization header, please refer to the following snippets of code.

const crypto = require('crypto')

const apiKey = '<MERCHANT_API_KEY>'
const secret = '<MERCHANT_SECRET_KEY>'
const nonce = Math.floor(Date.now() / 1000);

const hmac = crypto.createHmac('sha256', secret);
  
var generateAuthorizationHeader = function () {
  const content = `${apiKey}:${nonce}`;
  
  var signature = null
  var authorization = ''
  hmac.on('readable', () => {
    const data = hmac.read();
    if (data) {
      signature = data.toString('hex')
      authorization = `Bearer ${apiKey}:${nonce}:${signature}`
    }
  });
  hmac.write(content);
  hmac.end();
  
  console.log('HTTP Authorization Header')
  console.log('Authorization: ' + authorization)
  
  return authorization
}
generateAuthorizationHeader()

As you can see, the content of Authorization header is generated by creating a sha256 hash of apiKey:nonce. The sha256 hash algorithm should take the MERCHANT_SECRET_KEY as the secret key. The example result of the above snippets when you try to ran the code should be as follow

HTTP Authorization Header
Authorization: Bearer sudahlahbossque:1562328997:091e834ec466df5e31b87077134e9d44ac428f515b78055673a2cfa65dbd5956

After you have successfully created the Authorization header, then you can proceed on the next step to develop the integration according to scenario:

  • Get installment options

  • Checkout transaction

  • Handling transaction confirmation notification

  • Check transaction status

  • Refund / cancel transaction

Get Installment Options

This scenario occurs when the customer needs to see the available installment option for a certain transaction. The available option will be determined by amount of the transaction. This is the snippet of code that is required to request payment simulation from Indodana Merchant API.

const Promise = require('bluebird')
const superagent = Promise.promisifyAll(require('superagent'));
const path = require('path')

// Variable authorization should contain value of : 
//    'Bearer <api-key>:<nonce>:<signature>'
var authorization = generateAuthorizationHeader()

var baseUrl = "https://sandbox01-api.indodana.com/chermes/merchant"
var endpoint = `${baseUrl}/v2/payment_calculation`

var payload = {
  "amount": 4500000,
  "items": [
    {
      "id": "<MERCHANT-ITEM-ID>",
      "name": "iPhone 6S",
      "category": "<ITEM-CATEGORY>",
      "price": 4500000,
      "url": "http://merchant.com/phone/iphone-6s",
      "imageUrl": "http://merchant.com/phone/iphone-6s/cover.jpg",
      "type": "Mobile Phone",
      "quantity": 1,
      "parentType": "SELLER",
      "parentId": "<MERCHANT-SELLER-ID>"
    }
  ]
}

superagent
    .post(endpoint)
    .set('Authorization', authorization)
    .send(payload)
    .endAsync()
    .then(response => {
      // Do something with response.body
      console.log(response.body)
    })
    .catch(err => {
      // Handle error
      console.log(err.response.text)
    });

As you can see from the snippet above, the first step is always to generate an Authorization header. Once you have the Authorization signature, you will need to send a POST HTTP request with Authorization header set to the desired endpoint which hosts the payment simulation API. In our case, it is pointed to sandbox https://sandbox01-api.indodana.com/chermes/merchant/v2/payment_calculations. After you have executed the POST HTTP request, you will need to verify whether the request has been completed successfully or not. The sample responses (successful & error responses) that are possibly returned by the API can be found in the section of API Reference > Sample API Request & Response > Get Installment Options.

Normally after you have successfully executed the get installment options request, you will need to show the available installment options to the customer on your website / mobile app.

Purchase Transaction Request

This scenario occurs after the customer successfully chose their preferred installment options. You will get the ID of the installment option such as 30_days, 3_months, 6_months, and 12_months, then you will need to perform the checkout process. This is the snippet of code that is required to perform the checkout process from Indodana Merchant API. You will need to prepare a unique merchantOrderId that Indodana will use as an identifier to notify the completion of the purchase.

const Promise = require('bluebird')
const superagent = Promise.promisifyAll(require('superagent'));
const path = require('path')

// Variable authorization should contain value of : 
//    'Bearer <api-key>:<nonce>:<signature>'
var authorization = generateAuthorizationHeader()

var baseUrl = "https://sandbox01-api.indodana.com/chermes/merchant"
var endpoint = `${baseUrl}/v2/checkout_url`

var payload = {
    "transactionDetails": {
      "merchantOrderId": '<MERCHANT-ORDER-ID>',
      "amount": 4500000,
      "items": [
        {
          "id": "<MERCHANT-ITEM-ID>",
          "name": "iPhone 6S",
          "category": "<ITEM-CATEGORY>",
          "price": 4500000,
          "url": "http://merchant.com/phone/iphone-6s",
          "imageUrl": "http://merchant.com/phone/iphone-6s/cover.jpg",
          "type": "Mobile Phone",
          "quantity": 1,
          "parentType": "SELLER",
          "parentId": "<MERCHANT-SELLER-ID>"
        }
      ]
    },
    "customerDetails": {
      "firstName": "First",
      "lastName": "Last",
      "email": "first.last@gmail.com",
      "phone": "081212345678"
    },
    "sellers": [
      {
        "id": "<MERCHANT-SELLER-ID>",
        "name": "Penjual iPhone",
        "url": "https://merchant.com/shop",
        "sellerIdNumber": "<MERCHANT-SELLER-ID-NUMBER>",
        "email": "seller@merchant.com",
        "address": {
          "firstName": "Merchant",
          "lastName": "Seller",
          "address": "Kelapa Gading",
          "city": "Jakarta Utara",
          "postalCode": "11240",
          "phone": "081812345678",
          "countryCode": "ID"
        }
      }
    ],
    "billingAddress": {
      "firstName": "First",
      "lastName": "Last",
      "address": "Tomang",
      "city": "Jakarta Barat",
      "postalCode": "11430",
      "phone": "081987654321",
      "countryCode": "ID"
    },
    "shippingAddress": {
      "firstName": "First",
      "lastName": "Last",
      "address": "Tomang",
      "city": "Jakarta Barat",
      "postalCode": "11430",
      "phone": "081987654321",
      "countryCode": "ID"
    },
    "paymentType": "3_months",
    "approvedNotificationUrl": "https://payment-notification.merchant.com/transaction-confirmation-handler",
    "cancellationRedirectUrl": "http://merchant.com/phone/iphone-6s",
    "backToStoreUrl": "http://merchant.com/phone/iphone-6s",
    "expirationAt": "2019-12-31T18:00:00+07:00"
}

superagent
    .post(endpoint)
    .set('Authorization', authorization)
    .send(payload)
    .endAsync()
    .then(response => {
      // Do something with response.body
      console.log(response.body)
    })
    .catch(err => {
      // Handle error
      console.log(err.response.text)
    });

Same with the installment option request, you will still need to generate an Authorization header. Once you have the Authorization signature, you will need to send a POST HTTP request with Authorization header set to the desired endpoint which hosts the payment simulation API. In our case, it is pointed to sandbox https://sandbox01-api.indodana.com/chermes/merchant/v2/checkout_url.

After you have successfully executed the POST HTTP request, supposedly you will get a response object containing a redirection URL (field redirectUrl). The redirection URL, when it's accessed, would contain a login page that can be used by the customer to log in to their Indodana account using their Phone Number & PIN. The sample responses (successful & error responses) that are possibly returned by the API can be found in the section of API Reference > Sample API Request & Response > Purchase Transaction Checkout.

This is the screenshot of the login page that will be presented to the customer.

Please be aware that in our current workflow, we recommend the merchant to ensure that the customer would already have a Credit Limit Approval from Indodana before they access the redirection URL.

Once the customer login using their phone number and PIN, they will be presented to the transaction review page. The customer will be able to choose different tenures for their installment and finally confirm their purchase. The credit limit of the customer will be deducted once they authorized the purchase.

Handling Transaction Confirmation Notification

The next scenario that merchant needs to handle is the transaction confirmation notification. Once the customer has confirmed the purchase, Indodana will notify the merchant by calling the endpoint specified in the approvedNotificationUrl field of the Checkout Transaction Request Payload.

In this scenario, merchant will need to ensure that the endpoint that is specified in approvedNotificationUrl is valid and accessible. You will normally required to create an HTTP server that will receive the payload from Indodana and then handle the subsequent transaction flow.

Below is an example code which implements the server that you need to develop to handle the transaction confirmation. We assume that this code will be served through endpoint : https://payment-notification.merchant.com/transaction-approval-handler

It is recommended for you to be able to monitor and keep the log of the notification in your system for cross-checking / debugging whenever any issue happens.

We also encourage you to design an idempotent operation when you handle the transaction. Indodana will make sure that you acknowledge once that you have received a notification. If you fail to acknowledge, Indodana will keep sending the notification to you repeatedly until you successfully acknowledge by sending the proper response.

In order to validate that the notification is coming from Indodana, we encourage you to validate the request by checking the signature sent from Indodana in the authorization header. Please check code below for the example

const express = require('express');
const bodyParser = require('body-parser');
const crypto = require('crypto');
const _ = require('lodash');
// Activate JSON body parsing
const app = express();
app.use(bodyParser.json());

/**
 * Function to validate authorization header from Indodana.
 * It will return true if the authorization header is valid, otherwise will return false
 *
 * @param {string} authorizationHeader
 */
const validateAuthorizationHeader = (authorizationHeader) => {
  // Separate Auth Token from Bearer
  // authorizationHeader = Bearer <MERCHANT-API-KEY>:<NONCE>:<AUTHORIZATION-SIGNATURE>
  // authorizationValue = <MERCHANT-API-KEY>:<NONCE>:<AUTHORIZATION-SIGNATURE>
  const authorizationValue = _.chain(authorizationHeader)
    .split('Bearer')
    .last()
    .trim()
    .value();
  // Header format: <MERCHANT-API-KEY>:<NONCE>:<AUTHORIZATION-SIGNATURE>"
  const authorizationData = authorizationValue.split(':');
  const apiKey = authorizationData[0];
  const nonce = authorizationData[1];
  const indodanaSignature = authorizationData[2];
  const selfSignature = generateSelfSignature({
    apiKey: '<MERCHANT_API_KEY>',
    apiSecret: '<MERCHANT_API_SECRET>',
    nonce: nonce
  });
  return selfSignature === indodanaSignature;
};

/**
 * Function to generate signature to validate Indodana signature
 *
 * @param {Object} params
 * @param {string} params.apiKey
 * @param {string} params.apiSecret
 * @param {number} params.nonce
 */
const generateSelfSignature = ({ apiKey, apiSecret, nonce }) => {
  const hmac = crypto.createHmac('sha256', apiSecret);
  const content = `${apiKey}:${nonce}`;
  let signature = null;
  
  hmac.on('readable', () => {
    const data = hmac.read();
    if (data) {
      signature = data.toString('hex');
    }
  });
  hmac.write(content);
  hmac.end();
  
  return signature;
};

/**
 * Method to handle Indodana transaction confirmation notification
 * Indodana expect this method to response with status "OK" if transaction can be processed,
 * or "REJECT" if the transaction cannot be processed
 *
 * Indodana might hit this API multiple times, so please make sure this API is idempotent.
 */
app.post('/transaction-confirmation-handler', (req, res) => {
  // Do something with the confirmed transaction
  // Normally you will need to mark that the transaction is paid
  //    and proceed to the issuance / completing the transaction
  // This is example of the POST request that will be sent by Indodana
  // Headers:
  // {
  //   Authorization: "Bearer <MERCHANT-API-KEY>:<NONCE>:<AUTHORIZATION-SIGNATURE>"
  // }
  //
  // Body:
  // {
  //    "amount": 3500000.00,
  //    "paymentType": "3_months",
  //    "transactionStatus": "INITIATED" // Either "PAID" or "REJECTED",
  //    "merchantOrderId": "<MERCHANT-ORDER-ID>",
  //    "message": "Transaction status is processed",
  //    "transactionTime": "2019-09-12T18:18:18+07:00"
  //    "transactionId": "<INDODANA-TRANSACTION-ID>",
  // }
  
  // Validate request, make sure it is from Indodana
  const headersValidationResult = validateAuthorizationHeader(req.headers.authorization);
  
  if (headersValidationResult === true) {
    var notification = req.body;
    // Do something with the notification
    console.log(notification);
    // This is the ACK response that you will need to send to Indodana
    // to notify Indodana that you have successfully received the transaction confirmation
    const response = {
      status: 'OK',
      message: 'Confirm message from merchant if any'
    };
    
    return res.send(JSON.stringify(response));
  } else {
    const response = {
      status: 'REJECT',
      message: 'Signature does not match'
    };
    
    return res.send(JSON.stringify(response));
  }
});

The examples of expected request and expected response that will be provided by Indodana when sending the notification can be found in the section of API Reference > Sample API Request & Response > Handling Transaction Confirmation Notification.

Check Transaction Status

In times of operating, there are several times that you may need to provide / check the status of the transaction (e.g: at the time of customer inquiry, or when you need to synchronize the state of the transaction). Indodana has provided an API endpoint for the merchants to easily check their transaction status.

There are several transaction statuses that the merchant needs to be aware of:

  • INITIATED - Transaction has been forwarded to Indodana

  • EXPIRED - Customer failed to complete the transaction

  • PAID - Transaction passed the fraud detection and is confirmed to-be-paid by Indodana

  • PROCESSED - Transaction has been verified by customer and forwarded to merchant

  • CANCELLED - Merchant cancelled the transactions / refund the transactions

  • REJECTED - If the transactions is identified as fraudulent transactions or in blacklisted merchants

It's really simple to check your transaction status. This is the code that will be required to perform the transaction status check.

const Promise = require('bluebird')
const superagent = Promise.promisifyAll(require('superagent'));
const path = require('path')

// Variable authorization should contain value of : 
//    'Bearer <api-key>:<nonce>:<signature>'
var authorization = generateAuthorizationHeader()

var baseUrl = "https://sandbox01-api.indodana.com/chermes/merchant"
var endpoint = `${baseUrl}/v1/transactions/check_status`
  
var payload = {
  "merchantOrderId": "<MERCHANT-ORDER-ID>"
}

superagent
    .get(endpoint)
    .set('Authorization', authorization)
    .query(payload)
    .send()
    .endAsync()
    .then(response => {
      // Do something with response.body
      console.log(response.body)
    })
    .catch(err => {
      // Handle error
      console.log(err.response.text)
    });

The sample responses (successful & error responses) that are possibly returned by the API can be found in the section of API Reference > Sample API Request & Response > Check Transaction Status.

Purchase Transaction Cancellation / Refund

Merchant is able to void / cancel a completed transaction. Note that, there are two types of cancellation. The first is FULL CANCELLATION - when the amount that is posted in the Refund / Cancellation request is the same as the transaction amount. The second is PARTIAL CANCELLATION - when the amount that is posted in the Refund / Cancellation request is different / less than the transaction amount.

It is required for merchant to include the cancellation reason, the preferred cancellation reason that given to Indodana are:

  • Expired - when the seller does not give a response for the transaction after the predetermined time

  • Out of Stock - when the seller does not give a response for the transaction after the predetermined time

  • Items Returned - when the customer has cancelled the transaction because there was an issue with the item

  • Others - any other reason beside the three above

Full Cancellation

const Promise = require('bluebird')
const superagent = Promise.promisifyAll(require('superagent'));
const path = require('path')

// Variable authorization should contain value of : 
//    'Bearer <api-key>:<nonce>:<signature>'
var authorization = generateAuthorizationHeader()

var baseUrl = "https://sandbox01-api.indodana.com/chermes/merchant"
var endpoint = `${baseUrl}/v3/order_cancellation`
  
var payload = {
  "refundId":"<MERCHANT-REFUND-ID>",
  "merchantOrderId":"<MERCHANT-ORDER-ID>",
  "cancellationAmount" : 100000,
  "cancellationReason":"Out of stock",
  "cancelledBy":"<SELLER/CUSTOMER>",
  "cancellationDate":"2019-09-12T18:18:18+07:00"
}

superagent
    .post(endpoint)
    .set('Authorization', authorization)
    .send(payload)
    .endAsync()
    .then(response => {
      // Do something with response.body
      console.log(response.body)
    })
    .catch(err => {
      // Handle error
      console.log(err.response.text)
    });

The sample responses (successful & error responses) that are possibly returned by the API can be found in the section of API Reference > Sample API Request & Response > Refund / Purchase Transaction Cancellation / Refund.

Partial Cancellation

const Promise = require('bluebird')
const superagent = Promise.promisifyAll(require('superagent'));
const path = require('path')

// Variable authorization should contain value of : 
//    'Bearer <api-key>:<nonce>:<signature>'
var authorization = generateAuthorizationHeader()

var baseUrl = "https://sandbox01-api.indodana.com/chermes/merchant"
var endpoint = `${baseUrl}/v3/order_cancellation`
  
var payload = {
  "refundId":"<MERCHANT-REFUND-ID>",
  "merchantOrderId":"<MERCHANT-ORDER-ID>",
  "cancellationAmount" : 50000,  
  "cancellationReason":"Out of stock",
  "cancelledBy":"<SELLER/CUSTOMER>",
  "cancellationDate":"2019-09-12T18:18:18+07:00"
}

superagent
    .post(endpoint)
    .set('Authorization', authorization)
    .send(payload)
    .endAsync()
    .then(response => {
      // Do something with response.body
      console.log(response.body)
    })
    .catch(err => {
      // Handle error
      console.log(err.response.text)
    });

The sample responses (successful & error responses) that are possibly returned by the API can be found in the section of API Reference > Sample API Request & Response > Refund / Purchase Transaction Cancellation / Refund.

Last updated