Kaizen #192 - Implementing Custom Token Persistence in Python SDK

Kaizen #192 - Implementing Custom Token Persistence in Python SDK


Welcome back to another week of Kaizen!

Last week, we discussed how to implement Login with Zoho using OAuth 2.0 and saw how to bring it to life in a real-world application with the Zoho CRM Python SDK. We also discussed how Zylker Academy built a custom student portal powered by Zoho authentication.
In our sample project, we used the file-based persistence method, a simple setup where the access and refresh tokens are stored in a local file. While this method is great for getting started, it might not always fit your business requirements.
That is why our SDKs offer multiple ways to persist your tokens. 

This week, we will explore why token persistence matters for your app’s secure operation, and how to implement custom token persistence methods, including a practical example using SQLite.

Why does token persistence matter?

When a user logs in via OAuth, Zoho returns two tokens:
  • An access token (valid for one hour), used to access Zoho CRM data.
  • A refresh token, used to get a new access token when the current one expires.
If your app does not store these tokens properly, your users will be forced to log in again every time they make an API call. Or every time their access token gets expired. That is not inconvenient; it is a poor user experience.
When you use Zoho CRM SDKs, this is all handled for you behind the scenes. When you first authenticate with Zoho, the SDK stores your access and refresh tokens. Later, when a token expires, the SDK automatically uses the refresh token to get a new one. All you have to do is configure and initialize the SDK, and you are ready to start making API calls using the different methods offered by our SDKs! 

From the user’s perspective, it means:
  • They do not have to log in every time.
  • Their sessions are automatically renewed without interruption.
  • Token revocation can be done centrally.
From a developer’s perspective:
  • You can control how and where tokens are stored.
  • You have control to enforce policies like session timeouts or token cleanup.

Supported token persistence options

The Zoho CRM SDKs support three token persistence mechanisms:

File Persistence:

As we have already seen in last week's Kaizen, in this method, the tokens are stored in a local file of your choice. This can be configured while configuring and initializing the SDK. While this is simple and great for internal and local use, it might not always meet the needs of a growing business. For instance, if the file gets deleted or corrupted, you lose the tokens. It also poses a security risk, as storing tokens in files may expose them to unauthorised access if the file is not properly secured.

Database Persistence:

This stores tokens in a MySQL database, making it better suited for production environments. It is more robust and can handle larger-scale user management. 
Using this persistence method, you can only provide the following connection parameters - host, DB name, table name, username, password, and port number. 

Custom Persistence:

But what if neither of these options fits your needs? Maybe you are working in an environment without traditional storage like AWS's Secret Manager, or you prefer any other Database, or running a microservice in a container where local storage is more practical. That is where Custom Token Persistence comes in.

Custom Token Persistence

Custom persistence means you can implement your own logic for storing and retrieving OAuth tokens, instead of relying on the SDK’s default mechanism. To do this, you should create a class that implements the TokenStore interface and override a standard set of methods, each handling a specific part of the token lifecycle.

Here’s what your custom class must implement:
Method
Purpose
Return Type
find_token(self, token)
Given a token, return a full Token (OAuthToken) object from storage. Used before making any CRM API call.
Token(OAuthToken) object
save_token(self, token)
Called right after Zoho returns a new access/refresh token. Your implementation must persist it.
None
delete_token(self, id)
Delete a specific token using its unique ID.
None
get_tokens(self)
Return all stored tokens.
A list of Token(OAuthToken) objects
delete_tokens()
Delete all stored tokens. Useful during cleanup or logout.
None
find_token_by_id(id)
Retrieve a token by its unique identifier.
Token(OAuthToken) object

The token object is an instance of OAuthToken. The SDK will invoke these methods automatically as part of its flow. You just have to focus on where and how to store the tokens. With this, you can persist tokens to any storage as long as your class handles these methods correctly.

Understanding the token object

Before we dive deeper into custom token persistence, let's clarify what this token (OAuthToken) object is and how you should work with it.

The token object is an instance of OAuthToken.  This class bundles all the credentials and details the SDK needs to authenticate your API requests. Here’s what it holds:
  • access_token
  • refresh_token
  • client_id 
  • client_secret
  • redirect_url
  • expires_in
  • user_signature
  • id
  • api_domain

