Legacy API key migration - PHP ISDK

Hi,

My application connects with keap by using PHP ISDK code (xml-rpc - Keap Developer Portal), and I have followed the instruction to change the legacy key to the SAK key in isdk php file and in conn cfg php file. also change the urls from myappid to api in isdk php file.

but Im getting the following error

Fatal error : Uncaught iSDKException: ERROR: 5 - Didn’t receive 200 OK from remote server. (HTTP/2 401 ) in /home/kayadeve/public_html/kayaIS/isdk.php:88 Stack trace: #0 /home/kayadeve/public_html/kayaIS/index.php(7): iSDK->cfgCon(‘ISCON’) #1 {main} thrown in /home/kayadeve/public_html/kayaIS/isdk.php on line 88

can you please help to resolve this? also once resolved, is there anything I need to do further to avoid any future issue?

thanks,
Farrukh

@Farrukh_Naseem I refactored the original XMLRPC library, and you can find it linked in this forum post:

Assuming you’re using the PHP legacy iSDK library.

There are three key changes that are required.

  1. The URL endpoint changed: https://api.infusionsoft.com/crm/xmlrpc/v1
  2. Replace the legacy API key with a SAK
  3. Pass the SAK in the custom header: X-Keap-Api-Key: {SAK_GOES_HERE}

Hi @Marion_Dorsett2 , I have downloaded updated isdk file you shared along with xml folder.

gave SAK key in the $key variable

there is no $name filed I could found in isdk file to give app name, i gave app name to it to $logname veriable. and getting the following error

Fatal error : Uncaught iSDKException: ERROR: 5 - Didn’t receive 200 OK from remote server (HTTP/2 401 ) in /home/kayadeve/public_html/kayaIS/isdk.php:95 Stack trace: #0 /home/kayadeve/public_html/kayaIS/index.php(7): iSDK->cfgCon(‘ISCON’) #1 {main} thrown in /home/kayadeve/public_html/kayaIS/isdk.php on line 95

Hi @Farrukh_Naseem,

It is possible that you may have made a typing mistake. If the Legacy iSDK worked with the Legacy API Key, then you only need to change the necessary parts to make it work with the Service Account Key.

If you are using the “conn.cfg.php” file, then the format would be the following below.

InfusionSoftApp:<< APP NAME >>:i:<< SERVICE ACCOUNT KEY >>:This is the API key for the proposed project <<APP NAME>>.infusionsoft.com

Where “<< APP NAME >>” is your Keap Application Name, the Subdomain of the Web Address. And the “<< SERVICE ACCOUNT KEY >>” is the replacement key.

If you are using the “cfgCon” function directly, then set the Application Name and Service Account Key as the first two parameters.

$keap->cfgCon('<<APP NAME>>', '<<SERVICE ACCOUNT KEY>>');

For the URL Endpoint I have it set to “https://api.infusionsoft.com/crm/xmlrpc”.

As Marion said, make sure the Header Key has been set. Note, you need to make sure you have the right XML-RPC Library to set that. Marion iSDK should work for you.

Hope that was clear.

Hi Pav,

cfgCof function is setup exactly as you have guided.

I think the issue is Im not sure where to perform the third step

  1. Pass the SAK in the custom header: X-Keap-Api-Key: {SAK_GOES_HERE}

where I can find X-Keap-Api-Key: {SAK_GOES_HERE} to give my SAK

this code of ine already have the SAK key assigned

