Zoho CRM: APIv2 using PHP & cURL

What?
This is an article documenting how to access ZohoCRM with API v2 using PHP and cURL. The first few functions are to manage OAuth v2 and generate the refresh and access tokens. The second snippet of code below is using the functions to read data from Zoho CRM and to write data back to the system.

Why?
I've rewritten this code a few times and want to store the finalized version (following updates) making it as generic as I can in order to apply it to any client.

How?
Firstly, you will need to browse to https://accounts.zoho.eu/developerconsole and register your new app (or the one you will have completed once copying the below scripts).

Let's start with the first PHP file which I will call functions.php. Note that you will need to edit the global vars to be used by the functions located at the top of the script:
<?php

/*
	------------------------------------------------------------------------------------------------
	Zoho Authorization via oAuth2.0 for REST API v2
	------------------------------------------------------------------------------------------------

	Zoho API v2:	https://accounts.zoho.eu/developerconsole

	Documentation:	https://www.zoho.com/crm/help/api/v2/

	Available Scopes
	    users		users.all
	    org			org.all
	    settings	settings.all, settings.territories, settings.custom_views, settings.related_lists,
	    			settings.modules, settings.tab_groups, settings.fields, settings.layouts,
	    			settings.macros, settings.custom_links, settings.custom_buttons, settings.roles,
	    			settings.profiles
	    modules		modules.all, modules.approvals, modules.leads, modules.accounts, modules.contacts,
	    			modules.deals, modules.campaigns, modules.tasks, modules.cases, modules.events,
	    			modules.calls, modules.solutions, modules.products, modules.vendors,
	    			modules.pricebooks, modules.quotes, modules.salesorders, modules.purchaseorders,
	    			modules.invoices, modules.custom, modules.dashboards, modules.notes,
	    			modules.activities, modules.search

	Possible Module Names
		leads, accounts, contacts, deals, campaigns, tasks, cases, events, calls, solutions, products,
		vendors, pricebooks, quotes, salesorders, purchaseorders, invoices, custom, notes, approvals,
		dashboards, search, activities

*/

	// Global vars for Zoho API
	$zoho_apis_com = "https://www.zohoapis.com";
	$zoho_apis_eu = "https://www.zohoapis.eu";
	$refresh_access_token_url = "https://accounts.zoho.eu/oauth/v2/token";

	// Endpoint: Sandbox  // disable after testing
	$zoho_sandbox = "https://sandbox.zohoapis.eu";
	$zoho_sandbox_domain = "https://crmsandbox.zoho.eu/crm";

	// Global vars to be used by below functions specific to this app
	$zoho_client_id = "1000.your_client_id";
	$zoho_client_secret = "your_client_secret";
	$zoho_redirect_uri = "your_redirect_uri";  // will be URL to start.php in this example
	$access_token_path = "path_to_your_access_token_not_on_www_but_accessible_by_script/access_token.dat";
	$refresh_token_path = "path_to_your_access_token_not_on_www_but_accessible_by_script/refresh_token.dat";

	// function to use Zoho API v2
	function abZohoApi( $post_url, $post_fields, $post_header=false, $post_type='GET' )
	{
		// setup cURL request
		$ch=curl_init();

		// do not return header information
		curl_setopt($ch, CURLOPT_HEADER, 0);

		// submit data in header if specified
		if(is_array($post_header)){
			curl_setopt($ch, CURLOPT_HTTPHEADER, $post_header);
		}

		// do not return status info
		curl_setopt($ch, CURLOPT_VERBOSE, 0);

		// return data
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

		// cancel ssl checks
		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
		curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);

		// if using GET, POST or PUT
		if($post_type=='POST'){
			curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
			curl_setopt($ch, CURLOPT_POSTFIELDS, $post_fields);
		} else if($post_type=='PUT'){
			curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
			curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($post_fields));
		} else if($post_type=='DELETE'){
			curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
		}else{
			curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
			if($post_fields){
				$post_url.='?'.http_build_query($post_fields);
			}
		}

		// specified endpoint
		curl_setopt($ch, CURLOPT_URL, $post_url);

		// execute cURL request
		$response=curl_exec($ch);

		// return errors if any
		if (curl_exec($ch) === false) {
			$output = curl_error($ch);
		} else {
			$output = $response;
		}

		// close cURL handle
		curl_close($ch);

		// output
		return $output;
	}

	// Generate the refresh token by manually entering the following in the browser
	// Browse to https://accounts.zoho.eu/oauth/v2/auth?scope=ZohoCRM.modules.ALL,ZohoCRM.users.READ,ZohoCRM.settings.fields.read&client_id=your_client_id&response_type=code&access_type=offline&redirect_uri=your_url_path_to_start_page/start.php&prompt=consent

	// function to generate refresh token from Zoho authorization code
	function generate_refresh_token(){

		// use vars declared at beginning of script
		global $zoho_client_id, $zoho_client_secret, $zoho_redirect_uri, $access_token_path, $refresh_token_path;

		// Generate Access Token and Refresh Token - Read url GET values
		$zoho_grant_token = $_GET['code'];
		$zoho_location = $_GET['location'];
		$zoho_accounts_server = $_GET['accounts-server'];

		// Generate Access Token and Refresh Token
		$url_auth=urldecode($zoho_accounts_server)."/oauth/v2/token";

		// Build fields to post
		$fields_token=array("code"=>$zoho_grant_token, "redirect_uri"=>$zoho_redirect_uri, "client_id"=>$zoho_client_id, "client_secret"=>$zoho_client_secret, "grant_type"=>"authorization_code", "prompt"=>"consent");

		// Generate Access Token and Refresh Token - post via cURL
		$response_json = abZohoApi($url_auth, $fields_token, false, 'POST');

		// Generate Access Token and Refresh Token - format output (convert JSON to Object)
		$refresh_token_arr = json_decode($response_json, true);

		// store in var
		$refresh_token = isset($refresh_token_arr['refresh_token']) ? $refresh_token_arr['refresh_token'] : 0;

		// encode value to base64
		$refresh_token_base64 = base64_encode($refresh_token);

		// store encoded value to file
		file_put_contents($refresh_token_path, $refresh_token_base64);

		// -- do access token while we're here
		// store in access token
		$access_token = isset($refresh_token_arr['access_token']) ? $refresh_token_arr['access_token'] : 0;

		// encode value to base64
		$access_token_base64 = base64_encode($access_token);

		// store encoded value to file
		file_put_contents($access_token_path, $access_token_base64);

		// return array of json objects
		return $refresh_token_arr;
	}


	// function to generate access token from refresh token
	// returns minutes remaining of valid token
	function generate_access_token(){

		// use vars declared at beginning of script
		global $zoho_client_id, $zoho_client_secret, $access_token_path, $refresh_token_path, $refresh_access_token_url;

		// get refresh token from file
		$refresh_token = base64_decode( file_get_contents( $refresh_token_path ) );

		// build fields to post
		$refresh_fields = array("refresh_token" => $refresh_token, "client_id" => $zoho_client_id, "client_secret" => $zoho_client_secret, "grant_type" => "refresh_token");

		// send to Zoho API
		$this_access_token_json = abZohoApi($refresh_access_token_url, $refresh_fields, false, 'POST');

		// convert JSON response to array
		$access_token_arr = json_decode($this_access_token_json, true);

		// store in var
		$returned_token = $access_token_arr['access_token'];

		// encode value to base64
		$access_token_base64 = base64_encode($returned_token);

		// store encoded value to file
		file_put_contents($access_token_path, $access_token_base64);
	}

	// function to decode and read access token from file
	function read_token($file){

		// get access token from file
		$token_base64 = file_get_contents($file);

		// decode value to token
		$token = base64_decode($token_base64);

		// output
		return $token;
	}

	// function to sort our returned data (multidimensional array)
	function array_sort_by_column(&$arr, $col, $dir = SORT_DESC) {
		$sort_col = array();
		foreach ($arr as $key=> $row) {
			$sort_col[$key] = strtolower($row[$col]); // strtolower to make it case insensitive
		}
		array_multisort($sort_col, $dir, $arr);
	}

	// function to get minutes left on a generated file
	// defaults to an hour expiry time
	// usage: get_time_remaining( 'access.dat', 3600)
	function get_time_remaining($file, $expiry_in_seconds=3600){

		// get file modified time
		$file_modified_time = filemtime($file);

		// add 1 hour
		$file_expiry_time = $file_modified_time + $expiry_in_seconds;

		// calculate seconds left
		$diff = $file_expiry_time - time();

		// round to minutes
		$minutes = floor($diff/60);

		// output
		return $minutes;
	}


	// function to check access token and regenerate if necessary
	function check_access_token(){

		global $access_token_path;

		// get time remaining on access token (1 hour max)
		$access_token_time_remaining = get_time_remaining($access_token_path);

		// if less than 5 minutes left, regenerate token
		if($access_token_time_remaining<=5){

			// Generate Access Token from Refresh Token
			generate_access_token();

			// update time remaining on access token (again)
			$access_token_time_remaining = get_time_remaining($access_token_path);
		}

		// return time remaining (in minutes)
		return $access_token_time_remaining;

	}


	// get data: returns PHP Array (for functional sorting: PHP & JS)
	// usage: Leads: get_records("Leads")
	// usage: Lead: get_records("Leads", 98304820934029840)
	// usage: User: get_records("users", 10825000000119017)  // note that users has to be lowercase
	function get_records($zoho_category, $zoho_id=0, $fields_data=array()){

		global $access_token_path, $zoho_apis_eu;

		// get access token
		$access_token = read_token($access_token_path);

		// endpoint
		$url_data=$zoho_apis_eu."/crm/v2/".$zoho_category;

		// if array (eg. Related Records), accept ID and related_list_apiname
		if(is_array($zoho_id)){
			$url_data.= $zoho_id[0]>0 ? '/'.$zoho_id[0].'/'.$zoho_id[1] : '';
		}else{
			// add id if exists
			$url_data.= $zoho_id!=0 && $zoho_id!="" ? '/'.$zoho_id : '';
		}

		// add access token to header
		$header_data = array("Authorization: Zoho-oauthtoken ".$access_token);

		// send to Zoho API
		$response_json = abZohoApi($url_data, $fields_data, $header_data);

		// convert response to PHP array (for sorting)
		$response_arr = json_decode($response_json, true);

		// output
		return $response_arr;
	}

	// get data: returns PHP Array (for functional sorting: PHP & JS)
	// usage: Leads: search_records("Leads", array('Last_Name:starts_with:G', 'Email:equals:This email address is being protected from spambots. You need JavaScript enabled to view it.'))
	function search_records($zoho_category, $criteria=array()){

		global $access_token_path, $zoho_apis_eu;

		// get access token
		$access_token = read_token($access_token_path);

		// endpoint
		$url_data=$zoho_apis_eu."/crm/v2/".$zoho_category."/search?criteria=";

		// join the criteria
		if(count($criteria)==1){
			$url_data.= '('.$criteria[0].')';
		}elseif(count($criteria)>1){
			$url_data.= '(('.implode($criteria, ') and (').'))';
		}

		// add access token to header
		$header_data=array("Authorization: Zoho-oauthtoken ".$access_token);

		// send to Zoho API
		$response_json = abZohoApi($url_data, false, $header_data);

		// convert response to PHP array (for sorting)
		$response_arr = json_decode($response_json, true);

		// output
		return $response_arr;
	}

	// function to retrieve current user record
	// accepts User ID as parameter
	// returns array( authorized{ok,fail}, name, email, profile, role )
	function authenticate_user($user_id){

		global $access_token_path;

		// get access token
		$token = read_token($access_token_path);

		// pass parameter requesting only active users who are also confirmed
		$user_fields = array("type"=>"ActiveConfirmedUsers");

		// authenticate user (id was stored in cookie from GET var when this app was initially loaded)
		$zoho_user_record = get_records("users", $user_id, $user_fields);

		// failed by default
		$user_authorized = 'fail';
		$zoho_user_name=$zoho_user_email=$zoho_user_profile=$zoho_user_role="";
		if(isset($zoho_user_record['users'])){

			// record is readable
			$zoho_user_isactive = $zoho_user_record['users'][0]['status'];

			// if status is active
			if($zoho_user_isactive=='active'){
				$user_authorized = 'ok';
				$zoho_user_name = $zoho_user_record['users'][0]['full_name'];

				// user email
				$zoho_user_email = $zoho_user_record['users'][0]['email'];

				// user profile
				$zoho_user_profile = $zoho_user_record['users'][0]['profile']['name'];

				// user role
				$zoho_user_role = $zoho_user_record['users'][0]['role']['name'];
			}
		}

		// return vars
		return array("authorized"=>$user_authorized, "name"=>$zoho_user_name, "email"=>$zoho_user_email, "profile"=>$zoho_user_profile, "role"=>$zoho_user_role);
	}


	// function to ensure data was transferred
	// accepts array (JSON Response)
	// returns boolean
	function check_data_is_valid($data){
		$is_valid = false;
		if(isset($data['data'])){
			$is_valid = true;
		}
		return $is_valid;
	}
    