Implementing Custom Token Persistence with SQLite

Now that we've covered the basics of token persistence and how Zoho SDK supports custom stores, let’s dive into a practical, real-world example using SQLite as the backend for storing tokens.
SQLite is a lightweight, file-based database engine. It is perfect when you want a persistent store without the complexity of a full database server.

The CustomStoreSQLite Class

This class implements all six required methods of the TokenStore interface using SQLite as the backend. 

1. Initialization and Table Setup

When you create a CustomStoreSQLite object, it immediately checks if the token table exists in the SQLite database file zohooauth.db. If the DB or the table is missing, its __init__() method creates one with all the necessary columns to store token details like id, user_name, client_id, client_secret, refresh_token, access_token, grant_token, expiry_time, redirect_url and api_domain.


 def __init__(self):
        """
        Initializes the SQLite database and sets up the oauthtoken table if needed.
        """
        self.db_name = 'zohooauth.db'
        if not self.check_table_exists():
            connection = sqlite3.connect(self.db_name)
            cursor = connection.cursor()
            cursor.execute("CREATE TABLE  oauthtoken (id varchar(10) NOT NULL,user_name varchar(255), client_id "
                           "varchar(255), client_secret varchar(255), refresh_token varchar(255), access_token "
                           "varchar(255), grant_token varchar(255), expiry_time varchar(20), redirect_url varchar("
                           "255), api_domain varchar(255), primary key (id))")
 cursor.close()

This means the first time your app runs, it sets up its own database schema automatically.

2. Saving a Token - save_token(self, token)

Purpose:
This method is called every time Zoho returns a new token, whether after a login or a token refresh. Your implementation is responsible for safely persisting this token, typically by upserting (inserting or updating) a row in your database that uniquely identifies the token’s user and client combination.

Expected behaviour: 
The method must store the token in your custom database or storage system.
  • If a matching token already exists (based on user, refresh token, or client credentials), it should be updated.
  • If no match exists, a new entry must be created.
  • Tokens should not be duplicated. Multiple users should be managed separately.
Input Parameters: An instance of Token(OAuthToken) class containing details like access token, refresh token, user signature, client ID/secret, etc.

Return value: None. But must raise exceptions on failure.

Sample Implementation using SQLite:
Here is the logic used in the implementation of save_token() method:
  • If the user name is available, use it to update the token.
  • If no user name but the access token is available in the table, update by the access token.
  • If there is a refresh or grant token with the same client credentials, then update accordingly.
  • If none of these match, insert as a new row.

