Integrating Zoho API v2 in a Ruby on Rails application
I had previously integrated a client’s web application with the Zoho API and learned that they were planning to sunset the version 1 of the API at the end of 2019. So I set about figuring out a way to integrate V2 with our application. v2 of the Zoho API now uses oAuth, and is substantially different from v1. I took a look to see if there was a gem that would serve us as well as the rubyzoho gem had for v1 but didn’t find anything that looked easy to use or comprehensive. You could take a look at the zohohub gem, it looked promising.
Step 1: Register a client
The first step was to register a client as explainined in the Zoho docs. I then took note of the key and secret and stored those in environment variables. I use the dotenv gem to manage ENV variables in the development environment.
Step 2: Self-client
After that I used the self-client of generating a grant token request as explained in the Zoho API docs…. The scopes field could look something like this:
ZohoCRM.modules.custom.all,ZohoCRM.settings.all,ZohoCRM.modules.contacts.all,ZohoCRM.modules.all,ZohoCRM.users.all
Step 3: Getting access and refresh tokens
I then built myself a little utility class to get access and refresh tokens, and I plugged in the grant token that I had got from the self-client option above:
# Can be used to get access and refresh tokens when provided with a grant token from the self-client option
# Usage: Zoho::UseGrantTokenUtil.call(grant_token_goes_here)
class Zoho::UseGrantTokenUtil
include HTTParty
# debug_output - uncomment to get verbose output from HTTParty
# @param code [String] get a code from self -client util of Zoho CRM and then use it here
def self.call(code)
options = {
body: {
grant_type: "authorization_code",
client_id: ENV['ZOHO_CLIENT_ID'],
client_secret: ENV['ZOHO_SECRET'],
code: code,
redirect_uri: "http://localhost:3000"
}
}
self.post('https://accounts.zoho.com/oauth/v2/token', options)
end
end
I used this utility class to generate a refresh token which I then also stored in an ENV variable
Step 4: Getting access tokens using the refresh token
I decided to store access tokens in Redis along with an indication of when they expired. If I don’t have a currently active access token stored in Redis, we get a fresh one from Zoho using the refresh token that is stored in our ENV variable, and then store fresh one in Redis.
# This is used to get access tokens out of Redis,
# or renew them using the refresh token and store them to Redis
#
# Usage:
# token = Zoho::Tokens.get
#
class Zoho::Tokens
include HTTParty
# debug_output
require "redis"
def self.get
redis = Redis.new
token = redis.get("zoho:access_token")
if token && JSON.parse(token)["expires"].to_i >= Time.zone.now.to_i
JSON.parse(token)["token"]
else
Zoho::Tokens.refresh_token
end
end
def self.refresh_token
redis = Redis.new
options = {
body: {
grant_type: "refresh_token",
client_id: ENV['ZOHO_CLIENT_ID'],
client_secret: ENV['ZOHO_SECRET'],
refresh_token: ENV['ZOHO_REFRESH_TOKEN']
}
}
result = self.post('https://accounts.zoho.com/oauth/v2/token', options)
if result.code == 200
token = result.parsed_response["access_token"]
expires_in_sec = result.parsed_response["expires_in_sec"]
token_json = {
token: token,
expires: (Time.zone.now + expires_in_sec).to_i
}.to_json
redis.set("zoho:access_token", token_json)
return token
else
return false
end
end
end
Step 5: Base class for interacting with Zoho modules
Once I had a way of getting an access token easily set up, I built a base class that I could inherit from for most of what I needed to do with Zoho, and I can then subclass it for various modules. Some require more customization than others – users for example is inexplicably different than other modules and Zoho users may have custom modules and fields… This base class works much like ActiveRecord, I can instantiate a new object, passing in the required attrs, I can then save it and update it. I can create directly, can find based on an object’s id, can list all items for a module, or can do dynamic findby and findall_by method to look up a certain attribute.
# Any class that inherits from Zoho base
# should have a module name that matches the module in Zoho
# eg Zoho::Contacts
# and should define attributes that it wants to work with eg attributes :email, :name, etc...
#
# Classes that inherit from this get:
# find_by_{field} class method eg Zoho::Contacts.find_by_email "foo@bar.com"
# find_all_by_{field} class method eg Zoho::Contacts.find_by_email "foo@bar.com"
# find(id) class method eg Zoho::Contacts.find abc123
# all class method eg Zoho::Contacts.all
# save instance method
# update instance method
# create instance method
#
# Usage:
# z = Zoho::{module_name}.new(attrs hash goes here eg email: "foo@bar.com)
# z.save
#
class Zoho::Base
include HTTParty
include ActiveModel::AttributeAssignment
# debug_output
attr_accessor :id
def initialize(h = {})
h.each {|k,v| public_send("#{k}=",v) }
end
# @return [String] The base Zoho API url
def self.zoho_api_url
"https://www.zohoapis.com/crm/v2"
end
# @return [String] The Zoho API url for this module eg Contacts
def self.module_url
@module_url ||= File.join(self.zoho_api_url, self.module_name)
end
# Subclasses that require special processing for some attrs eg relationships
# @return [Array] Subclasses will use this to specify an array of attributes that require special handling
def self.special_attributes
[]
end
def self.attributes(*attributes)
@attributes ||= []
return @attributes - special_attributes unless attributes
a = attributes - special_attributes
attr_accessor(*a)
@attributes += a
end
def attributes
self.class.attributes - self.class.special_attributes
end
# Httparty options
# @return [Hash] a hash of options for Httparty request
def self.options
{headers: {"Authorization": "Zoho-oauthtoken #{Zoho::Tokens.get}"}}
end
# Making it easier to add Zoho search criteria to Httparty options
# @param q [String] A Zoho search criteria string
# @return [Hash] Httparty options with query params added
def self.query(q)
options.merge(query: {criteria: q})
end
# This is getting the Zoho module name from our class eg Zoho::Contacts become Contacts
# @return [String] The Zoho module name
def self.module_name
name.split('::')[1]
end
# @return [Array] Array of all Zoho items for this class
def self.all(**args)
url = module_url
# allowing extra query params to be added
if args.present?
url << "?#{args.first[0]}=#{args.first[1]}"
args.shift
args.each do |arg|
url << "&#{args.first[0]}=#{args.first[1]}"
end
end
result = get(url, options)
if result.code == 200
result_to_objects_array(result)
else
raise "Error getting data from Zoho: #{result.message}"
end
end
# Finds a single item from Zoho
# @param criteria [String] Valid Zoho search criteria
# @return [Zoho::{module}] Instance of the object
def self.find(id)
url = File.join(module_url, id)
result = get(url, options)
if result.code == 200
hash_to_object result.parsed_response["data"].first
else
raise "Error getting data from Zoho: #{result.message}"
end
end
# @param criteria [String] Valid Zoho search criteria
# @return [Hash] Non-symbolized hash of data
def self.search(criteria)
result = get(
File.join(module_url, "search"),
query(criteria)
)
if result.code == 200
result_to_objects_array(result)
else
return false
end
end
# This is taking HTTParty result data array and for each item in the array it creates a new object eg a Zoho::Contact object
# @return [Array] An array of objects
def self.result_to_objects_array(result)
data = result.parsed_response["data"]
data.inject([]) { |arr, item| arr << hash_to_object(item) }
end
# Using method missing here to define dynamic find by methods such as find_by_email
def self.method_missing(m, *args, &block)
if m.to_s.match("find_all_by")
field = m.to_s.split("find_all_by_")[1].split('_').map(&:titlecase).join('_')
a = args[0]
result = search("(#{field}:equals:#{a})")
return result
elsif m.to_s.match("find_by")
field = m.to_s.split("find_by_")[1].split('_').map(&:titlecase).join('_')
a = args[0]
result = search("(#{field}:equals:#{a})")
return result.first if result
else
raise "NO MATCH ON FINDER METHOD #{m}"
end
end
# this takes httparty hash and returns an instance object of this class with attrs assigned
# @param hash_item [Hash] Object in hash form
# @return [Zoho::Base] instance of the object
def self.hash_to_object(hash_item)
h = attributes.inject({}) { |attrs, a|
c = a.to_sym == :id ? "id" : converted_attribute(a)
val = hash_item[c]
attrs[a] = val
attrs
}
new(h)
end
# This is taking a symbol attr such as :account_name
# and converting it to a string that Zoho understands eg Account_Name
# @param a [Symbol] A symbolized attribute name
# @return [String] attribute name in Zoho form
def self.converted_attribute(a)
a.to_s.split('_').map(&:titlecase).join('_')
end
# Subclasses that require special processing will overwrite this
# @return [Hash] Subclasses will use this to modify the params for Zoho
def params_plus_special_processing(data)
return data
end
# this build JSON data for Zoho to ingest
# @return [String] A string of JSON data for Zoho
def params_for_zoho
params = attributes.inject({}) { |hash, attribute|
c = Zoho::Base.converted_attribute(attribute)
val = self.public_send(attribute)
hash[c] = val if val.present?
hash
}
params = params_plus_special_processing(params)
data = {data: [params]}
data.to_json
end
# Saves a record to Zoho
# @return [Zoho::{module}] returns the object being saved
def save
result = self.class.post(self.class.module_url,
self.class.options.merge(body: params_for_zoho)
)
if [200,201].include? result.code
created_item_id = result.parsed_response['data'].first['details']['id']
self.id = created_item_id
return self
else
raise "#{result.code} Error doing save to Zoho: #{result.message}"
end
end
# Creates a record in Zoho when passes in attrs for a new object
# @param attrs [Hash] Hash of params to create the object
# @return [Zoho::{module}] returns the object being saved
def self.create(attrs)
new(attrs).save
end
# updates the current record in Zoho
# @return [Zoho::{module}] returns the object being saved
def update
raise "ID REQUIRED" and return unless id.present?
result = self.class.put(
File.join(self.class.module_url, id),
self.class.options.merge(body: params_for_zoho)
)
if [200, 201].include? result.code
return self
else
raise "#{result.code} Error getting contacts from Zoho: #{result.message}"
end
end
end
Step 6: Inheriting from the base class for individual modules
I can then subclass it like this, for example:
# Usage
#
# Get all accounts:
# accounts = Zoho::Acounts.all
#
# Find one account
# account = Zoho::Accounts.find_by_account_name({account_name})
#
class Zoho::Accounts < Zoho::Base
# any Zoho fields you want assigned to found objects and sent to Zoho should be specified here
attributes :id, :account_name, :account_type
end
I have a good deal of experience integrating various APIs – including Zoho – into Ruby on Rails and Elixir/Phoenix applications. I would be happy to talk if you need something like this for your project. Get in touch!