Then we need a start page that the user will browse to (or that is the endpoint for the redirect), I will call this file start.php:
<?php

	// set header
    header('Content-Type: text/html; charset=utf-8');

	// include global functions
	include('./functions.php');
?>
<!doctype html>
<html>
    <head>
        <title>Zoho OAuth Script</title>
    </head>
    <body>
<?php
    // check access token, regenerate if expired (1 hour)
    check_access_token();

    // determine minutes left
    $access_token_time_remaining = get_time_remaining($access_token_path);

    // generate another token if about to expire
    if($access_token_time_remaining<5){
        echo '<h1>Oops! Something went wrong.</h1>';
        generate_access_token();
        echo '<p><b><u>Access</u></b> Token has been regenerated.  Please reload this page.</p><pre>';

        // get access token from file
        $access_token = base64_decode( file_get_contents( $access_token_path ) );

        // display access token
        echo "\t".$access_token;
        echo '</pre>';
    }else{
        echo '<h1>Yay! All went well.</h1>';
        echo '<p>Stored <b><u>Access</u></b> Token is valid for another '.$access_token_time_remaining.' minute'.($access_token_time_remaining==1?'':'s').'.</p><pre>';

        // get access token from file
        $access_token = base64_decode( file_get_contents( $access_token_path ) );

        // display access token
        echo "\t".$access_token;
        echo '</pre>';
    }

    // if refresh token is being generated
    if(isset($_GET['code'])){

        // read get vars (code) generate refresh and access token.  Store refresh token in file.
        $this_response_arr = generate_refresh_token();

        // get refresh token from file
        $refresh_token = base64_decode( file_get_contents( $refresh_token_path ) );

        // check refresh token exists and is of expected length
        if(strlen($refresh_token)==70){
            echo '<h1>Yay! All went well.</h1>';
            echo '<p><b>Refresh</b> Token successfully generated and stored.</p><pre>';
            print_r($this_response_arr);
            echo '</pre>';
        }else{
            echo '<h1>Oops! Something went wrong.</h1>';
            echo '<p><b>Refresh</b> token was not regenerated.</p><pre>';
            print_r($this_response_arr);
            echo '</pre>';
        }

    }