def save_token(self, token):
        if not isinstance(token, OAuthToken):
            return
        cursor = None
        connection = None
        try:
            connection = sqlite3.connect(self.db_name)
            oauth_token = token
            query = "update oauthtoken set "
            if oauth_token.get_user_signature() is not None:
                name = oauth_token.get_user_signature().get_name()
                if name is not None and len(name) > 0:
                    query = query + self.set_token(oauth_token) + " where user_name='" + name + "'"
            elif oauth_token.get_access_token() is not None and len(oauth_token.get_access_token()) > 0 and \
                    self.are_all_objects_null([oauth_token.get_client_id(), oauth_token.get_client_secret()]):
                query = query + self.set_token(
                    oauth_token) + " where access_token='" + oauth_token.get_access_token() + "'"
            elif ((oauth_token.get_refresh_token() is not None and len(oauth_token.get_refresh_token()) > 0) or
                  (oauth_token.get_grant_token() is not None and len(
                      oauth_token.get_grant_token()) > 0)) and oauth_token.get_client_id() is not None \
                    and oauth_token.get_client_secret() is not None:
                if oauth_token.get_grant_token() is not None and len(oauth_token.get_grant_token()) > 0:
                    query = query + self.set_token(
                        oauth_token) + " where grant_token='" + oauth_token.get_grant_token() + "'"
                elif oauth_token.get_refresh_token() is not None and len(oauth_token.get_refresh_token()) > 0:
                    query = query + self.set_token(
                        oauth_token) + " where refresh_token='" + oauth_token.get_refresh_token() + "'"
            query = query + " limit 1"
            try:
                cursor = connection.cursor()
                cursor.execute(query)
                if cursor.rowcount <= 0:
                    if oauth_token.get_id() is not None or oauth_token.get_user_signature() is not None:
                        if oauth_token.get_refresh_token() is None and oauth_token.get_grant_token() is None \
                                and oauth_token.get_access_token() is None:
                            raise SDKException(Constants.TOKEN_STORE, Constants.GET_TOKEN_DB_ERROR1)
                    if oauth_token.get_id() is None:
                        newId = str(self.generate_id())
                        oauth_token.set_id(newId)
                    query = "insert into oauthtoken (id,user_name,client_id,client_secret,refresh_token,access_token," \
                            "grant_token,expiry_time,redirect_url,api_domain) values (?,?,?,?,?,?,?,?,?,?);"
                    val = (token.get_id(),
                           token.get_user_signature().get_name() if token.get_user_signature() is not None else None,
                           token.get_client_id(), token.get_client_secret(), token.get_refresh_token(),
                           token.get_access_token(), token.get_grant_token(), token.get_expires_in(),
                           token.get_redirect_url(), token.get_api_domain())
                    cursor.execute(query, val)
            except Error as e:
                raise e
            finally:
                connection.commit()
                cursor.close() if cursor is not None else None
                connection.close() if connection is not None else None
        except Exception as ex:
 raise SDKException(Constants.TOKEN_STORE, Constants.SAVE_TOKEN_DB_ERROR, cause=ex)

3: Fetching a Token - find_token(self, token)

Purpose:
This is the method the SDK calls whenever it needs to make an API call on behalf of a user, but has only partial token information.
Depending on the token flow - Grant Token, Refresh Token, Access Token, or ID-based - only a specific token or ID may be provided during the API call. In such cases, find_token(self, token) method locates and return the complete OAuthToken object from storage if a matching one exists. If no matching token exists in the storage, this method will return None, and the SDK will proceed to generate a new token with the provided details and save it using the save_token(self, token) method. 

Expected behavior:
  • Based on the available details in the input token (user name, access token, refresh or grant token), this method should query storage and return a complete token object.
  • If no match is found, it should return None.
Input Parameters: A partially filled Token(OAuthToken) object.

Return value: A fully populated Token object if found, or None.

Sample Implementation using SQLite:
The find_token(self, token) method implementation does the following:
  • Dynamically builds a WHERE clause based on available attributes.
  • Queries the database for a matching record.
  • Fetches the matching record, if any, and populates the Token object with the full set of stored values (access token, refresh token, expiry time, etc.).
  • Returns the Token object if a matching record is found, or return None.
Without this method, your app wouldn’t know which token to use during API calls. For example, consider the case when a user reopens your app after hours. You have their refresh token stored. The SDK calls find_token(self, token) to get the full token and proceeds without requiring a fresh login.

def find_token(self, token):
        cursor = None
        connection = None
        try:
            connection = sqlite3.connect(self.db_name)
            if isinstance(token, OAuthToken):
                oauth_token = token
                query = "select * from oauthtoken"
                if oauth_token.get_user_signature() is not None:
                    name = oauth_token.get_user_signature().get_name()
                    if name is not None and len(name) > 0:
                        query = query + " where user_name='" + name + "'"
                elif oauth_token.get_access_token() is not None and self.are_all_objects_null(
                        [oauth_token.get_client_id(), oauth_token.get_client_secret()]):
                    query = query + " where access_token='" + oauth_token.get_access_token() + "'"
                elif oauth_token.get_refresh_token() is not None or oauth_token.get_grant_token() is not None and \
                        oauth_token.get_client_id() is not None and oauth_token.get_client_secret() is not None:
                    if oauth_token.get_grant_token() is not None and len(oauth_token.get_grant_token()) > 0:
                        query = query + " where grant_token='" + oauth_token.get_grant_token() + "'"
                    elif oauth_token.get_refresh_token() is not None and len(oauth_token.get_refresh_token()) > 0:
                        query = query + " where refresh_token='" + oauth_token.get_refresh_token() + "'"
                query = query + " limit 1"
                cursor = connection.cursor()
                cursor.execute(query)
                result = cursor.fetchone()
                if result is None:
                    return None
                self.set_merge_data(oauth_token, result)
        except Exception as ex:
            raise SDKException(Constants.TOKEN_STORE, Constants.GET_TOKEN_DB_ERROR1, cause=ex)
        finally:
            cursor.close() if cursor is not None else None
            connection.close() if connection is not None else None
        return token

