EML ControlPay API documentation version 1.1
Document Status
This document is currently Active
Introduction
EML is a non-bank payment card issuer which processes a great number of transactions each day. For many card programs, EML systems are solely responsible for applying the rules which will determine if an incoming transaction is approved or declined.
EML is capable of delegating a portion of this decision to a program partner through EML ControlPay. This is a real-time process in which EML sends details of an online transaction to the partner, and the partner will have a short time window to process their own rules before responding with an approve or decline instruction. EML will honour the instruction where possible, and will complete the remainder of the processing for the transaction accordingly.
This is the specification for a REST API. The API is implemented and operated by you, the partner. EML's software will be the client of your API, and will connect to your API endpoint in order to delegate transaction decisions to you.
Reading This Documentation
At the bottom of this page, you will find a list containing all of the resources supported by this API.
Clicking on a resource, /approve
for example, will reveal more information about its supported HTTP methods. Clicking on a method, POST for example, will bring up more detailed information, usage instructions, and examples.
Delegated Decisions
There are a range of complexities, quirks, and anachronisms which complicate the process of issuing payment cards. When designing and implementing your program with delegated transaction decisions, there are some points you need to be aware of.
You have a time limit
Total transaction latency is constrained by the payment networks, and your API is given just a small slice of that total time. You should aim to respond as quickly as possible, but at most your responses must be received within three seconds. This includes network latency, re-transmits, etc.
If a response is not received within the time limit, EML will apply stand-in processing rules. (These can vary from program-to-program.)
Some hints for managing your time limit:
- EML utilises HTTP Keep-Alive in order to minimise per-transaction overheads. Make sure it is supported by your infrastructure.
- Avoid launched-on-first-request configurations so we are not waiting on your application startup.
- Make sure your API can handle multiple requests concurrently.
Error responses will trigger stand-in processing
Emitting any response other than a HTTP 200
in the correct format, will cause us to apply stand-in processing for a transaction. If your systems are encountering issues, you can use this mechanism to "fail fast" and return the transaction to EML for handling.
Online requests only
Unfortunately, not all transactions received from payment networks are online. There are offline and advice transactions which are pre-determined by the payment network, and EML has no immediate right to decline them. As there are no decisions to be made, these transactions are not delegated to your decision endpoint.
This does mean that some transaction activity can occur on an account without you being notified. To rectify this, you can implement a webhook for our Transaction Notification API, which publishes details of both online and offline transactions.
EML's rules still apply
Even if your API approves a transaction, EML may still choose to decline it if some of our rules are violated.
Some of the reasons why this may happen are:
- The account has insufficient funds to complete the transaction
- The account is not in an active state
- The account has exceeded a program velocity limit (i.e., too much spent in a short space of time)
- The transaction is blocked due to fraud risk
Final amount is determined on clearing
Transactions are sent to your decision API during their initial authorisation. The final settlement amount of a transaction is determined when it clears, not during its initial authorisation. Most of the time, the amount during authorisation and clearing will match, but there are conditions where the amount can be adjusted up or down. If you are tracking expenditure over time, you will need to account for these adjustments.
To address this, you can implement a webhook for our Transaction Notification API, which publishes details of transaction clearing.
Authentication
You can verify the integrity and the origin of messages delivered to your endpoint by checking their HMAC value. HMAC refers to hash-based message authentication code, a widely used scheme for message authentication through cryptography.
Your calculation should take the form of: HMAC_SHA256(key, data)
, where:
- key refers to the hmac_secret which is a bitstring of exactly 256 bits/32 bytes that has been securely exchanged with EML when configuring your organisation as an integration partner.
- data refers to the complete body of the received HTTP request. This is a bitstring which must match the original JSON payload and UTF-8 encoding of the request.
For each message sent to your endpoint, in addition to the hmac_value, we will also include the hmac_key_id of the key used to sign the message. Use it to look up the correct hmac_key_secret needed to validate the HMAC. This is useful for messages delivered in the moments immediately after updating your key, which may still be signed with your previous key.
Sample Implementations
.NET Framework
More information about HMACSHA256 here
using System.Globalization; using System.Security.Cryptography; class Program { void Main() { var messageBodyJsonString = @"<message body goes here>"; var secretHexString = "<secret key as a HEX string goes here>"; UTF8Encoding textEncoding = new UTF8Encoding(); //Get bytes from the message body using UTF8 encoding. var messageBodyBytes = textEncoding.GetBytes(messageBodyJsonString); //Convert secret key HEX string to bytes. var secretKeyBytes = ConvertHexStringToByteArray(secretHexString); //Create HMACSHA356 object from secret key. using (var hmac = new HMACSHA256(secretKeyBytes)) { //Compute the HMAC SHA256 hash using the message body bytes. var digest = hmac.ComputeHash(messageBodyBytes); var hex = new StringBuilder(digest.Length * 2); foreach (var b in digest) { hex.AppendFormat("{0:X2}", b); } //Here's the HMAC value that needs to be validated with the Auth header HMAC value. var hmacValue = hex.ToString(); Console.WriteLine(hmacValue); } } byte[] ConvertHexStringToByteArray(string hexString) { if (hexString.Length % 2 != 0) { throw new ArgumentException(String.Format(CultureInfo.InvariantCulture, "The hex string cannot have an odd number of digits: {0}", hexString)); } byte[] data = new byte[hexString.Length / 2]; for (int index = 0; index < data.Length; index++) { string byteValue = hexString.Substring(index * 2, 2); data[index] = byte.Parse(byteValue, NumberStyles.HexNumber, CultureInfo.InvariantCulture); } return data; } }
Java
More information on Mac here.
import java.util.Random; import java.util.ArrayList; import java.text.SimpleDateFormat; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.security.NoSuchAlgorithmException; import java.io.UnsupportedEncodingException; import java.security.InvalidKeyException; import javax.xml.bind.DatatypeConverter; import java.util.Arrays; public class Main { public static void main(String[] args) { String secretKey = "<secret key as a HEX string goes here>"; String message = "<message body goes here>"; try{ //Get bytes from the message body using UTF8 encoding. byte[] messageBytes = message.getBytes("UTF-8"); //Convert secret key HEX string to bytes. byte[] secretKeyBytes = DatatypeConverter.parseHexBinary(secretKey); SecretKeySpec secretKeySpec = new SecretKeySpec(secretKeyBytes, "HmacSHA256"); //Setup Mac with HMAC SHA256 algorithm. Mac hmac = Mac.getInstance("HmacSHA256"); hmac.init(secretKeySpec); //Here's the HMAC value that needs to be validated with the Auth header HMAC value. String generatedHash = new String(DatatypeConverter.printHexBinary(hmac.doFinal(messageBytes))); System.out.println(generatedHash); } catch(InvalidKeyException e){ System.out.println("InvalidKeyException thrown."); e.printStackTrace(); } catch(UnsupportedEncodingException e){ System.out.println("UnsupportedEncodingException thrown."); e.printStackTrace(); } catch(NoSuchAlgorithmException e){ System.out.println("NoSuchAlgorithmException thrown."); e.printStackTrace(); } } }
Ruby
More information on HMAC here.
require 'openssl' def hex_to_bin(s) s.scan(/../).map { |x| x.hex.chr }.join end messageBodyJsonString = "<message body goes here>" secretHexString = "<secret key as a HEX string goes here>" #Get bytes from the message body using UTF8 encoding. messageBodyUtf8 = messageBodyJsonString.encode("UTF-8") #Convert secret key HEX string to bytes. bstringSecret = hex_to_bin(secretHexString) #Here's the HMAC value that needs to be validated with the Auth header HMAC value. mac = OpenSSL::HMAC.hexdigest("SHA256", bstringSecret, messageBodyUtf8)
Node.js
More information on implementing HMAC here.
const cryptoModule = require('crypto'); const bufferModule = require('buffer'); var messageBodyJsonString = '<message body goes here>'; var secretKey = '<secret key as a HEX string goes here>'; var secretKeyHex = bufferModule.Buffer.from(secretKey, 'hex'); var hmac = cryptoModule.createHmac('sha256', secretKeyHex); hmac.update(messageBodyJsonString); //Here's the HMAC value that needs to be validated with the Auth header HMAC value. console.log(hmac.digest('hex').toUpperCase());
iOS/OSX
Use CommonCrypto/CommonHMAC