Accept Card Payments with APIs

Another way to accept card payments is by using our Card Payment APIs. The detailed guide below will show you how to successfully charge cards on Korapay using our APIs.


Pre-requisites

  1. Before you start, ensure that your account on Korapay is activated.
  2. Card payments need to be enabled on your account before you can start accepting such payments. Reach out to our team at [email protected] with a request to allow card payments on your account.
  3. To make use of our Card APIs, you are also required to be PCI DSS certified (Level 1).

More information about PCI DSS Compliance

The Payment Card Industry Data Security Standard (PCI DSS) is a set of global security standards designed to ensure that all entities that process, store or transmit cardholder data and/or sensitive authentication data maintain a secure environment. PCI DSS sets a baseline level of protection for consumers and helps reduce fraud and data breaches across the entire payment ecosystem. It is applicable to any organization that accepts or processes payment cards.

To learn more about PCI DSS compliance, visit this FAQ, or visit the official PCI website.

Get started with accepting Card Payments using APIs

We currently only support payments in Nigerian Naira. Also, we support the following card schemes: Mastercard, VISA, Verve. Here are some test cards we created to help you simulate different scenarios for card payments as you integrate.

Step 1: Collect payment data required to charge card

After collecting the necessary card and payment information from your customer, prepare your data object to look like the example shown below. In this payload, you may choose to pass your user’s PIN or not.

{
    "reference": "test-card-payment-1", // must be at least 8 characters
    "card": {
      	"name": "Test Cards",
        "number": "5130000052131820",
        "cvv": "419",
        "expiry_month": "12",
        "expiry_year": "32",
        "pin": "0000" // optional
    },
    "amount": 1000,
    "currency": "NGN",
    "redirect_url": "https://merchant-redirect-url.com",
    "customer": {
        "name": "John Doe",
        "email": "[email protected]"
    },
    "metadata": {
       "internalRef": "JD-12-67",
       "age": 15,
       "fixed": true,
    }
}

Here are the request parameters:

ParameterTypeRequiredDescription
referenceStringTrueA unique reference for the payment. The reference must be at least 8 characters long.
cardObjectTrueThe details of the card you want to charge
card.nameStringFalseThe name on the credit/debit card. Required for KES card payments.
card.numberStringTrueThe card number/PAN on the credit/debit card
card.cvvStringTrueThe CVV on the credit/debit card
card.expiry_monthStringTrueThe expiry month on the credit/debit card
card.expiry_yearStringTrueThe expiry year on the credit/debit card
card.pinStringFalseThe PIN for the credit/debit card
amountNumberTrueThe amount for the charge
currencyStringTrueThe currency for the charge
redirect_urlStringFalseA URL to which we can redirect your customer after their payment is complete
customerObjectTrueThe information of your customer you want charge
customer.emailStringTrueThe email of your customer
customer.nameStringFalseThe name of your customer
metadataObjectFalseOptional - It takes a JSON object with a maximum of 5 fields/keys. Empty JSON objects are not allowed.

Each field name has a maximum length of 20 characters. Allowed characters: A-Z, a-z, 0-9, and -.

Step 2: Encrypt payment data

Korapay ensures the complete security of card data in transit by using AES-256 encryption. The stringified payment data from Step 1 must be encrypted with your encryption key before making requests to the Charge API. Your encryption key can be found under the API configuration tab in the Settings page of your dashboard.

You can find some sample code snippets below showing a function that handles AES-256 encryption.

// Javascript
const crypto = require("crypto");

function encryptAES256(encryptionKey, paymentData) {  
  const iv = crypto.randomBytes(16);

  const cipher = crypto.createCipheriv('aes-256-gcm', encryptionKey, iv);
  const encrypted = cipher.update(paymentData);

  const ivToHex = iv.toString('hex');
  const encryptedToHex = Buffer.concat([encrypted, cipher.final()]).toString('hex');
  
  return `${ivToHex}:${encryptedToHex}:${cipher.getAuthTag().toString('hex')}`;
}
// PHP
function encryptAES256($encryptionKey, $paymentData) {
     $method = "aes-256-gcm";
    $iv = openssl_random_pseudo_bytes(16);
  	$tag = "";
    $cipherText = openssl_encrypt($paymentData, $method, $encryptionKey, OPENSSL_RAW_DATA, $iv, $tag, "", 16);
    return bin2hex($iv).':'.bin2hex($cipherText).':'.bin2hex($tag);
}
#Python
# run pip install pycryptodome to use the GCM Mode
import json
from Crypto.Cipher import AES
from Crypto import Random
import base64
from binascii import hexlify as hexa