4: Deleting a Token - delete_token(self, id)

Purpose:
Delete a specific token record from storage based on a unique token ID. It is commonly used when a user logs out or an admin revokes access for a user.

Expected behaviour:
  • Locate the token record by its unique ID.
  • Delete the corresponding record from storage.
Input Parameters: The token ID to be deleted.

Return values: None

Sample Implementation using SQLite:

def delete_token(self, id):
        cursor = None
        try:
            connection = sqlite3.connect(self.db_name)
            try:
                cursor = connection.cursor()
                query = "delete from oauthtoken where id= " + id + ";"
                cursor.execute(query)
                connection.commit()
            except Error as ex:
                raise ex
            finally:
                cursor.close() if cursor is not None else None
                connection.close() if connection is not None else None
        except Error as ex:
            raise SDKException(code=Constants.TOKEN_STORE, message=Constants.DELETE_TOKEN_DB_ERROR, cause=ex)

5: Deleting All Tokens - delete_tokens(self)

Purpose: Delete all tokens from storage, typically used for global logout or cleanup scenarios.

Expected behaviour: Remove all token records from storage in a single operation.

Input Parameters: None

Return Values: None

Sample Implementation using SQLite:

def delete_tokens(self):
        cursor = None
        try:
            connection = sqlite3.connect(self.db_name)
            try:
                cursor = connection.cursor()
                query = "delete from oauthtoken;"
                cursor.execute(query)
                self.connection.commit()
            except Error as ex:
                raise ex
            finally:
                cursor.close() if cursor is not None else None
                connection.close() if connection is not None else None
        except Error as ex:
            raise SDKException(code=Constants.TOKEN_STORE, message=Constants.DELETE_TOKENS_DB_ERROR, cause=ex)

6: Fetch all tokens - get_tokens(self)

Purpose: Retrieve all currently stored tokens.

Expected behaviour:
  • Query storage for all token records.
  • Construct and return a list of token objects 
Input Parameters: None

Return Value: A list of Token objects representing all stored tokens.

Sample Implementation using SQLite:


def get_tokens(self):
        cursor = None
        try:
            connection = sqlite3.connect(self.db_name)
            tokens = []
            try:
                cursor = connection.cursor()
                query = "select * from oauthtoken;"
                cursor.execute(query)
                results = cursor.fetchall()
                for result in results:
                    oauth_token = object.__new__(OAuthToken)
                    self.set_oauth_token(oauth_token)
                    self.set_merge_data(oauth_token, result)
                    tokens.append(oauth_token)
                return tokens
            except Error as ex:
                raise ex
            finally:
                cursor.close() if cursor is not None else None
                connection.close() if connection is not None else None
        except Error as ex:
            raise SDKException(code=Constants.TOKEN_STORE, message=Constants.GET_TOKENS_DB_ERROR, cause=ex)

7. Finding a Token by ID - find_token_by_id(self, id)

Purpose: Retrieve a specific token by its unique id.

Expected behaviour:
  • Search storage for a token with the given ID.
  • If found, return the complete token object; if not, return None.
Input Parameters: The unique identifier of the token (id)

Return Values: Returns a fully populated Token(OAuthToken) object if found; otherwise, returns None.