?>
        <br />
        PHP Code to get all <b><u>Leads</u></b>:<br />
        <pre>
        $all_lead_records = get_records("Leads");
        print_r( $all_lead_records );
<?php
//        $all_lead_records = get_records("Leads");
//        print_r( $all_lead_records['data'][0] );
?>
        </pre>
        <br />
        PHP Code to get a specific <b><u>Lead</u></b>:<br />
        <pre>
        $this_lead_record = get_records("Leads", "78290000004647043");
        print_r( $this_lead_record );
<?php
        // if no lead exists with this ID then this will return blank
//        $this_lead_record = get_records("Leads", "78290000004647043");
//        print_r($this_lead_record);
?>
        </pre>
        <br />
        PHP Code to update or insert a <b><u>Lead</u></b> record in ZohoCRM:<br />
        <pre>
        // set lead name
        $data_array['data'][0]['First_Name']                = json_encode("John");
        $data_array['data'][0]['Last_Name']                 = json_encode("Smith");

        // build JSON post request
        $data_array['data'][0]['Email']                     = json_encode("This email address is being protected from spambots. You need JavaScript enabled to view it.");
        $data_array['data'][0]['Mobile']                    = json_encode("+44 1234 567 890");

        // merge to a JSON array to post
        $data_json          = json_encode($data_array);

        // prepare cURL variables
        $access_token       = base64_decode( file_get_contents( $access_token_path ) );
        $zoho_target_url    = $zoho_apis_eu . '/crm/v2/Leads/upsert';
        $zoho_header        = array('Authorization: Zoho-oauthtoken '.$access_token, 'Content-Type: application/json');

        // just do it
        $response_json      = abZohoApi($zoho_target_url, $data_json, $zoho_header, 'POST');

        // output response (optional)
        echo $response_json;
        </pre>
    </body>
</html>

Additional Notes
Note that in the above, the tokens are stored in Base64 encoded strings. You could add an actual encryption method (recommended) but for simplicity I'm using the built-in base64_encode function.

Update 2020

Please read the above article to understand the basic process which is:
  1. Register app to generate client ID/secret
  2. Get Grant Token (value of variable called "Code")
  3. Accept permissions of app with your Zoho login
  4. Use refresh token to generate access token
