Implementing Refund of Payment


This guide provides how to implement Iamport’s payment refund feature. Payment refund consists of 6 steps until the merchant client requests a refund and receives the response. The image below is a diagram of the process.
STEP1Request a refund
The merchant client requests a refund (POST) to the merchant server. At this point, the refund amount, the reason for the refund, and the order number will be sent together. The order number is used to query the payment information for the refund from the merchant database.

In case of a virtual account payment, additional account information (account holder, account number, bank code) to be refunded must be provided. The additional parameters to refund a virtual account payment can be found in
  1. Additional parameters to refund payments on a virtual account.
  <button onclick="cancelPay()">Refund</button>
  <script
    src="https://code.jquery.com/jquery-3.3.1.min.js"
    integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
    crossorigin="anonymous"></script><!-- jQuery CDN --->
  <script>
    function cancelPay() {
      jQuery.ajax({
        "url": "http://www.myservice.com/payments/cancel",
        "type": "POST",
        "contentType": "application/json",
        "data": JSON.stringify({
          "merchant_uid": "mid_" + new Date().getTime(), // Order number
          "cancel_request_amount": 2000, // Refund amount
          "reason": "Testing payment refund" // Reason for the refund
          "refund_holder": "Jone Doe", // [required to refund a virtual account payment] The recipient’s account holder for refund
          "refund_bank": "88" // [required to refund a virtual account payment] The recipient’s account bank code for refund (e.g. Shinhan Bank is 88 for KG Inicis)
          "refund_account": "56211105948400" // [required to refund a virtual account payment] The recipient’s account number for refund
        }),
        "dataType": "json"
      });
    }
  </script>
STEP2Acquire access token
The access token should be included in the request header to request a payment refund to the Iamport server. If the access token is not included or is invalid, the Iamport server returns 401.

You can get an access token (POST) using the Iamport REST API as shown below. More details for the access token can be found in REST API Access Token documentation.
  <!-- Node.js -->
  var express = require('express');
  var app = express();
  var axios = require('axios');
  /* ... code omitted here ... */
  app.post('/payments/cancel', async (req, res, next) => {
    try {
      /* Acquire access token */
      const getToken = await axios({
        url: "https://api.iamport.kr/users/getToken",
        method: "post", // POST method
        headers: { 
          "Content-Type": "application/json" 
        },
        data: {
          imp_key: "imp_apikey", // [Iamport Admin] REST API key
          imp_secret: "ekKoeW8RyKuT0zgaZsUtXXTLQ4AhPFW3ZGseDA6bkA5lamv9OqDMnxyeB9wqOsuO9W3Mx9YSJ4dTqJ3f" // [Iamport Admin] REST API Secret
        }
      });
      const { access_token } = getToken.data.response; // Access token
      /* Query payment Information */
      ...
    } catch (error) {
      res.status(400).send(error);
    }
  });
STEP3Query payment Information with order number
After receiving the access token from the Iamport server, the merchant server inquires the payment information for the refund from the merchant database (assuming that there is a table Payments that contains the payment information).
  /* ... model/payments.js ... */
  var mongoose = require('mongoose');
  var Schema = mongoose.Schema;
  ...
  var PaymentsSchema = new Schema({
    imp_uid: String, // Iamport unique ID (used as a unique number at refund request)
    merchant_uid: String, // order number (used when querying payment information)
    amount: { type: Number, default: 0 }, // Payment amount (used for calculating refundable amount)
    cancel_amount: { type: Number, default: 0 }, // Total amount refunded (used for calculating refundable amount)
    ...
  });
  ...
  module.exports = mongoose.model('Payments', PaymentsSchema);
The merchant server looks up the document (payment information) from the Payments table that have an order number which matches the order number received from the client through STEP 1 Request a refund.
  /* ... code omitted here ... */
  var Payments = require('./models/payments');
  app.post('/payments/cancel', async (req, res, next) => {
    try {
      /* Acquire access token */
      /* ... code omitted here ... */
      /* Query payment Information */
      const { body } = req;
      const { merchant_uid } = body; // Order number from the client
      Payments.find({ merchant_uid }, async function(err, payment) { 
        if (err) {
          return res.json(err);
        }
        const paymentData = payment[0]; // Retrieved payment information
        /* Request a refund to Iamport REST API */
        ...
      });
    } catch (error) {
      res.status(400).send(error);
    }
  });