Sample Implementation using SQLite:
This method should follows a similar pattern to find_token, but use the unique id as the search key.

 def find_token_by_id(self, id):
        cursor = None
        try:
            connection = sqlite3.connect(self.db_name)
            try:
                query = "select * from oauthtoken where id='" + id + "'"
                oauth_token = object.__new__(OAuthToken)
                self.set_oauth_token(oauth_token)
                cursor = connection.cursor()
                cursor.execute(query)
                results = cursor.fetchall()
                if results is None or len(results) <= 0:
                    raise SDKException(Constants.TOKEN_STORE, Constants.GET_TOKEN_BY_ID_DB_ERROR)
                for result in results:
                    self.set_merge_data(oauth_token, result)
                    return oauth_token
            except Error as ex:
                raise ex
            finally:
                cursor.close() if cursor is not None else None
                connection.close() if connection is not None else None
        except Error as ex:
            raise SDKException(code=Constants.TOKEN_STORE, message=Constants.GET_TOKEN_BY_ID_DB_ERROR, cause=ex)


Please find the complete custom_store_sqlite.py file here.

How to use this in your project

To start using this custom token persistence class in your own Python project, follow these steps:
  • Download the custom_store_sqlite.py and place this inside your project directory.
  • Import the class in the script where you initialize the SDK. In our sample project, this is the record.py file.
    from store.custom_store_sqlite import CustomStoreSQLite
  • In the SDK configuration, use the CustomStoreSQLite method instead of the FireStore method:

def init(self, client_id, code, location, redirect_url):
        environment = DataCenter.get(location)
        client_secret = "17565609051856218813123b9a98de52c301722b7d"
        logger = Logger.get_instance(level=Logger.Levels.INFO,
                                     file_path="./logs.txt")
        store = CustomStoreSQLite()
        token = OAuthToken(client_id=client_id,
                           client_secret=client_secret,
                           grant_token=code,
                           redirect_url=redirect_url)
        Initializer.initialize(environment=environment,
                               token=token,
                               logger=logger,
 store=store)

That’s it! With this, all token operations (save, fetch, delete) will be routed through your custom store backed by SQLite.


The above video demonstrates this is in action. You can see what the database looks like when populated. 

More Custom Persistence Implementations

The advantage of using Zoho CRM SDKs is that it doesn't box you in. You are free to implement token persistence in a way that fits your business logic, team expertise, or project requirements. Whether you prefer SQLite, NoSQL, or something entirely different, the SDK gives you full control through the TokenStore interface.

In the SQLite example above, we walked through how to implement a custom store using a persistent file-based database. You need to implement all the methods as explained in the previous section, no matter where you decide to persist your tokens. 

To make things easier, we have included two additional reference implementations:
  • An in-memory store, where tokens are stored in a dictionary
  • A list-based store, which keeps token records as simple lists
Each one fully implements the required methods of the TokenStore interface.

SQLite In-Memory DB

This implementation uses SQLite's in-memory mode (using ":memory:") to store tokens in RAM. Here, we have implemented all the required methods from the TokenStore interface: find_token(), save_token(), delete_token(), get_tokens(), delete_tokens() and find_token_by_id().

Please find the custom_store_in_memory.py file here.

List-Based Persistence Using Simple Lists

The second reference implementation is a list-based token store that keeps token records in an in-memory Python list of lists. Each inner list represents a token’s attributes, such as ID, user signature, client ID, access token, refresh token, and so on.
This custom store fully implements all required methods from the TokenStore interface.

Please find the custom_store_list.py file here.

We hope this was useful and gives you enough info to build your own token persistence methods tailored to your needs. We used Python SDK here, but you can apply the same logic with any of our other SDKs. It is all the same logic, just different programming languages. Just remember to implement the required methods exactly as expected by the SDK, as explained here.

Give it a try, and please let us know how it goes or if you hit any bumps!  Comment below, or send an email to support@zohocrm.com. We will be waiting to hear from you!

Happy coding!


We are excited to be approaching the 200th post in our Kaizen series! As we get closer to this milestone, we would love to hear from you. Have questions, suggestions, or topics you would like us to cover in our future Kaizen posts? Your feedback helps us make the series even better.
 