def encryptAES256(encryptionKey, paymentData):
     try:
         iv = Random.get_random_bytes(16)
         encObj = AES.new(encryptionKey.encode("utf8"), AES.MODE_GCM, iv)
         cipherText,authTag = encObj.encrypt_and_digest(paymentData.encode("utf8"))
         iv64 = base64.b64encode(iv).decode('ascii')
         ivToHex = hexa(iv).decode()
         cipherTextToHex = hexa(cipherText).decode()
         authTagToHex = hexa(authTag).decode()
         result = ivToHex + ":" + cipherTextToHex + ":" + authTagToHex
         return result
     except Exception as e:
         print(e)
// Java
import java.security.SecureRandom;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;

public class Encryption {
    private static final char[] HEX_ARRAY = "0123456789abcdef".toCharArray();

    private static String bytesToHex(byte[] bytes) {
        char[] hexChars = new char[bytes.length * 2];
        for (int j = 0; j < bytes.length; j++) {
            int v = bytes[j] & 0xFF;
            hexChars[j * 2] = HEX_ARRAY[v >>> 4];
            hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
        }
        return new String(hexChars);
    }

    private static byte[] encryptDataWithAes(byte[] plainText, byte[] aesKey, byte[] aesIv) throws Exception {
            GCMParameterSpec gcmSpec = new GCMParameterSpec(128, aesIv);
            Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
            SecretKeySpec   secretKeySpec = new SecretKeySpec(aesKey, "AES");
            cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, gcmSpec);
            byte[] cipherText = cipher.doFinal(plainText);

            return cipherText;
   }

    public static String encryptPayload(String payload, String key) throws Exception {
            SecureRandom r = new SecureRandom();

            byte[] ivBytes = new byte[16];
            r.nextBytes(ivBytes);

            byte[] keyBytes   = key.getBytes(StandardCharsets.UTF_8);
            byte[] inputBytes = payload.getBytes(StandardCharsets.UTF_8);
            byte[] encryptedBytes = encryptDataWithAes(inputBytes, keyBytes, ivBytes);
            
            byte[] cipherTextBytes = Arrays.copyOfRange(encryptedBytes, 0, payload.length());
            byte[] authTagBytes = Arrays.copyOfRange(encryptedBytes, payload.length(), encryptedBytes.length);
        
            String ivHex = bytesToHex(ivBytes);
            String encryptedHex = bytesToHex(cipherTextBytes);
            String authTagHex = bytesToHex(authTagBytes);
                        
            String result = new StringBuilder()
                    .append(ivHex)
                    .append(":")
                    .append(encryptedHex)
                    .append(":")
                    .append(authTagHex)
                    .toString();
            return result;
    }
}
using System;
using System.Security.Cryptography;
using System.Text;
using Org.BouncyCastle.Crypto.Modes;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Crypto.Engines;

public static class Encryption
	{
		private static readonly char[] HEX_ARRAY = "0123456789abcdef".ToCharArray();
		private static string BytesToHex(byte[] bytes)
		{
			char[] hexChars = new char[bytes.Length * 2];
			for (int i = 0; i < bytes.Length; i++)
			{
				int v = bytes[i] & 0xFF;
				hexChars[i * 2] = HEX_ARRAY[v >> 4];
				hexChars[i * 2 + 1] = HEX_ARRAY[v & 0x0F];
			}

			return new string (hexChars);
		}

		private static byte[] EncryptDataWithAes(byte[] plainText, byte[] aesKey, byte[] aesIv)
		{
			var cipher = new GcmBlockCipher(new AesEngine());
			var keyParam = new KeyParameter(aesKey);
			var ivParam = new ParametersWithIV(keyParam, aesIv);
			cipher.Init(true, ivParam);
			byte[] cipherText = new byte[cipher.GetOutputSize(plainText.Length)];
			int len = cipher.ProcessBytes(plainText, 0, plainText.Length, cipherText, 0);
			cipher.DoFinal(cipherText, len);
			return cipherText;
		}

		public static string EncryptPayload(string payload, string encryptionKey)
		{
			using RandomNumberGenerator r = RandomNumberGenerator.Create();
			byte[] ivBytes = new byte[16];
			r.GetBytes(ivBytes);
			byte[] keyBytes = Encoding.UTF8.GetBytes(encryptionKey);
			byte[] inputBytes = Encoding.UTF8.GetBytes(payload);
			byte[] encryptedBytes = EncryptDataWithAes(inputBytes, keyBytes, ivBytes);
			byte[] cipherTextBytes = encryptedBytes[..payload.Length];
			byte[] authTagBytes = encryptedBytes[payload.Length..];
			string ivHex = BytesToHex(ivBytes);
			string encryptedHex = BytesToHex(cipherTextBytes);
			string authTagHex = BytesToHex(authTagBytes);
			string result = $"{ivHex}:{encryptedHex}:{authTagHex}";
			return result;
		}
	}

