Accessing Currency Cloud's API using PHP

Preamble

I think you can register for demo access to Currency Cloud without being a client, but to activate webhooks on this you'd need to contact a "Solutions Consultant".

Hello World

Authentication and the X-Auth-Token header

From /developers/cookbooks/authenticating/ every request to the Currency Could API needs to be accompanied by an X-Auth-Token: header, this expires after 30 minutes of inactivity. I've taken the view to simply request a new one with every instantiation of the Class (done on the Constructor) and add it to an array of headers maintained in the Class.

Webhook and security

From /developers/cookbooks/push-notifications/ you can set up integrations that correspond with 'notifications' from Currencycloud. If you choose to do this there are 3 layers of security that should be implemented:

  1. Layer 3: IP Whitelisting, this could be implemented in an appliance, iptables or simply in PHP code (example uses the latter)
  2. Layer 6: Ensure that an https URL is provided
  3. Layer 7: Ask Currency Cloud to sign the message with a hash-based message authentication code and validate it when it arrives
    1. Cause Currency Cloud to generate a new key, each time you call this the old key is invalidated / replaced with the new one
      <?php
        require_once(AUTH_FILE);
        require_once(CLASS_FILE);
        $CurrencyCloud = new CurrencyCloud();
        echo $CurrencyCloud->generateHMAC();
      ?>
      # php x.php
      4233d40827244a688adcd1d404e7e1e5cc95b1e61fc6c09a
      
    2. set const CCHMACKey in the AUTH file

This is an example of a webhook script for (say) https://domain.example/web-hook/

<?php
  require_once(AUTH_FILE);
  $valid = 0;
  if (in_array($_SERVER['REMOTE_ADDR'], CCAllowedIPs) === true) {
    if ($_SERVER['REQUEST_METHOD'] == 'POST') {
      $rxBody = file_get_contents("php://input");
      $rxHeaderArr = getallheaders();
      if (array_key_exists('X-Hmac-Digest-Sha-512', $rxHeaderArr)) {
        if ($rxHeaderArr['X-Hmac-Digest-Sha-512'] == hash_hmac('sha512', $rxBody, CCHMACKey)) {
          $valid = 1;
          // do something with $rxBody
          echo 'OK';
          exit(0);
        }
      }
    }
  }
  header("HTTP/1.0 400 Bad Request");
  exit(1);
?>

Source Code

Authentication details

Seperating authentication data from the main Class allows (1) AUTH files to be located where permissions/access is suitably restricted, (2) the Class file to be under version control, without the risk of credentials leaking and (3) only one line of a program needs to change shift a program from Demo to Live.

    const CCReqHeaders = array('User-Agent: HelloWorld Agent', 'Accept: application/json');
    const CCBaseURL = 'https://directdemo.currencycloud.com';
    const CCAllowedIPs = array('52.214.236.241','52.214.33.127','52.215.88.1','52.56.108.189','52.56.108.195','35.157.104.248','35.157.113.98','35.157.98.135','108.128.119.77','52.215.246.166','63.33.40.150','52.214.236.241','52.214.33.127','35.177.158.89', '35.178.90.97', '92.207.224.178', '2.217.100.92');
    const CCHMACKey = "your HAMC key';
    const CCAuthArr = array(
      'login_id' => 'your email address', 
      'api_key' => 'your API key'
    );

Class file

  class CurrencyCloud
  {
    
    private $responseHeaders = array();
    private $reqHeaders = CCReqHeaders;
    private $respHeaders = array();
 
    public function __construct()
    {
      $resp = $this->callAPI('/v2/authenticate/api', CCAuthArr, 'POST');
      if (is_null($resp)) {
        throw new Exception("Currency Cloud: Response is not valid JSON");
      }
      if (!array_key_exists('auth_token', $resp)) {
        throw new Exception("Currency Cloud: auth_token not found in response");
      }
      $this->reqHeaders[] = 'X-Auth-Token: '. $resp['auth_token'];
    }


    public function generateHMAC() {
      $resp = $this->callAPI('/v2/contacts/generate_hmac_key', array(), 'POST');
      return $resp["hmac_key"];
    }


    public function getAllClients() {           /* this will take time to run, ideally don't call it on the UI thread and be aware that more results per page would be coded better (I've gone for POC- simple) */
      $Clients = array();
      $page = 0;
      do 
      {
        $page++;
        $resp = $this->callAPI('/v2/accounts/find?per_page=1&page=' . $page);
        if (array_key_exists('accounts', $resp)) {
          if (array_key_exists(0, $resp['accounts'])) {
            $Clients[] = $resp['accounts'][0];
          }
        }
        if (array_key_exists('pagination', $resp)) { 
          if (!array_key_exists('total_entries', $resp['pagination'])) {
            throw new Exception("Currency Cloud: getAllClients() unable to complete, total_entries missing");

          }
        } else {
          throw new Exception("Currency Cloud: getAllClients() unable to complete, no pagination");
        }
      }
      while ($page <= ($resp['pagination']['total_entries']));
      return $Clients;
    }


    public function getClient($guid) {
      $resp = $this->callAPI('/v2/accounts/' . $guid);
      return $resp;
    }


    public function getBene($guid) {
      $resp = $this->callAPI('/v2/beneficiaries/' . $guid);
      return $resp;
    }


    private function callAPI($url, $arr = array(), $method='GET') 
    {
      $ch = curl_init();
      curl_setopt($ch, CURLOPT_URL, CCBaseURL . $url);
      if ($method == 'POST') { 
        curl_setopt($ch, CURLOPT_POST, 1); 
        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($arr));
      } 
      curl_setopt($ch, CURLOPT_HTTPHEADER, $this->reqHeaders);      
      curl_setopt($ch, CURLINFO_HEADER_OUT, true);
      curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
      curl_setopt($ch, CURLOPT_HEADERFUNCTION, array(&$this, 'header'));

      $body = curl_exec($ch);
      
      /*  Outbound Debug
      $info = curl_getinfo($ch);
      var_dump($info);
      */

      $resp = json_decode($body,true);
      curl_close ($ch);
      return $resp;
    }


    private function header($curl, $hl)
    {
      $tmp = explode(':', $hl, 2);
      if (count($tmp)==2) {
        $this->respHeaders[$tmp[0]] = trim($tmp[1]);
      }
      return strlen($hl);
    }
   
    
  }