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:

  1. Click on "Add Client"
  2. Choose "Self Client" as the client type
  3. 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:

  1. Click on "Generate Code"
  2. Select the scopes you need (for Zoho Books, you'll want scopes like ZohoBooks.fullaccess.all or more specific ones based on your needs using ZohoBooks.{{scope_name}}.{{operation_type}})
  3. Set the time duration (don't worry, this is just for the initial code)
  4. 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.ca because 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 tab Client Secret)
  • {{client_secret}} with your Self Client Secret (available in the API Console, under the tab Client 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.

2026-01-30

Thank you for reading! If you have any questions or feedback, please feel free to contact us at hi@davette.ca.

$