My quickest solution to this is simply to upload the following 2 files to any webserver (doesn't have to be the client's who will be using this API).

Redirect URI file
I give the web URL to this file as the redirect_uri value in a token request. This redirect simply forwards all the received data (via GET method) on to our main script which will do everything else.
<?php
	header('Location: https://localhost/get_tokens.php?' . http_build_query($_GET));
?>

Get Tokens file
Not all of what this file does is necessary. I'm just posting the entire file of what I use. It doesn't have any identifying or data-sensitive info, speaking of which, you can remove the info outputs as I also use this script to explain OAuth2.0 and getting access to a ZohoCRM.

As an overview it does the following:
  1. Displays a HTML form for you to select your datacenter (EU / COM / COM.CN / etc)
  2. Displays a HTML form for you to enter the client ID, client secret, scope(s), redirect url
  3. Submit form and receive the CODE variable from the URL (GET method)
  4. You get redirected to an app permissions page where you need to login with the Zoho account that belongs to the organization and can authorize this app.
  5. The script will then display the refresh_token and uses this to generate an access_token value.
  6. Uses the access_token and gets the last 2 contact records added to your ZohoCRM.
<?php
/*
	---------------------------------------------------------------------------
	Zoho Authorization via oAuth2.0 for REST API v2
	---------------------------------------------------------------------------

    Zoho API v2:	https://accounts.zoho.eu/developerconsole
                                https://accounts.zoho.com/developerconsole

	Documentation:	https://www.zoho.com/crm/help/api/v2/

	Available Scopes
	    users           users.all
	    org		org.all
	    settings	settings.all, settings.territories, settings.custom_views, settings.related_lists,
	    			settings.modules, settings.tab_groups, settings.fields, settings.layouts,
	    			settings.macros, settings.custom_links, settings.custom_buttons, settings.roles,
	    			settings.profiles
	    modules	modules.all, modules.approvals, modules.leads, modules.accounts, modules.contacts,
	    			modules.deals, modules.campaigns, modules.tasks, modules.cases, modules.events,
	    			modules.calls, modules.solutions, modules.products, modules.vendors,
	    			modules.pricebooks, modules.quotes, modules.salesorders, modules.purchaseorders,
	    			modules.invoices, modules.custom, modules.dashboards, modules.notes,
	    			modules.activities, modules.search

	Possible Module Names
		leads, accounts, contacts, deals, campaigns, tasks, cases, events, calls, solutions, products, vendors, pricebooks, quotes, salesorders, purchaseorders, invoices, custom, notes, approvals,	dashboards, search,activities
*/
header("Content-Type: text/html");
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);

// init
$b_Sandbox = false;
$v_AccessTokenPath = $v_RefreshTokenPath = "";
$a_CheckFields = $a_GrantFields = $a_Header = $a_Payload = array();
$v_RootFolder = "../App_Data/ab";  // must be writeable by script (and inaccessible by public)
$v_TmpFolder = $v_RootFolder . "/tmp";
$v_DelFolder = $v_TmpFolder . "/delete_me";
$v_WorkingFolder = $v_TmpFolder;
$v_TmpFile = "grant_info_" . date("YmdH") . ".dat";
$v_Protocol = $_SERVER['HTTPS']=="on" ? "https" : "http";
$v_RedirectUri = $v_Protocol . "://" . $_SERVER['SERVER_NAME'] . "/zohoapi/redirect_uri.php";
$v_Separator = "<hr style='border:3px solid #eee;' />";

// used for data samples
$v_CrmEndpoint = ($b_Sandbox) ? "https://sandbox.zohoapis." . $v_TLD . "/crm/v2" : "https://www.zohoapis." . $v_TLD . "/crm/v2";
$v_DataEndpoint = "https://inventory.zoho.com/api/v1/items?organization_id=012345678"; // used to get sample data

// create temp folder if not exists
if (!file_exists($v_TmpFolder)) {
    mkdir($v_TmpFolder, 0777, true);
}
if (!file_exists($v_DelFolder)) {
    mkdir($v_DelFolder, 0777, true);
}

// get top level domain
if(isset($_GET['location'])){
    switch($_GET['location']){
        case "us":
            $v_TLD = "com";
            break;
        case "eu":
            $v_TLD = "eu";
            break;
        default:
            $v_TLD = "com";
    }
}else{
    $v_TLD = isset($_GET['tld']) ? strtolower($_GET['tld']) : "none";
}