Step 3: Charge card

After encrypting the payment data, make a POST request along with the encrypted payload to our charge card endpoint. See example:

curl --request POST \
     --url https://api.korapay.com/merchant/api/v1/charges/card \
     --header 'Authorization: Bearer YOUR_KORAPAY_SECRET_KEY' \
     --data '{
        "charge_data": c62ac600880756fa9456e812dbbaccc8:14ed28362057615390765db7eb6a9f4630f12d23c67ad462621cd7e8cc:f2197fe1911162010cefa3038a12188f}'

After making a successful request to the charge card endpoint, the status of the transaction in the response sent will determine the next step. The status can either be success, failed or processing.

If the status of the transaction is either success or failed, it is important to verify the payment to confirm the final status of the transaction. An example response can be found below.

{
    "status": true,
    "message": "Card charged successfully",
    "data": {
        "amount": 200,
        "amount_charged": 200,
        "auth_model": "NO_AUTH",
        "currency": "NGN",
        "fee": 2.6,
        "vat": 0.2,
        "response_message": "Card charged successfully",
        "payment_reference": "api-payment-1",
        "status": "success",
        "transaction_reference": "KPY-CA-qW10KFoPEa",
        "card": {
            "card_type": "mastercard",
            "first_six": "539983",
            "last_four": "8381",
            "expiry": "10/31"
        }
    }
}

If the transaction status is processing and the authmodel is present in the payload, this means an authorization step is required. The _auth_model returned in the data object would determine the type of authentication required for the card transaction.

Step 4: Authorize card transaction

There are multiple ways a card can be charged depending on its authentication model (or auth_model).

Charging a Card that Requires PIN

When making the initial request to charge a card, you can choose to include the card PIN in the request payload. If the card PIN is not included in the initial request but is required to authorize the card transaction, we would return a response data with the status as processing and auth_model as PIN as seen in the sample response below:

{
    "status": true,
    "message": "Charge in progress",
    "data": {
        "amount": 200,
        "amount_charged": 200,
        "auth_model": "PIN",
        "currency": "NGN",
        "fee": 2.6,
        "vat": 0.2,
        "response_message": "Card PIN required",
        "payment_reference": "api-payment-2",
        "status": "processing",
        "transaction_reference": "KPY-CA-aZvnjyUKQJ2r",
        "card": {
            "card_type": "mastercard",
            "first_six": "539983",
            "last_four": "8381",
            "expiry": "10/31"
        },
        "authorization": {
          "required_fields": ["pin"]
        }
    }
}

Next, when you get a response like this, simply collect the card PIN and make a request to our authorization card transaction endpoint with the card PIN and transaction reference as shown in the sample request payload below:

{
    "transaction_reference": "KPY-CA-APQsjG6hu3O",
    "authorization": {
        "pin": "0000"
    }
}

After making a successful request to the authorize charge endpoint, the status of the transaction in the response will determine the next step. This status can either be success, failed or processing.

If the status of the transaction is either success or failed, it is important to verify the payment to confirm the final status of the transaction.


Charging a Card that Requires OTP Authorization

After making the request to charge/authorize the card, if the status returned in the data object is processing and auth_model is OTP, this means an OTP has been sent to the phone/email tied to the customer’s bank account. You would need to collect the OTP in order to authorize the transaction.

What you need to do next is to collect the OTP sent to the customer’s phone/email and make a request to our authorize charge endpoint with the OTP and the transaction reference as shown in the sample request payload below:

{
    "transaction_reference": "KPY-CA-M2MnFT7uLDW5",
    "authorization": {
        "otp": "12345"
    }
}

Charging a Card that Requires 3DS Authorization

Based on the initial request made to charge/authorize the card, we automatically detect that 3DS authorization is required and immediately return the auth_model as 3DS and a redirect URL in the response as shown in the sample below:

{
    "status": true,
    "message": "Charge in progress",
    "data": {
        "amount": 1100,
        "amount_charged": 1100,
        "auth_model": "3DS",
        "payment_reference": "KPY-PAY-efy45pZCSkVg",
        "currency": "NGN",
        "fee": 14.3,
        "vat": 1.07,
        "response_message": "You would be redirected to a secure page to complete your authorization",
        "status": "processing",
        "transaction_reference": "KPY-CM-vNJsGZNBpNqD",
        "authorization": {
            "redirect_url": "https://checkout.korapay.com/charge-card/3ds/KPY-CM-vNJsGZNBpNqD"
        },
        "card": {
            "card_type": "mastercard",
            "first_six": "539983",
            "last_four": "8381",
            "expiry": "10/31"
        }

    }
}

You're required to redirect your customer to the Redirect URL that is returned in the authorization object. They will need to provide the OTP sent to their phone/email to authorize the transaction.

Once the transaction is complete, we will redirect your customer to the provided redirect_url in the initial request to charge the card if available.


Charging a Card that Requires AVS Authorization

When customers are paying with a card that uses the Address Verification System, we detect this automatically after the charge card request is made and return a response with the auth_model as AVS and status as processing.

What you need to do when you get a response like this is to collect the address details from the customer and make a request to our authorization card transaction endpoint with a request payload as shown in the sample request below:

{
    "transaction_reference": "KPY-CM-WqmUdKiFcfvH",
    "authorization": {
        "avs": {
            "state": "Lagos",
            "city": "Lekki",
            "country": "Nigeria",
            "address": "Osapa, Lekki",
            "zip_code": "101010"
        }
    }
}

If the status of the transaction at this point is either success or failed, verify the payment to confirm the final status of the transaction but if the transaction status is processing, this means an extra authorization step is required and the auth_model returned in the data object should be used to determine the next authorization type required for the card transaction.


Charging a Card that Requires Phone Enrollment (Verve Cards)

After making the request to charge the card, if the status returned in the data object is processing and auth_model is CARD_ENROLL, this means the customer’s card has not been enrolled for online payment and the enrollment would be required to proceed with the transaction.

What you need to do next when you get a response like this is to collect the phone number tied to the bank account and make a request to our authorization card transaction endpoint with the phone number collected and transaction reference as shown in the sample request below:

{
    "transaction_reference": "KPY-CA-EYskF4mguh4Q",
    "authorization": {
        "phone": "2348000000000"
    }
}

After making the request successfully, you would get a response on how to proceed. The status of the transaction (which can be found in the data object) at this point can either be success, failed or processing.

If the status of the transaction is either success or failed, verify the payment to confirm the final status of the transaction. But if the transaction status is processing, this means an extra authorization step is required and the auth_model returned in the data object should be used to determine the next authorization type required for the card transaction.


Step 5: Verify payment

After charging a card, it’s IMPORTANT that you make a verification request from your server to our verify charge endpoint to confirm the final status of the payment. The reference used here should be your payment reference.

Here's a sample request and response for verifying a card payment:

curl https://api.korapay.com/merchant/api/v1/charges/:reference
-H "Authorization: Bearer YOUR_KORAPAY_SECRET_KEY"
-X GET
{
   "status": true,
   "message": "Charge retrieved successfully",
   "data": {
       "reference": "KPY-PAY-o7ERgRnpdx3e",
       "status": "success",
       "amount": "2000.00",
       "amount_paid": "2000.00",
       "fee": "180.00",
       "currency": "NGN",
       "description": "Card payment for a book",
       "card": {
            "card_type": "mastercard",
            "first_six": "539983",
            "last_four": "8381",
            "expiry": "10/31"
        }
   }
}
{
   "status": true,
   "message": "Charge retrieved successfully",
   "data": {
       "reference": "KPY-PAY-rYF4c5ZWioeb",
       "status": "failed",
       "amount": "2000.00",
       "amount_paid": 0,
       "fee": "180.00",
       "currency": "NGN",
       "description": "Card payment for a book",
       "payment_attempts": [
         {
           "reference": "KPY-CM-unEr27sJ616S",
           "status": "failed",
           "channel": "card",
           "message": "You are not allowed to make this payment with your card, please contact your bank or financial institution"
         }
       ],
       "card": {
            "card_type": "mastercard",
            "first_six": "539983",
            "last_four": "8381",
            "expiry": "10/31"
        }
   }
}