STEP4Request a refund to Iamport REST API
The merchant server requests (POST) a refund payment to Iamport server, based on the refund information (refund amount, reason for refund) from the merchant client in STEP1 Request a refund and the payment information from the merchant database in STEP3 Query payment Information with order number. The table below describes the properties to be included in the request body when requesting a refund.
PropertyData TypeDescriptionrequired
imp_uidstringThe unique number in Iamport for the payment to be refunded(optional)

Either imp_uid or merchant_uid is required

imp_uid is taken if both are provided
merchant_uidstringThe unique number for the payment to be refunded(optional)

Either imp_uid or merchant_uid is required

imp_uid is taken if both are provided
amountdoubleRequested refund amount(optional)

Full refund if not provided

Refunds will be made for the amount specified
tax_freedoubleTax-free amount of requested refund amount(optional)

Set to 0 if not specified
checksumdoubleCancelable balance as of now before the cancellation transaction(optional)

Verify in advance whether the cancelable balance recorded by the API requester and the cancelable balance recorded by Iamport match, and if the verification fails, the transaction is not executed. If null, the verification process is skipped.
reasonstringReason for cancellation(optional)
refund_holderstringThe recipient’s account holder for refundrequired to refund a virtual account payment
refund_bankstringThe recipient’s account bank code for refundrequired to refund a virtual account payment
refund_accountstringThe recipient’s account number for refundrequired to refund a virtual account payment
1Unique number of refund
The unique refund number should be entered to specify the payment you want to refund. Either imp_uid or merchant_uid can be used as a unique refund number.

The priority of refund numbers

If both imp_uid and merchant_uid are provided as refund unique numbers, merchant_uid is ignored and imp_uid is used. Also, if specified imp_uid is not valid, refund will fail even if a correct merchant_uid is provided.
  /* ... code omitted here ... */
  app.post('/payments/cancel', async (req, res, next) => {
    try {
      /* Acquire access token */
      /* ... code omitted here ... */
      /* Query payment Information */
      const { body } = req;
      const { merchant_uid, reason } = body; // Order number from the client, reason for the refund
      Payments.find({ merchant_uid }, async function(err, payment) { 
        /* ... code omitted here ... */
        const paymentData = payment[0]; // Retrieved payment information
        const { imp_uid } = paymentData; // Extract imp_uid from the retrieved payment information
        /* Request a refund to Iamport REST API */
        const getCancelData = await axios({
          url: "https://api.iamport.kr/payments/cancel",
          method: "post",
          headers: {
            "Content-Type": "application/json",
            "Authorization": access_token // The access token issued from the Iamport server
          },
          data: {
            reason, // The reason for the refund received from the merchant client
            imp_uid, // Use imp_uid as a unique refund number
            ...
          }
        });
        const { response } = getCancelData.data; // Refund result
        /* Synchronize refund results */
        ...
      });
    } catch (error) {
      res.status(400).send(error);
    }
  });
2Refund amount
Specify the amount you want to refund. Specify the amount for the partial refund, or a full refund will be made.

Refund mobile micropayments

A partial refund is not possible for mobile micropayments due to the carrier’s policy. Only full refund is available.

The full refund is impossible either if the month of the payment and the month of the refund request are different. For example, payments made on January 31st are non-refundable on February 1st. Because the payment was made on January and the refund is requested on a different month, which is February.
  /* ... code omitted here ... */