Please take a moment to share your thoughts with us using this form - we'd really appreciate it!


  Previous Kaizen: Kaizen #191 - Implementing "Login with Zoho" using Python SDKKaizen Directory                    


    • Sticky Posts

    • Kaizen #222 - Client Script Support for Notes Related List

      Hello everyone! Welcome to another week of Kaizen. The final Kaizen post of the year 2025 is here! With the new Client Script support for the Notes Related List, you can validate, enrich, and manage notes across modules. In this post, we’ll explore how
    • Kaizen #217 - Actions APIs : Tasks

      Welcome to another week of Kaizen! In last week's post we discussed Email Notifications APIs which act as the link between your Workflow automations and you. We have discussed how Zylker Cloud Services uses Email Notifications API in their custom dashboard.
    • Kaizen #216 - Actions APIs : Email Notifications

      Welcome to another week of Kaizen! For the last three weeks, we have been discussing Zylker's workflows. We successfully updated a dormant workflow, built a new one from the ground up and more. But our work is not finished—these automated processes are
    • Kaizen #152 - Client Script Support for the new Canvas Record Forms

      Hello everyone! Have you ever wanted to trigger actions on click of a canvas button, icon, or text mandatory forms in Create/Edit and Clone Pages? Have you ever wanted to control how elements behave on the new Canvas Record Forms? This can be achieved
    • Kaizen #142: How to Navigate to Another Page in Zoho CRM using Client Script

      Hello everyone! Welcome back to another exciting Kaizen post. In this post, let us see how you can you navigate to different Pages using Client Script. In this Kaizen post, Need to Navigate to different Pages Client Script ZDKs related to navigation A.
    • Recent Topics

    • Zoho Books Extension: What Happens If Custom Fields Already Exist?

      When developing Zoho Books extensions, what happens if the target Zoho Books organization already has a custom field with the same API name as one defined in the extension? I’m asking because we originally created an on-Books version of this functionality,
    • Access token generate from the refresh token not working for API

      Dear Sir/Madam, When I use my refresh token to obtain new access_token, that token return INVALID_TOKEN when the same API is called. I made sure that my api site is correct and match the auth web site. However the original access_token work fine.
    • Feature Request: Enable Custom PDF Layout Editor for All Modules (Including Package Slips)

      Hello Zoho Community and Product Team, I am writing to share a suggestion that would significantly enhance the customization capabilities within Zoho Books. We all appreciate the power of the Custom PDF Layouts (the "New" template engine) that allows
    • why does my campaign move back to draft?

      Every time I try to send my email campaign, it reverts back to Draft status.. this has happened three times in a row..how do i find out what the problem is? Thanks
    • Integration with...

      Dear Zoho Commerce team, Please could you consider the integration within Zoho Commerce / Inventory and Qapla'? (https://www.qapla.it/en/) This app is better than Aftership in many ways: - Aftership integration require PRO plan and price start from more
    • Peppol: Accept Bill (Belgium)

      Hi, This topic might help you if you're facing the same in Belgium. We are facing an issue while accepting a supplier bill received by Peppol in Zoho Books. There is a popup with an error message: This bill acceptance could not be completed, so it was
    • Remove the “One Migration Per User” Limitation in Zoho WorkDrive

      Hi Zoho WorkDrive Team, Hope you are doing well. We would like to raise a critical feature request regarding the Google Drive → Zoho WorkDrive migration process. Current Limitation: Zoho WorkDrive currently enforces a hard limitation: A Zoho WorkDrive
    • CRM x WorkDrive: File storage for new CRM signups is now powered by WorkDrive

      Availability Editions: All DCs: All Release plan: Released for new signups in all DCs. It will be enabled for existing users in a phased manner in the upcoming months. Help documentation: Documents in Zoho CRM Manage folders in Documents tab Manage files
    • Service and Parts Line Item Limitations

      Hi FSM Team, We work with clients who deliver large-scale field service projects. In many cases, a single work order can contain 200+ service and parts line items. Currently, Zoho FSM limits work orders to 30 service and parts line items. While this works
    • Critical Need for Global Search in Zoho FSM

      Hello Zoho FSM Team, We are currently in the process of deciding whether to fully transition to Zoho FSM for managing our field service operations. At present, our team actively uses Zoho Desk (with over 50 users) and Service Fusion, which we are considering
    • 【Zoho CRM】やりとりの要約機能リリースのお知らせ

      ユーザーの皆さま、こんにちは。コミュニティチームの藤澤です。 今回は「Zoho CRM アップデート情報」の中から、新機能のやりとり要約機能をご紹介します。 Zoho CRMのやりとりの要約は、Ziaが生成する機能で、データに関連付けられた最近のやりとりを簡潔にまとめて提供します。 メール、通話、ミーティング、メモなどを1つの明確な要約にまとめ、重要なポイント、過去の話し合いの要点、結果、次のステップを表示することで、日常的な課題に対処します。 Ziaの強みは、単なる要約にとどまらず、内容の解釈も行える点です。やりとりの要約では、顧客との会話に潜む遅延、フォローアップ漏れ、約束、期限、感情、意図を特定します。
    • Recent post analytics not very useful

      Hi, I'm enjoying some aspects of the Zoho Social. like being able to schedule posts to multiple channels. However, the recent posts analytics is disappointing. For example, I put up a reel on Instagram and Facebook yesterday. Instagram insights shows
    • Scheduled Reports - Do not send empty report

      Hello, We are intensively using reports in the CRM, especially for sales managers.  When data is empty, they still receive an email. Can you add an option to avoid sending the report when data is empty?
    • Zoho CRM for Everyone's NextGen UI Gets an Upgrade

      Hello Everyone We've made improvements to Zoho CRM for Everyone's Nextgen UI. These changes are the result of valuable feedback from you where we’ve focused on improving usability, providing wider screen space, and making navigation smoother so everything
    • How to apply customized Zoho Crm Home Page to all users?

      I have tried to study manuals and play with Zoho CRM but haven't found a way how to apply customized Zoho CRM Home Page as a (default) home page for other CRM users.. How that can be done, if possible? - kipi Moderation Update: Currently, each user has
    • [Product Update] Locations module migration in Zoho Books integration with Zoho Analytics

      Dear Customers, As Zoho Books are starting to support an advance version of the Branches/Warehouses module called the Locations module, users who choose to migrate to the Locations module in Zoho Books will also be migrated in Zoho Analytics-Zoho Books
    • What happens to the files created by user, whose account is deleted.

      Hello, I created a folder in the My folders names "Quote" I shared the same with my colleague. She created some files and folders in that folder over the period of 1 and half year. Now she left company, and I deleted her account from Zoho. What happens
    • Internal Error When Accessing Team Inbox.

      All our users are seeing this error in teaminbox. Because its a critical tool kindly resolve this issue ASAP.
    • Sharing URLs and direct access

      Hello, I am storing my team's email signature images on Workdrive. I am creating a public image download share and adding “?directDownload=true” so that the image can be accessed without the Workdrive interface. A few questions: 1) Can we generate friendly
    • Zoho Mail Android app update: Calendar enhancements

      Hello everyone! In the most recent version of the Zoho Mail Android app update, we have introduced various new enhancements for the calendar module. Let's explore what's new. Drag and drop events to update the date and time We have enhanced the calendar
    • Enhance Delay Configuration in Zoho Flow

      Dear Zoho Flow Support Team, We are writing to request an improvement to the delay configuration process within Zoho Flow. Currently, users are required to manually enter the exact delay duration (e.g., "2 days") in the delay block. This can be time-consuming
    • Internal Server Error (500) When Attempting to View Banking Transactions

      I am experiencing an Internal Server Error (500) when attempting to view transactions across all of my banking accounts. Despite multiple attempts to resolve this, I have received little more than runaround from support, and the issue remains unresolved.
    • Contratação ProdutivosX

      Bem-vindo(a) ao processo de contratação da ProdutivosX. Este formulário tem como objetivo coletar informações essenciais para análise de perfil profissional, alinhamento de competências e possível integração ao equipamento ProdutivosX. A ProdutivosX é
    • Zoho Books - How to Invoke a Custom Function in Schedulers

      We have multiple schedulers that send emails to customers in batches. Currently, we are maintaining the same code across several schedulers. Is it possible to use a custom function inside a scheduler script? If yes, how can we invoke the custom function
    • Enhance Sign CRM integration

      Hello all, I'm working on a custom Deluge script to enhance the integration between Zoho CRM and Sign by using a writer merge template for additional flexibility. I want to replicate the post-sign document integration that exists between CRM and Sign
    • How create a draft via workflow?

      I wish to create a workflow rule for specific emails that creates a draft response - not an automatic email reply, but just a draft with a set response ready to be verified by an agent who can then manually select recipients. Alternatively, the workflow
    • Unified WhatsApp Number Management in Zoho Desk and SalesIQ

      Dear Zoho Desk Support Team, We are currently utilizing both Zoho Desk and Zoho SalesIQ for our customer support operations. While both platforms offer WhatsApp integration, we are facing challenges due to the requirement of separate WhatsApp numbers
    • Problem using Zoho Desk API

      Goodmorning, I am trying to use the Zoho Desk API to create a dashboard in Grafana, but I am having a problem. Following the instructions in the API documentation, I created the API Console application (server-based application). Then I created the string
    • Email Field Validation Incorrectly Rejects RFC-Compliant Addresses (Forward Slashes)

      I've encountered a validation issue with Zoho Creator's Email field that rejects RFC-compliant email addresses containing forward slashes, and I'm hoping the Zoho team can address this in a future update. The Issue When entering an email address containing
    • Support for Custom Fonts in Zoho Recruit Career Site and Candidate Portal

      Dear Zoho Recruit Team, I hope you're doing well. We would like to request the ability to use custom fonts in the Zoho Recruit Career Site and Candidate Portal. Currently only the default fonts (Roboto, Lato, and Montserrat) are available. While these
    • Customer ticket creation via Microsoft Teams

      Hi all, I'm looking to see if someone could point me in the right direction. I'd love to make it so my customers/ end users can make tickets, see responses and respond within microsoft teams. As Admin and an Agent i've installed the zoho assist app within
    • Whatsapp Connection Status still "Pending" after migration

      Hello, I migrated my WhatsApp API to Zoho from another provider a day ago. So far the connection status is still “Pending”. There is a problem? How long does it usually take?
    • Zoho Books Sandbox environment

      Hello. Is there a free sandbox environment for the developers using Zoho Books API? I am working on the Zoho Books add-on and currently not ready to buy a premium service - maybe later when my add-on will start to bring money. Right now I just need a
    • Plug Sample #15 - Enable Human-Like, Contextual Interactions in SalesIQ with Zia Agents

      Zia Agents are conversational AI assistants designed to understand user intent and respond intelligently, helping businesses automate conversations and offer personalized support at scale. While Zia Agents are yet to be publicly released, access is currently
    • Best way to schedule bill payments to vendors

      I've integrated Forte so that I can convert POs to bills and make payments to my vendors all through Books. Is there a way to schedule the bill payments as some of my vendors are net 30, net 60 and even net 90 days. If I can't get this to work, I'll have
    • Seamless Round-Trip Navigation for Related Blocks (Detail View)

      As highlighted previously in this post (and here, here, here, and here), we still lack a fundamental capability for seamless navigation in Related Blocks. The popup that appears when adding a related record doesn't exist for viewing/editing existing records,
    • Social Profile Logo Format

      Hello, I'm using Zoho Sites and am attempting to add a couple of social media accounts to my profile that aren't included in the default platforms that are available. What format is the logo required to be in? I have tried .png and .jpg with no success.
    • How to create Sepa Direct Debit XML file: solution and code

      Even though Books provides a payment integration for Stripe and Gocardless (in Europe) there are customers that want to use the Sepa services of their own bank. Mainly because Stripe and Gocardless are quite expensive. In that case they would need a Sepa
    • Traditional Spreadsheet vs Zoho Tables

      Hello everyone, This article explains the differences between Spreadsheet applications and no-code databases, such as Zoho Tables. While both tools belong to the "grid" family, the core difference lies in their purpose. A spreadsheet (such as Zoho Sheet,
    • RFQ MODEL

      A Request for quotation model is used for Purchase Inquiries to multiple vendors. The Item is Created and then selected to send it to various vendors , once the Prices are received , a comparative chart is made for the user. this will help Zoho books
    • Next Page