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 #198: Using Client Script for Custom Validation in Blueprint

      Nearing 200th Kaizen Post – 1 More to the Big Two-Oh-Oh! Do you have any questions, suggestions, or topics you would like us to cover in future posts? Your insights and suggestions help us shape future content and make this series better for everyone.
    • Kaizen #226: Using ZRC in Client Script

      Hello everyone! Welcome to another week of Kaizen. In today's post, lets see what is ZRC (Zoho Request Client) and how we can use ZRC methods in Client Script to get inputs from a Salesperson and update the Lead status with a single button click. In this
    • 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
    • Recent Topics

    • Zoho Mail will not set up in Thunderbird

      I am using Thunderbird 13.0.1 in Linux Mint 13 64-bit. I cannot set up my Zoho IMAP email in this client. This is evidently a common problem as evidenced by these postings in the Thunderbird forum: thunderbird can't seem to "find the settings" I cannot
    • Zoho Desk Partners with Microsoft's M365 Copilot for seamless customer service experiences

      Hello Zoho Desk users, We are happy to announce that Zoho Desk has partnered with Microsoft's M365 to empower customer service teams with enhanced capabilities and seamless experiences for agents. Microsoft announced their partnership during their keynote
    • The Social Wall: January 2026

      Hello everyone, We’re back with the first edition of The Social Wall of 2026. There’s a lot planned for the year ahead, and we’re starting with a few useful features and improvements released in January to help you get started. Create a GBP in Social
    • Pipeline: Copying rulesets from one data source to another

      When creating and editing data pipelines, it would be really helpful to be able to copy the ruleset from one data source and 'paste' it to another. This would save time and reduce manual mistakes.
    • Doubt about maximum email reach

      Good morning, greetings. This is Bramdon García from EDULABS S.A.S ESP, located in Colombia. I'm writing to inquire about the possibility of sending an email to 35,000 people simultaneously. Our company has a Zoho account, but we'd like to know if there's
    • Zoho calendar not working in browser

      Hello, I am new to Zoho. I have two accounts for two separate businesses. In one of them, calendar loads in a browser no problem. However, if I use this account (sairfeetmusic.co.uk) calendar does not load. I also cannot add it to my Thunderbird Lightening
    • Authentication Failure when adding POP3 accounts

      Hi everyone, I am a new user currently migrating from Gmail to Zoho Mail. I decided to make the switch following Google's decision to discontinue POP3 fetching. I previously used Gmail as my primary mail hub, and Zoho seems like the perfect alternative
    • Trident Application Folder

      Hi, How to choose installation folder for Trident, by default it gets installed in C drive. How to change drive?
    • Email disappeared to specific contact

      Good afternoon, this morning I emailed somebody. This email isn’t showing up in my sent folder. They sent me a response which I clicked on and it disappeared immediately. Why could this be?
    • Create an Eye-Catching Announcement Widget for Your Help Center

      Hello Everyone! In this week’s edition, let’s explore how to keep your customers updated with exciting news in the Help Center. See how ZylkerMobile wowed their customers by bringing updates right to their portal. ZylkerMobile, the renowned brand for
    • Updating records through Zoho Sheets View doesn't update timeline or trigger workflow rules

      I have noticed that when i update a series of record with the zoho sheets view (see here http://d.pr/i/ahnR) it doesn't update timeline history or trigger workflow rules. I am using it in the Deals module. Looking forward for some more info. Denis
    • Updation of Old Browsers Versions for Zoho CRM

      We have upgraded the default browser version for CRM to facilitate features like widgets and scripts that are often implemented aside from advanced functionalities for various business requirements. The latest version is aimed to provide better performance
    • Project-Based Inventory Consumption and Proforma Invoice in Zoho ERP

      While working with customers across different industries, we have identified a few functional questions and gaps that commonly arise—especially for project-based and service-oriented businesses. Many organizations, such as those in shipping, construction,
    • Zoho Desk domain Mapping not Working Showing CNAME Error

      I have created the subdomain and created the Cname as well as its instracted on the Zoho website  but when i try add the domain on help desk its showing error msg : Make sure you've mapped the CNAME entry and linked it to desk.cs.zohohost.com.au on your
    • Founders using Zoho — are you leveraging Zoho Campaigns + Zoho Social for thought leadership… or just sending emails?

      I’ve noticed something interesting in the Zoho ecosystem. Many founders use Zoho Campaigns and Zoho Social for basic marketing—newsletters, scheduled posts, and announcements. But very few are using these tools strategically to: • Position themselves
    • IMAP stopped working after enabling 2 factor authentication

      IMAP stopped working after enabling 2 factor authentication. Is there any solution for this?
    • Rename Service Report

      Some of our customers are requesting the name of the service report PDF to be in a specific format, for example, instead of REP-001.PDF some are requesting to include their name like customername.pdf. is that possible?
    • Outgoing emails rejected due to SpamCop RBL listing (IP 136.143.188.12)

      Hi All, I am writing to report a deliverability issue affecting outgoing emails from my Zoho Mail account. Recently, several messages sent from my domain (example.com) to external recipients have been rejected with the following error message (redacted
    • Share Record Ownership in Zoho Recruit

      We’re introducing User Fields in Zoho Recruit designed to make collaboration easier when multiple team members need to work on the same record. With User Fields, you can extend record ownership beyond a single user and enable smoother teamwork across
    • Recherche d'un développeur

      Bonjour, j'ai un projet de SAAS sur une base de zoho créator et zoho CRM et je recherche un développeur qualifié français pour créer l'application créator (fonctionnel et graphique) et les workflow et blueprint de CRM
    • Customer Parent Account or Sub-Customer Account

      Some of clients as they have 50 to 300 branches, they required separate account statement with outlet name and number; which means we have to open new account for each branch individually. However, the main issue is that, when they make a payment, they
    • Cloning a Pick List

      I have an existing Pick List in my Contacts that I want to drop into my Leads. Is there a way to copy or clone the field with it's accompanying Pick List? Thanks for your time. Moderation Update: While we are yet to support cloning action of modules and
    • I wish to upload 40000 Resumes in Zoho Recruit Database. Can I do this in batch of 1000 Resumes ?

      I would like to upload thousand or few hundred of resumes in Zoho Recruit in one go. Please let me know how can I do this Or migrate my 40000 resumes from previous ATS to Zoho Recruit.
    • Zoho Writer for Proposals

      Hi, one of the things we've struggled with since moving to Zoho ecosystem is our proposal software Qwilr does not integrate well. It surprises me Zoho doesn't have proposal software but given all the capabilities of Zoho Writer, I'm wonder if anyone is
    • Custom Fonts in Zoho CRM Template Builder

      Hi, I am currently creating a new template for our quotes using the Zoho CRM template builder. However, I noticed that there is no option to add custom fonts to the template builder. It would greatly enhance the flexibility and branding capabilities if
    • Foutmelding bij uitvoering/opslaan functie 'Left expression is of type TEXT and right expression is of type NUMBER'

      Hoi! Ik heb een workflow (zie bijlage) die wordt getriggerd zodra de verwachte weekomzet van een bedrijf wordt aangepast naar een bedrag hoger dan 0. Op dat moment moet een functie (zie bijlage) gaan berekenen wat het benodigde kredietlimiet moet zijn.
    • Hoe kun je Nederlandse loonstroken boek in Zoho Books?

      Beste Community, Heeft er iemand een idee hoe je standaard loonstroken kunt inboeken in Zoho Books? Ik ben benieuwd hoe jullie dit doen en wat de mogelijkheden zijn.
    • Unable to Filter Retail Sales Orders & Need Paid/Unpaid Filter – Zoho Books

      Hi Zoho Team, Recently you introduced Retail – Standard and Retail – Premium templates for Sales Orders. However, in the Sales Order module we still cannot filter or segregate Retail Sales Orders separately from normal B2B sales orders. Because of this,
    • Service op locatie organiseren met Zoho FSM: waar lopen organisaties tegenaan?

      Bij organisaties met service teams op locatie merken we vaak dat de complexiteit niet zozeer in de planning zelf zit, maar in wat er rond die planning gebeurt. Denk aan opvolging na interventies, consistente servicerapporten, en het bijhouden van installaties
    • Possible to delete the "Big Deal Alert" in Zoho CRM?

      Hi, Is it possible to delete the "Big Deal Alert" in Zoho CRM? My company has no need for it and I want to remove it to clean up my email templates list. Thank you. Moderation Update: Currently, the option to delete the "Big Deal Alert" template is in
    • Allow selection of select inactive users in User data fields

      Hello, We sometimes need to select a previous employee that has an inactive account in the User data field. For example, when doing database cleanup and indicating actions are done by a certain employee that weren't filled out when they were part of the
    • [Webinar] Top 10 Most Used Zoho Analytics Features in 2025

      Zoho Analytics has evolved significantly over the past year. Discover the most widely adopted features in Zoho Analytics in 2025, based on real customer usage patterns, best practices, and high-impact use cases. Learn how leading teams are turning data
    • Do buttons and vba msgbox work on mobile, specifially the iPhone zoho sheets app?

      In Zoho sheets on the web, I inserted a button and assigned a VBA macro to it. It pops up a msgbox with some text. When I go onto the iPhone mobile zoho sheets app, the button is there. When I click on that button, the spinning asterisk appears for a
    • [Webinar] Solving business challenges- Handling the challenge of transitioning to Zoho Writer from legacy tools

      Moving to Zoho Writer is a great way to consolidate your business tools and become more agile. With multiple accessibility modes, no-code automation, and extensive integration with business apps and content platforms, Zoho Writer helps solve your organization's
    • How can I effectively manage a website with your help?

      I’m wondering if it’s possible to develop a custom website with specific features using Zoho as an alternative platform. My goal is to create a website similar to https://tmsim.ph, with the same kind of functionality and user experience. I would truly
    • Introducing the Yes/No field: Binary decisions made beautiful

      Greetings, form architects! What would you do when you need a simple yes/no answer on your form? Normally, you add a Radio field. Type Yes. Type No. Until now. The new Yes/No field is purpose-built for binary decisions. It is preconfigured, visually consistent,
    • Move email between inboxes?

      Is it possible to move emails from one team inbox to another? We would like to be able to have a single "catch-all" inbox for incoming requests, and then move the email to the appropriate department inbox. I was hoping we would be able to accomplish this
    • The power of workflows in Zoho Marketing Automation - Video Webinar

      In this Zoho Marketing Automation video webinar, our experts walk you through: Why you may want to create marketing workflows How to create marketing workflows Use Zoho CRM data and apply workflows to automate your marketing strategy How workflows can
    • Zoho CRM's sales trend and sales follow-up trend dashboards are now customizable

      Dear Customers, We're here with good news! Sales trend and sales follow-up trend are two system-defined dashboards that help you understand trends and anomalies in your sales outreach and conversion efforts. They use Zia's intelligence to identify patterns
    • Introducing Rule-Based AI Coding Assistants for Zoho Finance Widgets

      Hello customers, We’ve introduced rule-based AI coding assistants to speed up Zoho Finance widget development. You can try them out in Cursor AI and GitHub Copilot. This helps you build widgets quickly using simple prompts, while ensuring the generated
    • Next Page