Introduction
There is heaps of excitement around digital finance following release of M-PESA API. Mobile payments for C2B online checkout. It's been a year since. As taunted there is expected developers innovation around this.I will provide a quick start guide for java developers geared towards integrating existing Java applications.
Pre-requisites
JDK 7 or latestInstruction steps
- Download Full M-PESA API Guide here
- Read through Developers Guide C2B_OnlineCheckout_V2.0.doc
- Generate JAX-WS artifacts . Your client will use this api to access the published web service.
Generate JAX-WS artifacts
- >> mkdir src
-
>> wsimport -XadditionalHeaders -clientjar safaricom-lipanampesa-onlinecheckout-wsdl.jar -s src -p ke.co.makara.integration.mpesa.lnmocheckout http://safaricom.co.ke/mpesa_online/lnmo_checkout_server.php?wsdl
- Check wsimport --help to understand above options;
- Optionally create source jar
>> jar cvf safaricom-lipanampesa-onlinecheckout-source-wsdl.jar ke/
Generated artifacts
- Service Endpoint Interface (SEI) - LNMOPortType.java
- Service class - LnmoCheckoutService.java
- If a wsdl:fault is present in the WSDL, an Exception class
- Java classes mapped from schema types eg ProcessCheckOutRequest.java
- If a wsdl:message is present in WSDL, asynchronous response beans eg ProcessCheckOutResponse.java
Web Service Client
- Create a java class say LNMOCheckoutTester
- In the Java client application, create an instance of the LnmoCheckoutService service
LnmoCheckoutService lnmoCheckoutService = new LnmoCheckoutService(); // lina na mpesa online checkout instance
- The Service class will be created during the build.
- Obtain a proxy to the service from the service using the getLnmoCheckout() method
LNMOPortType soap = lnmoCheckoutService.getLnmoCheckout();
- Configure the service endpoint
((BindingProvider)soap).getRequestContext().put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, url + "lnmo_checkout_server.php"); // specifying this property on the RequestContext
- Compose our client request M-PESA checkout message
ProcessCheckOutRequest checkOutRequest = objFactory.createProcessCheckOutRequest();
checkOutRequest.setMERCHANTTRANSACTIONID("54635469064");
checkOutRequest.setREFERENCEID("TD346534GH");
checkOutRequest.setAMOUNT(13300);
checkOutRequest.setMSISDN("0721XXXXXX");
checkOutRequest.setENCPARAMS("");
checkOutRequest.setCALLBACKURL("https://makara.co.ke:8443/odt/checkout");
checkOutRequest.setCALLBACKMETHOD("GET");
checkOutRequest.setTIMESTAMP(String.valueOf(date.getTime()));
- Configure request headers
CheckOutHeader requestHeader = objFactory.createCheckOutHeader();
requestHeader.setMERCHANTID(MERCHANT_ID);
Date timestamp = new Date();
String encryptedPassword = getEncryptedPassword(MERCHANT_ID, PASSKEY, timestamp);
requestHeader.setPASSWORD(encryptedPassword.toUpperCase());
requestHeader.setTIMESTAMP(String.valueOf(timestamp.getTime()));
- Invoke the service endpoint with the dispatch client
- Process the response message from the service as per your business requirement
ProcessCheckOutResponse checkOutResponse = new ProcessCheckOutResponse();
checkOutResponse.getRETURNCODE();
checkOutResponse.getDESCRIPTION();
checkOutResponse.getTRXID();
checkOutResponse.getCUSTMSG();
- Tracing SOAP Traffic.
System.setProperty("com.sun.xml.internal.ws.transport.http.client.HttpTransportPipe.dump", "true");
- Updated 22/11/2016 : Source code
/*
* LNMOCheckoutTester.java
*
* Nov 20, 2016 Joseph Makara - Created File to tester Lina Na M-PESA Online checkout
*
*
*/
package testMe;
import java.io.*;
import java.security.*;
import java.util.*;
import javax.net.ssl.*;
import javax.xml.ws.*;
import ke.co.makara.integration.mpesa.lnmocheckout.*;
import org.apache.commons.codec.binary.*;
/**
* @author Joseph Makara
*
*/
public class LNMOCheckoutTester {
private static final String PASSKEY = "234fdsghfsg5654dgfhgf6dsfdsafsd43dgfhdgfdgfh74567";
private static final String MERCHANT_ID = "678fsgd54354";
private static final String REFERENCE_ID = "";
private static final String ENDPOINT_URL = "https://safaricom.co.ke/mpesa_online/";
private static final String CALLBACK_URL = "https://makara.co.ke:8443/odt/checkout";
private static final String CALLBACK_METHOD = "GET";
static {
HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
if (hostname.equals("safaricom.co.ke")) return true;
return false;
}
});
}
/**
* @param args
*/
public static void main(String[] args) {
LNMOPortType soap = outBoundLNMOCheckout(ENDPOINT_URL);
ObjectFactory objFactory = new ObjectFactory();
CheckOutHeader requestHeader = objFactory.createCheckOutHeader();
requestHeader.setMERCHANTID(MERCHANT_ID);
Date timestamp = new Date();
String encryptedPassword = getEncryptedPassword(MERCHANT_ID, PASSKEY, timestamp);
requestHeader.setPASSWORD(encryptedPassword);
requestHeader.setTIMESTAMP(String.valueOf(timestamp.getTime()));
ProcessCheckOutRequest checkOutRequest = objFactory.createProcessCheckOutRequest();
checkOutRequest = processCheckOut(timestamp);
soap.processCheckOut(checkOutRequest, requestHeader);
ProcessCheckOutResponse checkOutResponse = new ProcessCheckOutResponse();
checkOutResponse.getRETURNCODE();
checkOutResponse.getDESCRIPTION();
checkOutResponse.getTRXID();
checkOutResponse.getENCPARAMS();
checkOutResponse.getCUSTMSG();
}
private static ProcessCheckOutRequest processCheckOut(Date date){
ProcessCheckOutRequest checkOutRequest = new ProcessCheckOutRequest();
checkOutRequest.setMERCHANTTRANSACTIONID("54635469064");
checkOutRequest.setREFERENCEID("TD346534GH");
checkOutRequest.setAMOUNT(3.45);
checkOutRequest.setMSISDN("0721826284");
checkOutRequest.setENCPARAMS("");
checkOutRequest.setCALLBACKURL(CALLBACK_URL);
checkOutRequest.setCALLBACKMETHOD(CALLBACK_METHOD);
checkOutRequest.setTIMESTAMP(String.valueOf(date.getTime()));
return checkOutRequest;
}
/**
* Convert the concatenated string to bytes
* Hash the bytes to get arbitary binary data
* Convert the binary data to string use base64
*
* @param merchantId
* @param passkey
* @param date
* @return
*/
private static String getEncryptedPassword(String merchantId, String passkey, Date date) {
String encodedPassword = null;
StringBuilder builder = new StringBuilder(merchantId)
.append(passkey)
.append(date.getTime());
try {
String sha256 = getSHA256Hash(builder.toString());
return new String(Base64.encodeBase64(sha256.getBytes("UTF-8")));;
} catch (NoSuchAlgorithmException | UnsupportedEncodingException ex) {
ex.printStackTrace();
}
return encodedPassword;
}
private static LNMOPortType outBoundLNMOCheckout(String url) {
System.setProperty("com.sun.xml.internal.ws.transport.http.client.HttpTransportPipe.dump", "true");
LnmoCheckoutService lnmoCheckoutService = new LnmoCheckoutService();
LNMOPortType soap = lnmoCheckoutService.getLnmoCheckout();
((BindingProvider)soap).getRequestContext().put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY,
url + "lnmo_checkout_server.php");
return soap;
}
private static String getSHA256Hash(String input) throws NoSuchAlgorithmException {
MessageDigest mDigest = MessageDigest.getInstance("SHA-256");
byte[] result = mDigest.digest(input.getBytes());
StringBuffer sb = new StringBuffer();
for (int i = 0; i < result.length; i++) {
sb.append(Integer.toString((result[i] & 0xff) + 0x100, 16).substring(1));
}
return sb.toString();
}
}
Sample Soap Request
[HTTP request - https://safaricom.co.ke/mpesa_online/lnmo_checkout_server.php]---
Accept: text/xml, multipart/related
Content-Type: text/xml; charset=utf-8
SOAPAction: ""
User-Agent: JAX-WS RI 2.2.9-b130926.1035 svn-revision#5f6196f2b90e9460065a4c2f4e30e065b245e51e
<?xml version="1.0" ?>
<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
<S:Header>
<ns2:CheckOutHeader xmlns:ns2="tns:ns">
<MERCHANT_ID>F2678987M</MERCHANT_ID>
<PASSWORD>QQFSOITZB5OJMJTW073/SCWLN5WMDL6LO0FP6DJZ8TQ=</PASSWORD>
<TIMESTAMP>1479116220855</TIMESTAMP>
</ns2:CheckOutHeader>
</S:Header>
<S:Body>
<ns2:processCheckOutRequest xmlns:ns2="tns:ns">
<MERCHANT_TRANSACTION_ID>54635469064</MERCHANT_TRANSACTION_ID>
<REFERENCE_ID>TD346534GH</REFERENCE_ID>
<AMOUNT>3.45</AMOUNT>
<MSISDN>0721826284</MSISDN>
<ENC_PARAMS></ENC_PARAMS>
<CALL_BACK_URL>https://makara.co.ke:8443/odt/checkout</CALL_BACK_URL>
<CALL_BACK_METHOD>lnmo</CALL_BACK_METHOD>
<TIMESTAMP>1479116220855</TIMESTAMP>
</ns2:processCheckOutRequest>
</S:Body>
</S:Envelope>
Attachments
Soap Message Request
source code
client jar
source jar
Stock take and Future
- There is more Safaricom can do to streamline things, Create proper and complete API Guide ..
This developer guide provides a good head start for any one familiar with web services integration. Instead of ranting about horrible state of things get your hands dirty and do stuff
Community frustrations
Just How Open is Safaricom’s Open API?
Safaricom Integration Nightmare part 2
Java 6 wsimport limitation
The problem with Java 6 wsimport is that the JAX-WS runtime needs to fetch the WSDLs from the endpoint each time a service instance is created, which could incur a network overhead. The WSDL location is saved in the generated artifacts and the JAX-WS runtime fetches the metadata, which is useful if the endpoint policy or the service definition has changed. In the absence of the runtime fetch of the metadata, the clients would need to be regenerated if the endpoint policy or the service definition have changed.What is new in Java 7 wsimport?
Java 7 supports Java API for XML Web Services (JAX-WS) 2.2.4, which has introduced a new (since JAX-WS 2.2.2) wsimport option called - clientjar as shown in the following sample command:wsimport -clientjar wsclient.jar
http://example.com/service/hello?WSDL
The -clientjar option fetches the WSDLs and the schemas and packages them with the generated client-side artifacts into a JAR file. By including the generated JAR file in the classpath of the web service client, there is no need to fetch the WSDLs from the endpoint each time a service instance is created, thus saving on network overhead.