// if client id in url then get and store
if(isset($_GET['client_id'])){
    if($_GET['client_id'] != ""){
        file_put_contents($v_TmpFolder . "/" . $v_TmpFile, json_encode($_GET, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
    }
}

// begin HTML output
echo "<html><head><title>JoelLipman - API v2 Tokenizer (" . strtoupper($v_TLD) . ")</title><style>";
$v_AppStyle = "body{font-family:Arial,Verdana,Sans-serif;background-color:#ccc;}select,option,input.txt{padding:5px 10px;outline:0;border:1px solid#ccc;border-radius:5px;}input.btn{padding:10px 20px;outline:0;color:#fff;background:#10bc83;border-radius:2px;border:0;-webkit-box-shadow:2px 2px 2px 0px rgba(51,51,51,0.3);-moz-box-shadow:2px 2px 2px 0px rgba(51,51,51,0.3);box-shadow:2px 2px 2px 0px rgba(51,51,51,0.3);cursor:pointer;}input.txt{width:400px;}form{margin:20px;}h3{margin:0;}select{float:right;}td{padding:2px 5px;}label{display:inline-block;width:100px;}div.panel{background-color:#fff;border-radius:10px;margin:20px 10px;padding:20px 10px;-webkit-box-shadow:5px 5px 5px 0px rgba(51,51,51,0.3);-moz-box-shadow:5px 5px 5px 0px rgba(51,51,51,0.3);box-shadow:5px 5px 5px 0px rgba(51,51,51,0.3);}form{margin:0;padding:0;}#copyright{bottom:0;margin:0 auto;text-align:center;width:97%;margin-bottom:10px;}#copyright a{color:#999;text-decoration:none;font-size:10pt;line-height:20px;}#menu_bottom a{color:#f00;text-decoration:none;bottom:30;font-size:10pt;line-height:20px;}.centered{text-align:center;margin:0 auto;}h5{color:#999;font-weight:100;font-size:10pt;margin-bottom:5px;margin-top:10px;}tt.source{color:#fff;background-color:#000;}tt.target{background-color:yellow;}tt.bold{color:red;font-weight:700;}ul{margin:10px 0 0 85px;font-size:75%;color:#666;padding:0;}pre{margin:0;}";
$v_AppStyleFormatted = trim(preg_replace('/\s+/', ' ', $v_AppStyle));
$a_ReplaceFrom1 = array("px ", "0 ", " a");
$a_ReplaceTo1 = array("px?", "0?", "?a");
$v_AppStyleFormatted = str_replace($a_ReplaceFrom1, $a_ReplaceTo1, $v_AppStyleFormatted);
$a_ReplaceFrom2 = array(" ", "?");
$a_ReplaceTo2 = array("", " ");
$v_AppStyleFormatted = str_replace($a_ReplaceFrom2, $a_ReplaceTo2, $v_AppStyleFormatted);
echo $v_AppStyleFormatted . "</style></head><body>";
if((isset($_GET['tld']) || isset($_GET['location']))&&(file_exists($v_TmpFolder . "/" . $v_TmpFile))){
    //
    // 0. initialize vars
    //
    echo "<div class='panel'>";
    echo "<h3>0. INITIALIZE...</h3>";
    echo "<h5>Info:</h5>";
    $v_AuthEndpoint = "https://accounts.zoho." . $v_TLD . "/oauth/v2/auth";
    $v_TokenEndpoint = "https://accounts.zoho." . $v_TLD . "/oauth/v2/token";
    if(file_exists($v_TmpFolder . "/" . $v_TmpFile) && isset($_GET['code'])){
        $m_GrantInfo = json_decode(file_get_contents($v_TmpFolder . "/" . $v_TmpFile), true);
        $a_CheckFields['client_id'] = $m_GrantInfo['client_id'];
        $a_CheckFields['client_secret'] = $m_GrantInfo['client_secret'];
        $a_CheckFields['scopes'] = $m_GrantInfo['scopes'];
        $a_CheckFields['tld'] = $m_GrantInfo['tld'];
        $a_CheckFields['redirect_uri'] = $m_GrantInfo['redirect_uri'];
        rename($v_TmpFolder . "/" . $v_TmpFile, $v_DelFolder . "/" . $v_TmpFile);
    }else{
        $a_CheckFields['client_id'] = $_GET['client_id'];
        $a_CheckFields['client_secret'] = $_GET['client_secret'];
        $a_CheckFields['scopes'] = $_GET['scopes'];
        $a_CheckFields['tld'] = $_GET['tld'];
        $a_CheckFields['redirect_uri'] = $_GET['redirect_uri'];
    }
    $a_InitFields['tld'] = $a_CheckFields['tld'];
    $a_InitFields['client_id'] = $a_CheckFields['client_id'];
    $a_InitFields['client_secret'] = $a_CheckFields['client_secret'];
    $a_InitFields['scopes'] = $a_CheckFields['scopes'];
    $a_InitFields['redirect_uri'] = $a_CheckFields['redirect_uri'];
    $a_Info = array();
    $a_Info['api']['endpoint'] = $v_AuthEndpoint;
    $a_Info['api']['header'] = false;
    $a_Info['api']['ssl_check'] = false;
    $a_Info['sandbox'] = $b_Sandbox ? "<tt class='bold'>true</tt>" : "<tt class='bold'>false</tt>";
    echo "<pre>" . json_encode(array_merge($a_InitFields,$a_Info), JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
    //
    // 1. get grant token
    //
    echo "</div><div class='panel'>";
    echo "<h3>1. GRANT TOKEN...</h3>";
    if($a_CheckFields['client_id']!="")
    {
        $v_GrantTokenPath = $v_RootFolder . $a_CheckFields['client_id'] . "/grant_token.dat";
        $v_RefreshTokenPath = $v_RootFolder . $a_CheckFields['client_id'] . "/refresh_token.dat";
        $v_AccessTokenPath = $v_RootFolder . $a_CheckFields['client_id'] . "/access_token.dat";
        $v_WorkingFolder = $v_RootFolder . $a_CheckFields['client_id'];
        if (!file_exists($v_WorkingFolder)) {
            mkdir($v_WorkingFolder, 0777, true);
        }
    }
    echo "<h5>Info:</h5>";
    $a_Info = array();
    $a_Info['api']['endpoint'] = $v_AuthEndpoint;
    $a_Info['api']['method'] = "GET";
    $a_Info['api']['header'] = false;
    $a_Info['api']['ssl_check'] = false;
    $a_Info['expires'] = "<tt class='bold'>" . "in 1 to 10 minutes" . "</tt>";
    $a_Info['date_created'] = file_exists($v_GrantTokenPath) ? date("Y-m-d H:i:s P", filectime($v_GrantTokenPath)) : date("Y-m-d H:i:s P");
    $a_Info['date_modified'] = file_exists($v_GrantTokenPath) ? date("Y-m-d H:i:s P", filemtime($v_GrantTokenPath)) : date("Y-m-d H:i:s P");
    $a_Info['date_expires'] = file_exists($v_GrantTokenPath) ? date("Y-m-d H:i:s P", filemtime($v_GrantTokenPath) + 300) : date("Y-m-d H:i:s P", strtotime(time() + 300));
    echo "<pre>" . json_encode($a_Info, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
    echo "<h5>Request (GET):</h5>";
    $a_Request = array();
    $a_Request['client_id'] = $a_CheckFields['client_id'];
    $a_Request['redirect_uri'] = $a_CheckFields['redirect_uri'];
    $a_Request['scope'] = $a_CheckFields['scopes'];
    $a_Request['response_type'] = "code";
    $a_Request['access_type'] = "offline";
    $a_Request['prompt'] = "consent";
    $a_Request['state'] = "testing";
    echo "<pre>" . json_encode($a_Request, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
    $v_GrantTokenUrl = $v_AuthEndpoint . "?" . http_build_query($a_Request);
    if(isset($_GET['code']))
    {
        echo "<h5>Response (GET):</h5>";
        $a_Response = array();
        $a_Response['state'] = $_GET['state'];
        $a_Response['code'] = $_GET['code'];
        $a_Response['location'] = $_GET['location'];
        $a_Response['accounts-server'] = $_GET['accounts-server'];
        if($v_GrantTokenPath != "" && $b_Sandbox){
            file_put_contents($v_GrantTokenPath, json_encode($a_Response, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
        }
        foreach($a_Response as $v_Key => $v_Value){
            if($v_Key == "code"){
                $a_Response['code'] = "<tt class='source'>" . $v_Value . "</tt>";
            }
        }
        echo "<pre>" . json_encode($a_Response, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        //
        // 2. get refresh token
        //
        echo "</div><div class='panel'>";
        echo "<h3>2. REFRESH TOKEN...</h3>";
        echo "<h5>Info:</h5>";
        $a_Info = array();
        $a_Info['api']['endpoint'] = $v_TokenEndpoint;
        $a_Info['api']['method'] = "POST";
        $a_Info['api']['header'] = false;
        $a_Info['api']['ssl_check'] = false;
        $a_Info['expires'] = "<tt class='bold'>" . "Unless revoked or overwritten, a refresh token NEVER expires." . "</tt>";
        $a_Info['date_created'] = file_exists($v_RefreshTokenPath) ? date("Y-m-d H:i:s P", filectime($v_RefreshTokenPath)) : date("Y-m-d H:i:s P");
        $a_Info['date_modified'] = file_exists($v_RefreshTokenPath) ? date("Y-m-d H:i:s P", filemtime($v_RefreshTokenPath)) : date("Y-m-d H:i:s P");
        echo "<pre>" . json_encode($a_Info, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        echo "<h5>Request (POST):</h5>";
        $a_Payload = array();
        $a_Payload['code'] = $_GET['code'];
        $a_Payload['client_id'] = $a_CheckFields['client_id'];
        $a_Payload['client_secret'] = $a_CheckFields['client_secret'];
        $a_Payload['redirect_uri'] = $a_CheckFields['redirect_uri'];
        $a_Payload['grant_type'] = "authorization_code";
        $h_Curl=curl_init();
        $a_CurlOptions = array(
            CURLOPT_URL => $v_TokenEndpoint,
            CURLOPT_CUSTOMREQUEST => "POST",
            CURLOPT_POSTFIELDS => $a_Payload,
            CURLOPT_HEADER => 0,
            CURLOPT_VERBOSE => 0,
            CURLOPT_RETURNTRANSFER => 1,
            CURLOPT_SSL_VERIFYPEER => 0,
            CURLOPT_SSL_VERIFYHOST => 0
        );
        curl_setopt_array($h_Curl, $a_CurlOptions);
        $v_RefreshCurl = curl_exec($h_Curl);
        curl_close($h_Curl);
        foreach($a_Payload as $v_Key => $v_Value){
            if($v_Key == "code"){
                $a_Payload['code'] = "<tt class='target'>" . $v_Value . "</tt>";
            }
        }
        echo "<pre>" . json_encode($a_Payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        echo "<h5>Response (POST):</h5>";
        $a_RefreshResponse = json_decode($v_RefreshCurl, true);
        $v_RefreshToken = "ERROR";
        if($v_RefreshTokenPath != ""){
            file_put_contents($v_RefreshTokenPath, json_encode($a_RefreshResponse, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
        }
        if(isset($a_RefreshResponse['refresh_token']))
        {
            $v_RefreshToken = $a_RefreshResponse['refresh_token'];
            foreach($a_RefreshResponse as $v_Key => $v_Value){
                if($v_Key == "refresh_token"){
                    $a_RefreshResponse['refresh_token'] = "<tt class='source'>" . $v_Value . "</tt>";
                }
            }
            echo "<pre>" . json_encode($a_RefreshResponse, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        }else{
            echo "<pre>ERROR: Check your submitted variables are the same: Client ID, Client Secret, Redirect URI, Scope(s)</pre>";
        }
        //
        // 3. get access token
        //
        echo "</div><div class='panel'>";
        echo "<h3>3. ACCESS TOKEN...</h3>";
        echo "<h5>Info:</h5>";
        $a_Info = array();
        $a_Info['api']['endpoint'] = $v_TokenEndpoint;
        $a_Info['api']['method'] = "POST";
        $a_Info['api']['header'] = false;
        $a_Info['api']['ssl_check'] = false;
        $a_Info['expires'] = "<tt class='bold'>" . "in 1 hour after creation" . "</tt>";
        $a_Info['date_created'] = file_exists($v_AccessTokenPath) ? date("Y-m-d H:i:s P", filectime($v_AccessTokenPath)) : date("Y-m-d H:i:s P");
        $a_Info['date_modified'] = file_exists($v_AccessTokenPath) ? date("Y-m-d H:i:s P", filemtime($v_AccessTokenPath)) : date("Y-m-d H:i:s P");
        $a_Info['date_expires'] = file_exists($v_AccessTokenPath) ? date("Y-m-d H:i:s P", filemtime($v_AccessTokenPath) + 3600) : date("Y-m-d H:i:s P", strtotime(time() + 3600));
        echo "<pre>" . json_encode($a_Info, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        echo "<h5>Request (POST):</h5>";
        $a_Payload = array();
        $a_Payload['refresh_token'] = $v_RefreshToken;
        $a_Payload['client_id'] = $a_CheckFields['client_id'];
        $a_Payload['client_secret'] = $a_CheckFields['client_secret'];
        $a_Payload['redirect_uri'] = $a_CheckFields['redirect_uri'];
        $a_Payload['grant_type'] = "refresh_token";
        $h_Curl=curl_init();
        $a_CurlOptions = array(
            CURLOPT_URL => $v_TokenEndpoint,
            CURLOPT_CUSTOMREQUEST => "POST",
            CURLOPT_POSTFIELDS => $a_Payload,
            CURLOPT_HEADER => 0,
            CURLOPT_VERBOSE => 0,
            CURLOPT_RETURNTRANSFER => 1,
            CURLOPT_SSL_VERIFYPEER => 0,
            CURLOPT_SSL_VERIFYHOST => 0
        );
        curl_setopt_array($h_Curl, $a_CurlOptions);
        $v_AccessCurl = curl_exec($h_Curl);
        curl_close($h_Curl);
        $a_AccessResponse = json_decode($v_AccessCurl, true);
        if($v_AccessTokenPath != ""){
            file_put_contents($v_AccessTokenPath, json_encode($a_AccessResponse, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
        }
        foreach($a_Payload as $v_Key => $v_Value){
            if($v_Key == "refresh_token"){
                $a_Payload['refresh_token'] = "<tt class='target'>" . $v_Value . "</tt>";
            }
        }
        echo "<pre>" . json_encode($a_Payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        echo "<h5>Response (POST):</h5>";
        if(isset($a_AccessResponse['access_token'])){
            $v_AccessToken = $a_AccessResponse['access_token'];
            foreach($a_AccessResponse as $v_Key => $v_Value){
                if($v_Key == "access_token"){
                    $a_AccessResponse['access_token'] = "<tt class='source'>" . $v_Value . "</tt>";
                }
            }
            echo "<pre>" . json_encode($a_AccessResponse, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        }else{
            echo "<pre>ERROR: Check the Refresh Token</pre>";
        }
        //
        // 4. get data sample (last 2 records)
        //
        echo "</div><div class='panel'>";
        echo "<h3>4. DATA SAMPLE...</h3>";
        echo "<h5>Info:</h5>";
        $a_Info = array();
        $a_Info['api']['endpoint'] = $v_DataEndpoint;
        $a_Info['api']['method'] = "GET";
        $a_Info['api']['header'] = true;
        $a_Info['api']['ssl_check'] = false;
        $a_Info['date'] = date("Y-m-d H:i:s P");
        echo "<pre>" . json_encode($a_Info, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        echo "<h5>Request (GET):</h5>";
        $a_Header = array();
        $a_Header[] = "Authorization: Zoho-oauthtoken " . $v_AccessToken;
        $a_Header[] = "Content-Type: application/x-www-form-urlencoded;charset=UTF-8";
        $a_Payload = array();
        $a_Payload['page'] = 1;
        $a_Payload['per_page'] = 2;
        $a_Payload['sort_by'] = "id";
        $a_Payload['sort_order'] = "D";  // in CRM this is "asc" or "desc".  in books/inventory this is either "A" or "D"
        $a_Request = array("header" => $a_Header, "parameters" => $a_Payload);
        $h_Curl=curl_init();
        $a_CurlOptions = array(
            CURLOPT_URL => $v_DataEndpoint . "&" . http_build_query($a_Payload),
            CURLOPT_CUSTOMREQUEST => "GET",
            CURLOPT_HEADER => 1,
            CURLOPT_HTTPHEADER => $a_Header,
            CURLOPT_VERBOSE => 0,
            CURLOPT_RETURNTRANSFER => 1,
            CURLOPT_SSL_VERIFYPEER => 0,
            CURLOPT_SSL_VERIFYHOST => 0
        );
        curl_setopt_array($h_Curl, $a_CurlOptions);
        $v_DataCurl = curl_exec($h_Curl);
        foreach($a_Request as $v_Key => $v_Value){
            if($v_Key == 0){
                $a_Request['header'][0] = str_replace("Zoho-oauthtoken ","Zoho-oauthtoken <tt class='target'>", $a_Request['header'][0]) . "</tt>";
            }
        }
        echo "<pre>" . json_encode($a_Request, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        echo "<h5>Response (GET):</h5>";
        if(stripos($v_DataCurl, '"code"')>0){
            $v_DataStart = stripos($v_DataCurl, '{"code"');
            $v_DataStr = trim(substr($v_DataCurl, $v_DataStart));
            $a_DataResponse = json_decode($v_DataStr, true);
            $a_WriteJson['response'] = $a_DataResponse;
            echo "<pre>" . json_encode($a_WriteJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        }elseif(stripos($v_DataCurl, '"data"')>0){
            $v_DataStart = stripos($v_DataCurl, '{"data"');
            $v_DataStr = trim(substr($v_DataCurl, $v_DataStart));
            $a_DataResponse = json_decode($v_DataStr, true);
            $v_Index = 0;
            foreach($a_DataResponse['data'] as $a_Contact){
                $a_WriteJson['records'][$v_Index]['id'] = $a_Contact['id'];
                $a_WriteJson['records'][$v_Index]['name'] = $a_Contact['Full_Name'];
                $a_WriteJson['records'][$v_Index]['email'] = $a_Contact['Email'];
                $a_WriteJson['records'][$v_Index]['created_time'] = str_replace("+", " +", str_replace("T", " ", $a_Contact['Created_Time']));
                $v_Index++;
            }
            echo "<pre>" . json_encode($a_WriteJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "</pre>";
        }else{
            echo "<pre>" . $v_DataCurl . "</pre>";
        }
        echo "</div>";
    }
    else
    {
        echo "<br /><form>";
        echo '<input type="button" value="Click here to open App Permissions »" onclick="javascript:top.location.href=\'' . $v_GrantTokenUrl . '\';" class="btn" style="margin-left: 70px;" />';
        echo "</form>
        <ul>
        <li>Login as a Zoho User with access to the API</li>
        <li>Read and click on accept to allow the app access.</li>
        <li>There is a limit of 5 refresh tokens per minute.</li>
        <li>There is a limit of 19 refresh tokens per app.</li>
        <li>The 20th refresh token will overwrite the 1st.</li>
        </ul><br />";
    }

    // END
    echo "</div><div class='panel'>";
    echo "<h5>Script finished.</h5>";
    echo "<div id='menu_bottom' style='position:normal'><a href='./get_tokens.php' target='_top'>» Start Over</a></div></div>";
    echo "<div id='copyright' style='position:normal'><a href='//joellipman.com/' target='_top'>Copyright © ".date("Y")." Joel Lipman Ltd</a></div></div>";
}else{
    $v_TldSelected = "";
    $a_TldOptions = array('com','com.au','com.cn','eu','in');
    echo "
<div class='panel'><form method='get' action='get_tokens.php'>
    <h3>Configure App</h3>
    <table>
    <tr><td colspan='2'>Please select the domain your ZohoCRM is on:<br /><i style='font-size:75%;'>(eg. https://crm.zoho.com = COM, https://crm.zoho.eu = EU)</i>   <select name='tld' id='tldSelect'>";
    foreach($a_TldOptions as $v_Option){
        $v_SelectedStr = (strtolower($v_TLD) == $v_Option) ? " selected='true'" : "";
        echo "<option value='" . strtolower($v_Option) . "'" . $v_SelectedStr . ">" . strtoupper($v_Option) . "</option>";
    }
    echo "
    </select></td></tr>
    <tr><td colspan='2' class='centered'><input type='button' value='Open Zoho App Registration' onclick='javascript:openAppConsole();' class='btn' /></td></tr>
    <tr><td colspan='2'> </td></tr>
    <tr><td>Client ID</td><td><input name='client_id' value='' class='txt' onclick='this.select();' /></td></tr>
    <tr><td>Client Secret</td><td><input name='client_secret' value='' class='txt' onclick='this.select();' /></td></tr>
    <tr><td>Scopes</td><td><input name='scopes' value='ZohoCRM.modules.ALL,ZohoCRM.settings.READ,ZohoCRM.users.READ' class='txt' onclick='this.select();' /></td></tr>
    <tr><td>Redirect URI</td><td><input name='redirect_uri' value='" . $v_RedirectUri . "' class='txt' onclick='this.select();' /></td></tr>
    <tr><td colspan='2' style='text-align:center;'><input type='submit' value='Get Grant Token' class='btn' /></td></tr>
    </table>
</form></div>
<script>
function openAppConsole() {
    var e = document.getElementById('tldSelect');
    var strOption = e.options[e.selectedIndex].value;
    window.open('https://accounts.zoho.' + strOption + '/developerconsole', '_blank', 'location=yes,height=600,width=800,scrollbars=yes,status=yes');
}
</script>
";
    echo "<div id='copyright' style='position:fixed'><a href='//joellipman.com/' target='_top'>Copyright © ".date("Y")." Joel Lipman Ltd</a></div>";
}
echo "</body></html>";


Warning(s):
  • Do not have multiple staff using this file at the same time.
  • Make a note of the refresh_token as this is all you need to access data (use it to generate an access_token).
    • The refresh token does NOT expire unless overwritten or revoked.
    • There is a limit of 5 refresh tokens per minute.
    • There is a limit of 19 refresh tokens per app.
    • The 20th refresh token will overwrite the 1st.
  • An access token will last 1 hour.

Minimal Upsert
I'm including the code below in this article because it is possibly clearer to understand (and sometimes I need a quick reference):
<?php
// init
$v_DataEndpoint = "https://sandbox.zohoapis.com/crm/v2/CustomModuleApiName/upsert";
$v_AccessToken = "<ENTER_YOUR_ACCESS_TOKEN_VALUE_HERE>";
$a_Header = array('Authorization: Zoho-oauthtoken '.$v_AccessToken, 'Content-Type: application/json');

// set record mandatory field(s) value
$a_Data['data'] = array();
$a_Data['data'][0]['Name'] = "Upsert Test via PHP/cURL Script";

// setup cURL request
$h_cURL=curl_init();
curl_setopt($h_cURL, CURLOPT_HEADER, 0);
curl_setopt($h_cURL, CURLOPT_HTTPHEADER, $a_Header);
curl_setopt($h_cURL, CURLOPT_VERBOSE, 0);
curl_setopt($h_cURL, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($h_cURL, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($h_cURL, CURLOPT_SSL_VERIFYHOST, 0);
curl_setopt($h_cURL, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($h_cURL, CURLOPT_POSTFIELDS, json_encode($a_Data));
curl_setopt($h_cURL, CURLOPT_URL, $v_DataEndpoint);
$r_cURL = curl_exec($h_cURL);
if (curl_exec($h_cURL) === false) {
	$v_Output = curl_error($h_cURL);
} else {
	$v_Output = $r_cURL;
}
curl_close($h_cURL);

// output
echo $v_Output;

Related Articles

Joes Revolver Map

Joes Word Cloud

Accreditation

Badge - Certified Zoho Creator Associate
Badge - Certified Zoho Creator Associate

Donate & Support

If you like my content, and would like to support this sharing site, feel free to donate using a method below:

Paypal:
Donate to Joel Lipman via PayPal

Bitcoin:
Donate to Joel Lipman with Bitcoin - Valid till 8 May 2022 3QnhmaBX7LQSRsC9hh6Je9rGQKEGNQNfPb
© 2021 Joel Lipman .com. All Rights Reserved.