app.post('/payments/cancel', async (req, res, next) => {
  try {
    /* Acquire access token */
    /* ... code omitted here ... */
    /* Query payment Information */
    const { body } = req;
    const { merchant_uid, reason, cancel_request_amount } = body; // Order number from the client, reason for the refund, refund amount
    Payments.find({ merchant_uid }, async function(err, payment) { 
      /* ... code omitted here ... */
      const paymentData = payment[0]; // Retrieved payment information
      const { imp_uid } = paymentData; // Extract imp_uid from the retrieved payment information
      /* Request a refund to Iamport REST API */
      const getCancelData = await axios({
        url: "https://api.iamport.kr/payments/cancel",
        method: "post",
        headers: {
          "Content-Type": "application/json",
          "Authorization": access_token // The access token issued from the Iamport server
        },
        data: {
          reason, // The reason for the refund received from the merchant client
          imp_uid, // Use imp_uid as a unique refund number
          amount: cancel_request_amount, // The requested refund amount from the merchant client
          ...
        }
      });
      const { response } = getCancelData.data; // Refund result
      /* Synchronize refund results */
      ...
    });
  } catch (error) {
    res.status(400).send(error);
  }
});
3Refundable amount (checksum)
Specify the refundable amount (checksum). For example, the checksum of a 10000 won product is 10000 won. If a 10000 won product has been partially refunded in the past with 1000 won, the checksum is 9000 won for a subsequent refund.

The specified checksum checks whether the refundable amount recorded by the merchant server matches the refundable amount recorded by the Iamport server. If they do not match, the refund request will fail. If no specified, the verification process will be skipped.

checksum is not mandatory, but it is recommended in case the merchant server and Iamport server are not synchronized.

Reasons to provide checksum

Suppose you want to partially refund a 10000 won product. Iamport server succeeded in refund, but what if the merchant did not reflect this due to a server or database error? The checksum of the Iamport server would be 9000, and the checksum of the merchant server would still be 10000. If you enter checksum(10000) later when you want to refund the remaining amount, the Iamport server recognizes that checksum(9000) is different and fails the refund.
  /* ... code omitted here ... */
  app.post('/payments/cancel', async (req, res, next) => {
    try {
      /* Acquire access token */
      /* ... code omitted here ... */
      /* Query payment Information */
      const { body } = req;
      const { merchant_uid, reason, cancel_request_amount } = body; // Order number from the client, reason for the refund, refund amount
      Payments.find({ merchant_uid }, async function(err, payment) { 
        /* ... code omitted here ... */
        const paymentData = payment[0]; // Retrieved payment information
        const { imp_uid, amount, cancel_amount } = paymentData; // Extract imp_uid, amount (payment amount), cancel_amount (the total refund amount) from the retrieved payment information
        const cancelableAmount = amount - cancel_amount; // The refundable amount (= payment amount - total refunded amount)
        if (cancelableAmount <= 0) { // if already refunded in full
          return res.status(400).json({ message: "This order has already been fully refunded." });
        }
        ...
        /* Request a refund to Iamport REST API */
        const getCancelData = await axios({
          url: "https://api.iamport.kr/payments/cancel",
          method: "post",
          headers: {
            "Content-Type": "application/json",
            "Authorization": access_token // The access token issued from the Iamport server
          },
          data: {
            reason, // The reason for the refund received from the merchant client
            imp_uid, // Use imp_uid as a unique refund number
            amount: cancel_request_amount, // The requested refund amount from the merchant client
            checksum: cancelableAmount // [Recommended] Specify refundable amount
          }
        });
        const { response } = getCancelData.data; // Refund result
        /* Synchronize refund results */
        ...
      });
    } catch (error) {
      res.status(400).send(error);
    }
  });
4Additional parameters to refund a virtual account payment
In order to refund a virtual account payment, you must join a PG’s virtual account special service.

Reasons to join the virtual account special service

Unlike credit cards, the payment/refund free is excluded for the refund of virtual account payments. For example, the merchant receives a settlement of 9,700 won from the PG company excluding the payment fee of 300 won for the original payment of 10000 won. 300 won will be charged separately as a refund fee. Therefore, the merchant must pay PG 10300 won (the settlement of 9700 won + the payment fee of 300 won + the refund fee of 300 won). To prevent any confusion that may occur during this process, PG provides virtual account refunds only to merchants that joined the virtual account special service.
In the case of a virtual account refund, account holder, account number, and bank code of recipient are required.

The payment refunds for credit cards, real-time account transfers, and mobile micropayment cancel the approved transactions. In case of the virtual accounts, on the other hand, is one-way payment method where the recipient of refund is unknown. Therefore, the recipient information (account holder, account number, bank code) for the refund must be provided.

The bank codes vary by PG

Since the virtual account bank code is different for each PG company, a different code must be used for a different PG even if the bank is the same. For example, the code of Shinhan Bank is 88 for KG Inicis, but BK88 for KCP.

