I am trying to setup the Royal Mail Shipping API (if anyone has any experience of this i'd be grateful if you could assist).
In the documentation they provide I need to download a certificate (a .p12 file) & import this onto my Windows machine - this is pretty straightforward using the 'Certificate Import Wizard'. Once it gets to the "Set Security Level' I must select High & this will request permission with a password each time this is used.
In Internet Explorer in 'Internet Options' within the Content tab I can view the Certificates and can clearly see that this certificate has been imported and hasn't expired.
The next step is to extract the certificate components, here I have to run the three following commands using OpenSSL to generate the .pem files.
$ openssl pkcs12 -in mycert.p12 -cacerts -nokeys -out cacert.pem
$ openssl pkcs12 -in mycert.p12 -clcerts -nokeys -out mycert.pem
$ openssl pkcs12 -in mycert.p12 -nocerts -nodes -out mykey.pem
The documentation states the cacert.pem file can be directly referenced by an application using the file itself, which I believe I have done within my PHP script however it isn't clear where I should put the other mycert & mykey pem files.
The documentation states the following in regards to this :-
How an application passes the issued client SSL certificate when establishing an SSL network connection to is application and environment dependent but it would essentially need to access both the "mycert.pem" and "mykey.pem" file, or in some cases, a single combined file containing both cert and key.
So nowhere does it say how these two files are used by the application, at the moment I've just left them in the same directory as the cacert.pem file.
If I try to access the url https://api.royalmail.com/shipping/onboarding directly from the browser, it asks for me to select a certificate, I select this & then enter the correct password when it asks to 'Grant or deny permission to use this key'. Once I enter the correct password the following page appears - can anyone confirm if this would mean the issue is at my end OR something that Royal Mail hasn't configured correctly at their end.
In addition to this, the actual PHP script that I have that is used to send SOAP requests to the Shipping API isn't working (probably related to everything above).
Within my PHP script the soapclient options are set up as follows:
$soapclient_options['cache_wsdl'] = 'WSDL_CACHE_NONE';
$soapclient_options['local_cert'] = 'certs/cacert.pem';
$soapclient_options['passphrase'] = $api_certificate_passphrase;
$soapclient_options['trace'] = true;
$soapclient_options['ssl_method'] = 'SOAP_SSL_METHOD_SSLv3';
$soapclient_options['location'] = 'https://api.royalmail.com/shipping/onboarding';
$client = new SoapClient('SAPI/ShippingAPI_V2_0_8.wsdl', $soapclient_options);
$client->__setLocation($soapclient_options['location']);
When I run the PHP script (this is basically the same code that Royal Mail provide themselves with my own personal API login details) I get the following message in the browser:
Could not connect to host
REQUEST: email@yoursite.co.ukAPI rngfJ+4dt4Gt855a5pr6u38i3B4= ODcwMTE5Nzc3 2015-10-13T11:02:20Z 2015-10-13T11:02:201.00526348001DeliveryDSD12015-10-13bobSS23, Some AvenueLondonE10g1000000
Obviously this cannot connect to the host for some unknown reason, the latter is simply the request that was sent. The rest of the PHP script is exactly the same as the Royal Mail's script they sent to me & have confirmed is used by others and working fine.
I am working in a WAMP environment although the eventual code will be in a Linux environment. Can anyone help I am really getting baffled & Royal Mail themselves haven't been able to provide any solid technical support yet.
UPDATE
This is the full error message displayed in the browser (i've changed the email address for security purposes)
Invalid Request REQUEST: myemail@company.co.ukAPI dgCW98Vqw3ladYgPPpNialODhvI= MTMzMjE1NjM4 2015-10-13T13:25:30Z 2015-10-13T13:25:302.00526348001DeliveryDSD12015-10-13Jon DoeSS23, Some RoadLondonE10g1000000
I've merged the two pem files into a single file called 'bundle.pem' & referenced this in the 'local_cert' variable for the SoapClient & BINGO this is now connecting. This now longer shows the Could not connect but states an 'Invalid Request' instead, so at least now this is connecting and giving me a different error.
My entire PHP script is below:
<?php
ini_set('default_socket_timeout', 120);
ini_set('soap.wsdl_cache_enabled',1);
ini_set('soap.wsdl_cache_ttl',1);
$api_password = "xxxxxxxxxxxxxx!";
$api_username = "xxxxxxxxx@xxxxxxxxx.co.ukAPI";
$api_application_id = "xxxxxxxxxxxx";
$api_service_type = "D";
$api_service_code = "SD1";
$api_service_format = "";
$api_certificate_passphrase = 'xxxxxxxxxx';
$api_service_enhancements = "";
$data = new ArrayObject();
$data->order_tracking_id = "";
$data->shipping_name = "Jon Doe";
$data->shipping_company = "SS";
$data->shipping_address1 = "23, Some Road";
$data->shipping_address2 = "";
$data->shipping_town = "London";
$data->shipping_postcode = "E1";
$data->order_tracking_boxes = "0";
$data->order_tracking_weight = "1000";
$time = gmdate('Y-m-d\TH:i:s');
$created = gmdate('Y-m-d\TH:i:s\Z');
$nonce = mt_rand();
$nonce_date_pwd = pack("A*",$nonce) . pack("A*",$created) . pack("H*", sha1($api_password));
$passwordDigest = base64_encode(pack('H*',sha1($nonce_date_pwd)));
$ENCODEDNONCE = base64_encode($nonce);
$soapclient_options = array();
$soapclient_options['cache_wsdl'] = 'WSDL_CACHE_NONE';
$soapclient_options['local_cert'] = 'royalmail/cert/bundle.pem';
$soapclient_options['passphrase'] = $api_certificate_passphrase;
$soapclient_options['trace'] = true;
$soapclient_options['ssl_method'] = 'SOAP_SSL_METHOD_SSLv3';
$soapclient_options['exceptions'] = true;
$soapclient_options['location'] = 'https://api.royalmail.com/shipping/onboarding';
//launch soap client
$client = new SoapClient('royalmail/ShippingAPI_V2_0_8.wsdl', $soapclient_options);
$client->__setLocation($soapclient_options['location']);
//headers needed for royal mail
$HeaderObjectXML = '<wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
<wsse:UsernameToken wsu:Id="UsernameToken-000">
<wsse:Username>'.$api_username.'</wsse:Username>
<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">'.$passwordDigest.'</wsse:Password>
<wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">'.$ENCODEDNONCE.'</wsse:Nonce>
<wsu:Created>'.$created.'</wsu:Created>
</wsse:UsernameToken>
</wsse:Security>';
//push the header into soap
$HeaderObject = new SoapVar( $HeaderObjectXML, XSD_ANYXML );
//push soap header
$header = new SoapHeader( 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd', 'Security', $HeaderObject );
$client->__setSoapHeaders($header);
//build the request
$request = array(
'integrationHeader' => array(
'dateTime' => $time,
'version' => '1.0',
'identification' => array(
'applicationId' => $api_application_id,
'transactionId' => $data->order_tracking_id
)
),
'requestedShipment' => array(
'shipmentType' => array('code' => 'Delivery'),
'serviceOccurence' => '1',
'serviceType' => array('code' => $api_service_type),
'serviceOffering' => array('serviceOfferingCode' => array('code' => $api_service_code)),
'serviceFormat' => array('serviceFormatCode' => array('code' => $api_service_format)),
'shippingDate' => date('Y-m-d'),
'recipientContact' => array('name' => $data->shipping_name, 'complementaryName' => $data->shipping_company),
'recipientAddress' => array('addressLine1' => $data->shipping_address1, 'addressLine2' => $data->shipping_address2, 'postTown' => $data->shipping_town, 'postcode' => $data->shipping_postcode),
'items' => array('item' => array(
'numberOfItems' => $data->order_tracking_boxes,
'weight' => array( 'unitOfMeasure' => array('unitOfMeasureCode' => array('code' => 'g')), 'value' => ($data->order_tracking_weight*1000) //weight of each individual item
)
)
)
)
);
//if any enhancements, add it into the array
if($api_service_enhancements != "") {
$request['requestedShipment']['serviceEnhancements'] = array('enhancementType' => array('serviceEnhancementCode' => array('code' => $api_service_enhancements)));
}
//try make the call
try {
$response = $client->__soapCall( 'createShipment', array($request), array('soapaction' => 'https://api.royalmail.com/shipping/onboarding') );
} catch (Exception $e) {
//catch the error message and echo the last request for debug
echo $e->getMessage();
echo " REQUEST:\n" . $client->__getLastRequest() . "\n";
die;
}
//check for any errors
if(isset($response->integrationFooter->errors)) {
$build = "";
//check it wasn't a single error message
if(isset($response->integrationFooter->errors->error->errorCode)) {
$build .= $output_error->errorCode.": ".$output_error->errorDescription."<br/>";
} else {
//loop out each error message, throw exception will be added ehre
foreach($response->integrationFooter->errors->error as $output_error) {
$build .= $output_error->errorCode.": ".$output_error->errorDescription."<br/>";
}
}
echo $build; die;
}
print_r($response);
echo "REQUEST:\n" . $client->__getLastRequest() . "\n";
die;
?>
Just for additional clarity I've added a dump of the $request variable just before it reaches the try/catch block (note this is kinda long).
Array
(
[integrationHeader] => Array
(
[dateTime] => 2015-10-13T13:34:44
[version] => 1.0
[identification] => Array
(
[applicationId] => 0526348001
[transactionId] =>
)
)
[requestedShipment] => Array
(
[shipmentType] => Array
(
[code] => Delivery
)
[serviceOccurence] => 1
[serviceType] => Array
(
[code] => D
)
[serviceOffering] => Array
(
[serviceOfferingCode] => Array
(
[code] => SD1
)
)
[serviceFormat] => Array
(
[serviceFormatCode] => Array
(
[code] =>
)
)
[shippingDate] => 2015-10-13
[recipientContact] => Array
(
[name] => Jon Doe
[complementaryName] => SS
)
[recipientAddress] => Array
(
[addressLine1] => 23, Some Road
[addressLine2] =>
[postTown] => London
[postcode] => E1
)
[items] => Array
(
[item] => Array
(
[numberOfItems] => 0
[weight] => Array
(
[unitOfMeasure] => Array
(
[unitOfMeasureCode] => Array
(
[code] => g
)
)
[value] => 1000000
)
)
)
)
)
Firstly, accessing the https://api.royalmail.com/shipping/onboarding directly will not work, because it's only accessible via the API.
With Royal Mail, do you have all of the CDM files and WSDL files? Make sure the CDM files are in the same directory as the WSDL files.
Here's what I did when working with the API;
Then, when it came to actually making the connection, I did something like this:
I hope this helps.
Edit
Try this request
I think you need to use Version 2 for that Service Occurrence, if I remember rightly. Also, just...formatting. Makes it easier to debug.
! Although you've mentioned you've done most of these steps, I'll detail them anyway to complete the instructions
Installing the CA to the server
/root/Desktop
for example)*.pem
files into/etc/ssl/certs
mkdir royalmail
)*.pem
files (mv *.pem /etc/ssl/certs/certificates
)mycert.pem
andmykey.pem
into it (only from-----BEGIN .... -------
to EOF)You should have the following files;
Using SOAPClient
Now that the certificates are installed, we can now test the connection (assuming your certificates are in
/etc/pki/tls/certs/certificates/royalmail/shippingv2
)If you run
You should be able to connect to port 443 (there may be a handshake failure in
OpenSSL
though - at least with my test just now on our staging environment).We can now instantiate the
SoapClient
using a local copy of the WSDL and specifying the local certificate in the$options
parameter.Authentication
In your documentation, you should find a file called
rm_password_digest.php
or something similar, which details how to create the authentication headers.This will help you build the following in the SOAPHeader