Step 6: Setup Webhook

You can set your application to receive a confirmation via webhooks when a card payment is successful. Please visit Webhooks to see more information about the webhook request body and how to verify and handle the webhook request.


Error Responses

Korapay’s RESTful API uses conventional HTTP response codes to indicate the success or failure of requests. Error responses from our card APIs come in the format below:

{
    "status": false,
    "message": "Payment has already been completed"
    "data": {}
}

Our error response includes a message key that gives more details on the error encountered.

Invalid request data
This error occurs when the request is sent with invalid data, more details of the error can be found in the data object which is also sent back as a response. Try the request again once the errors returned in the data object are resolved.

Card not supported
This error occurs when the card type sent in the request to charge card is not supported. Supported cards are Mastercard, VISA and Verve.

Transaction not found
This error occurs when you try to authorize a transaction with a transaction reference that does not exist.

Payment has already been completed
This error occurs when you try to authorize an already completed payment.

Transaction has already been completed
This error occurs when you try to authorize an already completed transaction.

Duplicate payment reference
This error occurs when the reference sent in the request to charge card has already been used for a previous payment.

Authorization is not required for this transaction
This error occurs when you try to authorize a transaction that does not require authorization.

Unable to decrypt charge data, please check encryption and try again
This error occurs when the encrypted data sent to the charge card endpoint could not be decrypted.

Service currently unavailable
This error means we could not successfully reach the service meant to process the request so you can re-query the payment to get a final status or you can report this to us.

An error occurred, please contact support
This error does not indicate any error with your request, so you can re-query the payment to get a final status or you can report this to us.

An error occurred, please try again
This error does not indicate any error with your request, so you can re-query the payment to get a final status or you can report this to us.

Internal server error. It would be nice if you report this to us
This response does not indicate any error with your request, so you can re-query the payment to get a final status or you can report this to us.


Failed Transaction Messages

The transaction message gives details on the reason a transaction failed. Below is a list of the various failed transaction messages:

Transaction has been flagged as fraudulent
This message is returned by the card issuer when the transaction does not check out and a temporary hold is placed. Kindly ask the customer to contact their bank or financial institution.

Incorrect card PIN
This message is returned when the PIN supplied to validate the transaction is incorrect.

Card could not be charged due to insufficient funds
This message is returned when the cardholder does not have enough funds to pay the specified transaction amount.

We are unable to complete this transaction at this time, please try again later
This message is returned by the card issuer when they do not accept the transaction. This can also be caused when a transaction for the same amount is attempted multiple times quickly for the same card or because of an invalid format or field in the transaction details.

The OTP entered is invalid
This message is returned when the OTP supplied to authorize the transaction is incorrect.

Withdrawal limit exceeded
The message is returned when the card issuer has declined the transaction as it will exceed the customer’s card limit. The customer should use an alternate debit card.

Card record was not found
The message is returned when the card issuer has declined the transaction because the debit card number does not exist. The customer should use an alternate card.

An error occurred, please try again
This message is returned when either the next party in the card transaction is unreachable or we could not get a definite reason for failure. The customer should try again.

Refer to Financial Institution
This is an error from the card issuer which indicates that there’s a problem with the card or cardholder account.

Card could not be charged due to bank restrictions. Please contact your bank or financial institution
This message is returned by the card issuer and indicates that there’s a problem with the card or cardholder account.

Card not enrolled for online payment. Please contact your bank or financial institution
This message is returned by the card issuer and indicates that the card has not been enabled to be used for online payments.

Withdrawal frequency exceeded
The message is returned when the card issuer has declined the transaction as it will exceed the customer's card withdrawal limit. The customer should use an alternate debit card.

Pin tries exceeded
This message is returned when the customer enter's the incorrect PIN multiple times and has maxed out the number of PIN attempts allowed to validate the transaction.

Otp attempts exceeded
This message is returned when the customer enter's the incorrect OTP multiple times and has maxed out the number of OTP attempts allowed to authorize the transaction.

The amount requested is above the limit permitted by your bank, please contact your bank or financial institution
The message is returned when the card issuer has declined the transaction as it will exceed the customer's transaction limit.

You are not allowed to make this payment with your card, please contact your bank or financial institution
This message is returned by the card issuer and indicates that there's a problem with the card or cardholder account.

Issuer or Switch Inoperative
This means that the customer's bank or card network is temporarily unavailable due to a downtime.