The virtual account bank codes for PG companies can be found in the code table (required for virtual account refunds) at the bottom of Iamport API documentation. Currently, work is underway to unify the virtual account bank code into the 3-digit standard code provided by KFTC and will be updated in the future.
  /* ... code omitted here ... */
  app.post('/payments/cancel', async (req, res, next) => {
    try {
      /* Acquire access token */
      /* ... code omitted here ... */
      /* Query payment Information */
      const { body } = req;
      const { merchant_uid, reason, cancel_request_amount, refund_holder, refund_bank, refund_account } = body; // Order number from the client, reason for the refund, refund amount, recipient information (account holder, account number, bank code)
      Payments.find({ merchant_uid }, async function(err, payment) { 
        /* ... code omitted here ... */
        const paymentData = payment[0]; // Retrieved payment information
        const { imp_uid, amount, cancel_amount } = paymentData; // Extract imp_uid, amount (payment amount), cancel_amount (the total refund amount) from the retrieved payment information
        const cancelableAmount = amount - cancel_amount; // The refundable amount (= payment amount - total refunded amount)
        if (cancelableAmount <= 0) { /// if already refunded in ful
          return res.status(400).json({ message: "This order has already been fully refunded." });
        }
        ...
        /* Request a refund to Iamport REST API */
        const getCancelData = await axios({
          url: "https://api.iamport.kr/payments/cancel",
          method: "post",
          headers: {
            "Content-Type": "application/json",
            "Authorization": access_token // The access token issued from the Iamport server
          },
          data: {
            reason, // The reason for the refund received from the merchant client
            imp_uid, // Use imp_uid as a unique refund number
            amount: cancel_request_amount, // The requested refund amount from the merchant client
            checksum: cancelableAmount, // [Recommended] Specify refundable amount
            refund_holder, // [required to refund a virtual account payment] The recipient’s account holder for refund
            refund_bank, // [required to refund a virtual account payment] The recipient’s account bank code for refund (e.g. Shinhan Bank is 88 for KG Inicis)
            refund_account // [required to refund a virtual account payment] The recipient’s account number for refund
          }
        });
        const { response } = getCancelData.data; // Refund result
        /* Synchronize refund results */
        ...
      });
    } catch (error) {
      res.status(400).send(error);
    }
  });
Request with additional parameters to refund payments on a virtual account. If successful, the PG will transfer the refund amount to the account the next day. This generally takes about one working day.
STEP5Synchronize refund results
The merchant server should reflect (synchronize) the results to the merchant database after the refund is complete through Iamport REST API. The order number is extracted from the refund result, and the document (payment information) that matches the order number is found and updated from the Payments table in merchant database.
  /* ... code omitted here ... */
  app.post('/payments/cancel', async (req, res, next) => {
    try {
      /* Acquire access token */
      /* ... code omitted here ... */
      /* Query payment Information */
      Payments.find({ merchant_uid }, async function(err, payment) { 
        /* ... code omitted here ... */
        /* Request a refund to Iamport REST API */
        /* ... code omitted here ... */
        const { response } = getCancelData.data; // Refund result
        /* Synchronize refund results */
        const { merchant_uid } = response; // Extract the order information from the refund result
        Payments.findOneAndUpdate({ merchant_uid }, response, { new: true }, function(err, payment) { // Extract and synchronize the payment information that matches the order information
          if (err) {
            return res.json(err);
          }
          res.json(payment); // response to the client with the refund result
        });
      });
    } catch (error) {
      res.status(400).send(error);
    }
  });
STEP6Handling refund response
Write a logic to handle the response for the refund request from the merchant server based on the success or failure.
  <button onclick="cancelPay()">cancelPay</button>
  <script
    src="https://code.jquery.com/jquery-3.3.1.min.js"
    integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
    crossorigin="anonymous"></script><!-- jQuery CDN --->
  <script>
    function cancelPay() {
      jQuery.ajax({
        /* ... code omitted here ... */
      }).done(function(result) { // Refund is successful 
        alert("Refund succeeded");
      }).fail(function(error) { // Refund is failed
        alert("Refund failed");
      });
    }
  </script>