\PhpXmlRpc\Client::OPT_CUSTOM_HEADER => array('X-Keap-API-Key: ’ . $this->key),

can you tell me where exactly I will have to perform the 3rd step

  1. Pass the SAK in the custom header: X-Keap-Api-Key: {SAK_GOES_HERE}

hopefully this will resolve my issue

Im geeting this error now

Fatal error : Uncaught iSDKException: No connection information found. in /home/kayadeve/public_html/farrukh/isdk.php:59 Stack trace: #0 /home/kayadeve/public_html/farrukh/index.php(7): iSDK->cfgCon(‘ISCON’) #1 {main} thrown in /home/kayadeve/public_html/farrukh/isdk.php on line 59

@Farrukh_Naseem, if you look at Marion code you will see what he has done.

If you are using the original XML-RPC Library with the SDK then that will need upgrading.
The Newer Version of the XML-RPC Library has support for Customisable Headers.

I posted a reply to Marion in which you can use that instead of modifying the library.

I made changes to the “methodCaller” function. Here is what I changed to work with the latest XML-RPC Library.

        // * Original Way *
        // $call = $this->xmlrpc_request($service, $callArray);
        // array_unshift($call->params, $this->encKey);
 
        /* Set up the call */
        // 2023-06-05 - Fix to make it work for the PHP XML-RPC library v4.10.1 as "params" is not exposed anymore.
         $call = $this->xmlrpc_request($service, array_merge([ $this->xmlrpc_encode($this->key) ], $callArray));

        // Check the OAuth Token to see if it needs to be refreshed prior to the call.
        $headers = 
        [
            "Content-Type: text/xml",
            "Accept-Charset: UTF-8,ISO-8859-1,US-ASCII",
            "Expect:",
            "X-Keap-API-Key: {$this->key}"
        ];

         $this->client->setOption('extracurlopts', [ CURLOPT_HTTPHEADER => $headers ]);

My iSDK has been modified over the years, so it is not the same as the original one.

Hi Pav, can you share your isdk.php file?

@Farrukh_Naseem, I will share you the parts that are the most relevant to you, and for other developers to see. The iSDK that I have got has been modified over the years, so there is differences in some areas.

Firstly, what version of PHP are you using? If you got PHP v8.1, v8.2, v8.3 then you should be okay below.
Anything older, then I recommend that you update it to a newer version if you can.

Secondly, you need to download the latest PHP XML-RPC Library from here.

Thirdly, in the iSDK the following areas would need to be changes.

Replace loading of the “xmlrpc.inc” file with the XML-RPC Libray Autoloader.

require_once('__DIR__ . /phpxmlrpc/src/Autoloader.php');   // Path to the Autoloader.

\PhpXmlRpc\Autoloader::register();   // Register the Autoloader.

Here are the changes done within the “cfgCon” function so that it points to new API Endpoint.

// * Original Location *
// $this->client = $this->xmlrpc_client("https://$appname.infusionsoft.com/api/xmlrpc");

// * New Location *
$this->client = $this->xmlrpc_client("https://api.infusionsoft.com/crm/xmlrpc");

Here are the changes done to the “methodCaller” function.

Note, there is an “api_access_mode” property that has been added to switch between the Legacy API Key / Service Account Key and OAuth. The Headers are different for the different connection methods.

public function methodCaller($service, $callArray)
{
    // Set up the Headers Array based on the PHP XML-RPC Library default Headers.
    $headers = 
    [
        "Content-Type: text/xml",
        "Accept-Charset: UTF-8,ISO-8859-1,US-ASCII",
        "Expect:"            
    ];

    if($this->api_access_mode === INFUSIONSOFT_ISDK_API_ACCESS_MODE_LEGACY_API_ENCRYPTED_KEY ||
       $this->api_access_mode === INFUSIONSOFT_ISDK_API_ACCESS_MODE_SERVICE_ACCOUNT_KEY)
    {   
        $headers[] = "X-Keap-API-Key: {$this->key}";
    }
    elseif($this->api_access_mode === INFUSIONSOFT_ISDK_API_ACCESS_MODE_OAUTH)
    {           
        $headers[] = "Authorization: Bearer {$this->key}";
    }

    // * Original Way *
    // $call = $this->xmlrpc_request($service, $callArray);
    // array_unshift($call->params, $this->encKey);
 
   // Change done to make it work for the PHP XML-RPC Library v4.10.1 onwards as "params" is not exposed anymore.
   $call = $this->xmlrpc_request($service, array_merge([ $this->xmlrpc_encode($this->key) ], $callArray));

   // Set this for PHP XML-RPC so that the Headers can be overridden.     
   $this->client->setOption('extracurlopts', [ CURLOPT_HTTPHEADER => $headers ]);


    /* Send the call */
    $now = time();
    $start = microtime();
    $result = $this->client->send($call);

    $stop = microtime();

    /* Check the returned value to see if it was successful and return it */
    if (!$result->faultCode()) 
    {
        if ($this->log_function) 
        {
            $this->log(array('Method' => $service, 'Call' => $this->log_data, 'Start' => $start, 'Stop' => $stop, 'Now' => $now, 'Result' => $result, 'Error' => 'No', 'ErrorCode' => 'No Error Code Received' ));
        }
            return $result->value();
        } 
        else 
        {            
            if ($this->log_function) 
            {
                $this->log(array('Method' => $service, 'Call' => $this->log_data, 'Start' => $start, 'Stop' => $stop, 'Now' => $now, 'Result' => $result, 'Error' => 'Yes', 'ErrorCode' => "ERROR: " . $result->faultCode() . " - " . $result->faultString()));
            }
                        
            if ($this->debug == "kill") {
                die("ERROR: " . $result->faultCode() . " - " .
                    $result->faultString());
            } elseif ($this->debug == "on") {                
                return "ERROR: " . $result->faultCode() . " - " .
                $result->faultString();                                                
            } elseif ($this->debug == "throw") {
                throw new iSDKException($result->faultString(), $result->faultCode());
            } elseif ($this->debug == "off") {
                //ignore!
            }
        }

    }

Hope that was clear.

Hi Pav,

here is what I did, I am using php 8.2 version, and I xmlrpc-4.10.3.

Please look at the sequence of my code below

in index.php Im creating the instance as follow

<?php require_once("isdk.php"); $myApp = new iSDK; if ($myApp->cfgCon("ISCON")) {echo "connected"; }else {echo "Not Connected...";} ?>

here is my conn.cfg.php

$connInfo = array(‘ISCON:APP_NAME:i:SAK_KEY:This is the connection for APP_NAME.infusionsoft.com’);

in isdk.php (that I downloaded from Marion’s link along with xmlrpc-4.10.3 folder), it already includes

require_once(‘DIR . /phpxmlrpc/src/Autoloader.php’); // Path to the Autoloader.

\PhpXmlRpc\Autoloader::register(); // Register the Autoloader.

$this->client = $this->xmlrpc_client("https://api.infusionsoft.com/crm/xmlrpc");```

and I replaced methodCaller with your function. but Im getting the following error


Warning: Undefined property: iSDK::$api_access_mode in /home/kayadeve/public_html/farrukh/isdk.php on line 133

Fatal error: Uncaught Error: Undefined constant "INFUSIONSOFT_ISDK_API_ACCESS_MODE_LEGACY_API_ENCRYPTED_KEY" in /home/kayadeve/public_html/farrukh/isdk.php:133 Stack trace: #0 /home/kayadeve/public_html/farrukh/isdk.php(680): iSDK->methodCaller('DataService.get...', Array) #1 /home/kayadeve/public_html/farrukh/isdk.php(89): iSDK->dsGetSetting('Application', 'enabled') #2 /home/kayadeve/public_html/farrukh/index.php(7): iSDK->cfgCon('ISCON') #3 {main} thrown in /home/kayadeve/public_html/farrukh/isdk.php on line 133

I know, Im very near, can you please help me to identify the issue.

@Farrukh_Naseem, the iSDK that I maintained has been modified over the years, so it will not match to what Marion and other developers would have.

Add the following constants.

define('INFUSIONSOFT_ISDK_API_ACCESS_MODE_LEGACY_API_ENCRYPTED_KEY', 'legacy_api_encrypted_key');
define('INFUSIONSOFT_ISDK_API_ACCESS_MODE_OAUTH',                    'oauth');
define('INFUSIONSOFT_ISDK_API_ACCESS_MODE_SERVICE_ACCOUNT_KEY',      'service_account_key');

Add the following property to the top of the class. This will default the Access Mode to the Service Account Key.

 private $api_access_mode = INFUSIONSOFT_ISDK_API_ACCESS_MODE_SERVICE_ACCOUNT_KEY;     

I no longer use the “conn.cfg.php” file as I feed the Application Name and API Key directly to the “cfgCon” function.

In Marion iSDK code, that has a modified XML-RPC Library. What I have shared earlier is to avoid modifying it, so you can use its own functions directly to set the Header.

The main parts to change in the iSDK are the following.

I am sure by making the changes you will have it working.

Hi Pav,

Added the code two things,

  1. as legacy key will not be. used anymore in the future, should I skip
    define(‘INFUSIONSOFT_ISDK_API_ACCESS_MODE_LEGACY_API_ENCRYPTED_KEY’, ‘legacy_api_encrypted_key’);
    because in the condition below its using OR operator between legacy and SAK. so giving SAK will work

  2. I added

 private $api_access_mode = INFUSIONSOFT_ISDK_API_ACCESS_MODE_SERVICE_ACCOUNT_KEY;     

in the class, and it giving

Fatal error : Uncaught Error: Undefined constant “INFUSIONSOFT_ISDK_API_ACCESS_MODE_SERVICE_ACCOUNT_KEY” in /home/kayadeve/public_html/farrukh/index.php:5 Stack trace: #0 {main} thrown in /home/kayadeve/public_html/farrukh/index.php on line 5

undefined constant error.

@Farrukh_Naseem,

I have made the “methodCaller” simpler, by removing the api_access_mode variable and constants from it. It will work for both the Legacy API Key and Service Account Key.

public function methodCaller($service, $callArray)
{
    // Set up the Headers Array based on the PHP XML-RPC Library default Headers.
    $headers = 
    [
        "Content-Type: text/xml",
        "Accept-Charset: UTF-8,ISO-8859-1,US-ASCII",
        "Expect:",
        "X-Keap-API-Key: {$this->key}";
    ];
   
    // * Original Way *
    // $call = $this->xmlrpc_request($service, $callArray);
    // array_unshift($call->params, $this->encKey);
 
   // Change done to make it work for the PHP XML-RPC Library v4.10.1 onwards as "params" is not exposed anymore.
   $call = $this->xmlrpc_request($service, array_merge([ $this->xmlrpc_encode($this->key) ], $callArray));

   // Set this for PHP XML-RPC so that the Headers can be overridden.     
   $this->client->setOption('extracurlopts', [ CURLOPT_HTTPHEADER => $headers ]);


    /* Send the call */
    $now = time();
    $start = microtime();
    $result = $this->client->send($call);

    $stop = microtime();

    /* Check the returned value to see if it was successful and return it */
    if (!$result->faultCode()) 
    {
        if ($this->log_function) 
        {
            $this->log(array('Method' => $service, 'Call' => $this->log_data, 'Start' => $start, 'Stop' => $stop, 'Now' => $now, 'Result' => $result, 'Error' => 'No', 'ErrorCode' => 'No Error Code Received' ));
        }
        return $result->value();
    } 
    else 
    {            
        if ($this->log_function) 
        {
            $this->log(array('Method' => $service, 'Call' => $this->log_data, 'Start' => $start, 'Stop' => $stop, 'Now' => $now, 'Result' => $result, 'Error' => 'Yes', 'ErrorCode' => "ERROR: " . $result->faultCode() . " - " . $result->faultString()));
        }
                        
        if ($this->debug == "kill") {
            die("ERROR: " . $result->faultCode() . " - " . $result->faultString());
        } elseif ($this->debug == "on") {                
            return "ERROR: " . $result->faultCode() . " - " . $result->faultString();                                                
        } elseif ($this->debug == "throw") {
            throw new iSDKException($result->faultString(), $result->faultCode());
        } elseif ($this->debug == "off") {
            //ignore!
        }
    }
}