Authenticating Zoho Books API Without User Input
The Problem with OAuth 2.0
If you've ever tried to integrate with the Zoho Books API, you've probably run into this frustrating situation: the documentation says you need OAuth 2.0 authentication. And if you're familiar with OAuth, you know what that means—redirecting users to authorize your app, handling callbacks, managing token expiration, and constantly refreshing tokens.
But here's the thing: what if you just want to run a server-side script or automate some tasks? You don't want to deal with user authorization flows. You just want to authenticate once and be done with it.
And gladly, there's a much simpler way using Zoho's Self Client feature that lets you get a refresh token that remains valid indefinitely (unless manually revoked).
The Solution: Zoho Self Client
Zoho Self Client is designed exactly for this use case—when you're building an internal tool or automation that doesn't need users to authorize anything. Here's how to set it up.
Step 1: Create a Self Client App
Head over to the Zoho Developer Console and create a new Self Client application:
- Click on "Add Client"
- Choose "Self Client" as the client type
- Give it a name and create it
Once created, you'll get a Client ID and Client Secret. Keep these handy—you'll need them soon.
Step 2: Generate a Token with Your Desired Scope
This is where you define what permissions your application needs. In the Self Client interface:
- Click on "Generate Code"
- Select the scopes you need (for Zoho Books, you'll want scopes like
ZohoBooks.fullaccess.allor more specific ones based on your needs usingZohoBooks.{{scope_name}}.{{operation_type}}) - Set the time duration (don't worry, this is just for the initial code)
- Generate the code
You'll get a code that looks like a long string of random characters. Copy it immediately—it's only valid for a short time.
Step 3: Exchange the Code for a Refresh Token
Now comes the magic part. Make a POST request to get your refresh token:
POST https://accounts.zohocloud.ca/oauth/v2/token
?code={{code}}
&client_id={{client_id}}
&client_secret={{client_secret}}
&grant_type=authorization_code
Note: We're using
accounts.zohocloud.cabecause our Zoho instance is hosted in the Canadian data center. Depending on your data center location, you may need to use a different domain (e.g.,accounts.zoho.com,accounts.zoho.eu,accounts.zoho.in, etc.). Check Zoho's data center documentation to find the correct domain for your instance.
Replace:
{{code}}with the code you just generated{{client_id}}with your Self Client ID (available in the API Console, under the tabClient Secret){{client_secret}}with your Self Client Secret (available in the API Console, under the tabClient Secret)
The response will look something like this:
{
"access_token": "1000.abc123...",
"refresh_token": "1000.def456...",
"scope": "...",
"api_domain": "https://www.zohoapis.ca",
"token_type": "Bearer",
"expires_in": 3600
}
Save that refresh_token somewhere safe! This token won't expire on its own and you'll reuse it in your app logic. Store it securely in environment variables or a secrets manager—never commit it to version control.
Step 4: Use the Refresh Token to Get Access Tokens
Now whenever you need to make an API call, you'll request a new access token using your refresh token:
POST https://accounts.zohocloud.ca/oauth/v2/token
?refresh_token={{refresh_token}}
&client_id={{client_id}}
&client_secret={{client_secret}}
&grant_type=refresh_token
This returns a fresh access token that's valid for one hour:
{
"access_token": "1000.xyz789...",
"scope": "...",
"api_domain": "https://www.zohoapis.ca",
"token_type": "Bearer",
"expires_in": 3600
}
Use this access_token in your API requests with the Authorization: Zoho-oauthtoken {access_token} header.
Pro Tip: Cache Your Access Token
Since access tokens are valid for an hour, you don't need to request a new one for every API call. That would be wasteful and might hit rate limits.
Instead, cache the access token along with its expiration time. Only request a new one when the current token is about to expire. Here's a simple pattern:
let cachedToken = null;
let tokenExpiry = null;
async function getAccessToken() {
// If we have a valid cached token, use it
if (cachedToken && tokenExpiry && Date.now() < tokenExpiry) {
return cachedToken;
}
// Otherwise, get a fresh token
const params = new URLSearchParams({
refresh_token: process.env.ZOHO_REFRESH_TOKEN,
client_id: process.env.ZOHO_CLIENT_ID,
client_secret: process.env.ZOHO_CLIENT_SECRET,
grant_type: "refresh_token",
});
const response = await fetch(
`https://accounts.zohocloud.ca/oauth/v2/token?${params}`,
{ method: "POST" }
);
if (!response.ok) {
const error = await response.json();
throw new Error(
`Failed to refresh token: ${error.error || response.statusText}`
);
}
const data = await response.json();
cachedToken = data.access_token;
tokenExpiry = Date.now() + data.expires_in * 1000 - 60000; // Refresh 1 min early
return cachedToken;
}
Wrapping Up
The Zoho Self Client approach turns what could be a complicated OAuth dance into a simple, set-it-and-forget-it solution. You authenticate once, get your long-lived refresh token, and you're good to go. Perfect for background jobs, scheduled tasks, or any server-side automation.
No more dealing with authorization callbacks or constantly refreshing credentials. Just straightforward API access the way it should be.
Thank you for reading! If you have any questions or feedback, please feel free to contact us at hi@davette.ca.