Web Tabs
Web Tabs are similar to browser tabs, but accessible within Zoho Books. You can open any web page or application that provides an embed URL directly inside Zoho Books, so your team can access external tools without switching tabs. You can also enable JWT authentication on a web tab so your external app can verify that requests came from Zoho Books for the right organization and user.
Insight: A JWT is a standard format for securely transmitting information as a signed JSON object. It consists of three parts: a header, a payload, and a signature, separated by dots.
Web tabs can be used in many ways depending on your business needs.
Scenario: Zylker Manufacturing uses a third-party ERP system to manage production planning and raw material costs. Their finance team tracks production runs in the ERP while managing purchase bills and payments in Zoho Books. They create a web tab pointing to the ERP’s URL so accountants can pull up production data without leaving Zoho Books.
Notes:
- If the web pages use ‘http,’ they won’t open in web tabs.
- Some of the web pages or applications cannot be opened using web tabs as they are prevented from opening in other applications. This is to prevent clickjacking attacks.
- The web tabs you create will neither be linked to any other module of Zoho Books nor affect their data.
Create a Web Tab
To create a new web tab:
- Go to Settings.
- Select Web Tabs under Customization.
- Click + New Web Tab in the top right corner.
- Enter a name for the web tab in the Tab Name field.
- Enter the URL of the external application in the URL field.
- To include dynamic values like the organization name or customer ID in the URL, click Insert Placeholders and select the values you need.
- Select This URL belongs to a Zoho app or website if the URL points to a Zoho product or website.
Scenario: Zylker Manufacturing embeds their ERP system as a web tab in Zoho Books. Without JWT authentication, the ERP has no way to identify which company opened the tab. Anyone with the URL could open it and potentially see another company’s production data. Enabling JWT authentication solves this: Zoho Books signs each request with a token that identifies Zylker’s organization and user, so the ERP loads only Zylker’s data.
- To verify that requests to your external app came from Zoho Books for the right organization and user, enable JWT Authentication.
- Enter a Secret Key between 32 and 500 characters. Zoho Books uses this key to sign each token it sends to your app. Keep it on your server only and do not include it in frontend code.
- Select a Token Validity period: 10 minutes, 30 minutes, 1 hour, or 3 hours (default).
- Under Visibility, select who can view this web tab:
- Only Me: Only you can see the web tab.
- Only Selected Users & Roles: Select specific users and roles from the dropdown that appears.
- Everyone: All users in your organization can see the web tab.
- Click Save.

After saving, the web tab appears in the left sidebar under Web Tabs. Click it to open your external app within Zoho Books.
Note: When JWT authentication is enabled, Zoho Books attaches a signed token to each request that opens the web tab. Your server verifies this token to confirm the request came from Zoho Books for the right organization and user. Tokens expire after the validity period you set. When a token expires, your web tab application can request a new one programmatically.
Edit a Web Tab
You can edit a web tab to update its name, URL, JWT authentication settings, or visibility. To edit a web tab:
- Go to Settings.
- Select Web Tabs under Customization.
- Click the web tab you want to edit, or hover over it, click the Dropdown icon, and select Edit.

- Make the necessary changes and click Save.
Validate a JWT Token
The Validate JWT Token feature lets you verify a token your app received from Zoho Books. Use it to confirm the token details or diagnose issues before making changes to your backend.
To validate a JWT token:
- Go to Settings.
- Select Web Tabs under Customization.
- Click the web tab you want to edit, or hover over it, click the Dropdown icon, and select Edit.
- Click Validate JWT Token in the top-right corner of the Edit Web Tab page.
- Paste your JWT token into the JWT Token field.
- Click Verify. Zoho Books verifies the token against the Secret Key saved on this web tab and shows one of these results:
| Result | What it means |
|---|---|
| Token is valid | The signature is valid and the token has not expired. Zoho Books shows the signature as Valid, the validity as Valid until {date and time}, and the decoded payload with the organization ID, user ID, token type, and web tab ID. |
| Token is invalid | The token signature is invalid. It was not signed with this web tab’s Secret Key, or the token was modified after it was issued. Zoho Books shows the signature as Invalid and the payload as Unverified, since it cannot be trusted. |
| Token has expired | The token’s expiry time has passed. Zoho Books shows the signature as Valid, but the validity shows Expired on {date and time}. Reload the web tab to get a new token. |
| tab_id does not match this web tab | The tab_id in the payload of this JWT Token doesn’t match your web tab. Zoho Books shows the signature as Valid and the validity as Valid until {date and time}, but the token belongs to a different web tab. |

Note: If you’re building the server integration for your web tab, see JWT Authentication on this page for payload claims, server-side validation steps, the token refresh flow, and code samples in Node.js, Python, Java, PHP, and Go.
Mark a Web Tab as Inactive
If you no longer need a web tab, you can mark it as inactive instead of deleting it. Inactive web tabs are hidden from the left sidebar and cannot be opened, but they can be marked as active later if needed.
To mark a web tab as inactive:
- Go to Settings.
- Select Web Tabs under Customization.
- Hover over the web tab you want to mark as inactive, click the Dropdown icon, and select Mark as Inactive.

Mark a Web Tab as Active
To mark an inactive web tab as active:
- Go to Settings.
- Select Web Tabs under Customization.
- Hover over the inactive web tab you want to mark as active, click the Dropdown icon, and select Mark as Active.

Delete a Web Tab
If you no longer need a web tab, you can delete it. Deleting a web tab removes it permanently from Zoho Books and cannot be undone.
To delete a web tab:
- Go to Settings.
- Select Web Tabs under Customization.
- Hover over the web tab you want to delete, click the Dropdown icon, and select Delete.

- Click OK in the confirmation pop-up.
JWT Authentication
Web tabs can be created under Settings for your organization users, under Customer Portal settings for your customers, and as components inside extensions built on the Zoho Books Developer Portal. You can enable JWT authentication on any of these web tabs so your external app can verify that requests came from Zoho Books for the right organization and user. The token mechanism is the same in all three cases.
How the Token Is Delivered
The JWT token is not appended to the web tab URL. Zoho Books delivers it to your app via postMessage from the Books parent window after your app loads in the iframe.
When a user opens the web tab, Zoho Books:
- Loads your app’s URL in the iframe.
- Posts the JWT token to your app using the ZOHO_WEBTAB_AUTH_TOKENS message type.
Your app receives the message and forwards the token to your backend for validation. Your server validates the token before rendering any data.
The message your app receives:
{
"type": "ZOHO_WEBTAB_AUTH_TOKENS",
"jwt_token": "eyJhbGciOiJIUzI1NiJ9..."
}When a session ends, Zoho Books sends jwt_token: null in the same message type.
JWT Payload Claims
After your server verifies the token signature, you can read the following claims from the payload:
| Claim | Description |
|---|---|
| organization_id | The Zoho Books organization ID. Map this to your tenant to load the right data. |
| user_id | The Books user ID for web tabs accessed by organization users, or the contact/customer ID for Customer Portal web tabs. |
| token_type | Always access. Reject any token where this value differs. |
| tab_id | The ID of the web tab this token was issued for. |
| iat | Issued-at time as a Unix timestamp in seconds. |
| exp | Expiry time as a Unix timestamp in seconds. Reject the token after this time. |
Token settings:
| Setting | Value |
|---|---|
| Algorithm | HS256 (HMAC-SHA256) |
| Signing key | The Secret Key configured on the web tab, as UTF-8 bytes |
| Format | Standard JWT (header.payload.signature) |
Token validity options:
| Token Validity setting | Lifetime |
|---|---|
| 10 minutes | 600 seconds |
| 30 minutes | 1800 seconds |
| 1 hour | 3600 seconds |
| 3 hours (default) | 10800 seconds |
What JWT Validation Protects
Validating the JWT token confirms that:
- The web tab request was generated by Zoho Books.
- The token was signed using the Secret Key configured for that web tab.
- The token was not modified in transit.
- The token has not expired.
- The request belongs to the expected web tab, organization, and user.
Do not trust placeholder values or JWT claim values until signature verification and expiry checks pass.
Validate on Your Server
Run these checks in order on every JWT token your frontend receives via postMessage:
- Reject if the token is missing or blank.
- Verify the signature with your web tab Secret Key and algorithm HS256.
- Reject if exp is in the past.
- Reject if token_type is not access.
- Optionally reject if tab_id does not match your web tab ID.
- Only then use organization_id and user_id to load data.
Do not render sensitive content before these checks pass.
Token Refresh
JWT tokens expire after the Token Validity period you set (default is 3 hours). Your app does not call Zoho APIs directly to get a new token. Instead, it sends a postMessage to the Books parent window, and Books returns a new token.
| Step | Who | Action |
|---|---|---|
| 1 | Your app | Detects expiry or receives a 401 from your API |
| 2 | Your app | Sends ZOHO_WEBTAB_REQUEST_TOKEN_REFRESH to the Books parent via postMessage |
| 3 | Books client | Calls the refresh API internally |
| 4 | Books client | Returns a new token via ZOHO_WEBTAB_AUTH_TOKENS postMessage |
| 5 | Your app | Validates the new token on your server |
The refresh request your app sends:
{
"type": "ZOHO_WEBTAB_REQUEST_TOKEN_REFRESH"
}Rules:
- Only the Books client calls the refresh API. Do not call Zoho refresh endpoints from your backend.
- If refresh fails, ask the user to reload the web tab.
- Do not log full JWT tokens or your Secret Key.
URL Placeholders and Trust
You can include supported placeholders in the web tab URL using Insert Placeholders when configuring the web tab. For example:
https://yourapp.example.com/entry?customer_id=${CONTACT.CONTACT_ID}Zoho Books resolves placeholders before loading your app. Treat placeholder values as untrusted until the JWT is validated on your server. An attacker could craft a URL with arbitrary placeholder values. Only read organization_id and user_id from the verified JWT payload.
Secret Key Security
The Secret Key is shared only between Zoho Books and your backend.
| Rule | Detail |
|---|---|
| Minimum length | 32 characters |
| Maximum length | 500 characters |
| Storage | Environment variables or a secret manager on your server only |
Never store the Secret Key in:
- Frontend JavaScript
- Mobile apps
- Public repositories
- Logs
- Browser-visible responses
- Client-side configuration files
Server SDK Samples
Use these samples to validate a JWT token on your backend. Store your Secret Key in an environment variable. Never include it in frontend code.
Node.js
Dependency: jsonwebtoken
const jwt = require("jsonwebtoken");
function validateJwtToken(token, secret, expectedTabId) {
const claims = jwt.verify(token, secret, { algorithms: ["HS256"] });
if (claims.exp * 1000 < Date.now()) throw new Error("Token expired.");
if (claims.token_type !== "access") throw new Error("Invalid token type.");
if (expectedTabId && String(claims.tab_id) !== String(expectedTabId)) {
throw new Error("Invalid tab.");
}
return {
organization_id: claims.organization_id,
user_id: claims.user_id,
tab_id: claims.tab_id,
};
}Python
Dependency: PyJWT
import jwt
import time
def validate_jwt_token(token, secret, expected_tab_id=None):
claims = jwt.decode(token, secret.encode("utf-8"), algorithms=["HS256"])
if claims["exp"] < time.time():
raise ValueError("Token expired.")
if claims.get("token_type") != "access":
raise ValueError("Invalid token type.")
if expected_tab_id and str(claims.get("tab_id")) != str(expected_tab_id):
raise ValueError("Invalid tab.")
return {
"organization_id": claims["organization_id"],
"user_id": claims["user_id"],
"tab_id": claims["tab_id"],
}Java
Dependency: io.jsonwebtoken:jjwt
Claims claims = Jwts.parser()
.setSigningKey(webTabSecret.getBytes(StandardCharsets.UTF_8))
.parseClaimsJws(token)
.getBody();
if (claims.getExpiration().before(new Date())) {
throw new IllegalArgumentException("Token expired.");
}
if (!"access".equals(claims.get("token_type", String.class))) {
throw new IllegalArgumentException("Invalid token type.");
}
if (expectedTabId != null && !expectedTabId.equals(claims.get("tab_id", String.class))) {
throw new IllegalArgumentException("Invalid tab.");
}
// Use claims.get("organization_id") and claims.get("user_id")PHP
Dependency: firebase/php-jwt
$claims = (array) JWT::decode($token, new Key($webTabSecret, 'HS256'));
if (($claims['exp'] ?? 0) < time()) {
throw new InvalidArgumentException('Token expired.');
}
if (($claims['token_type'] ?? '') !== 'access') {
throw new InvalidArgumentException('Invalid token type.');
}
if ($expectedTabId !== null && $expectedTabId !== (string) $claims['tab_id']) {
throw new InvalidArgumentException('Invalid tab.');
}
// Use $claims['organization_id'] and $claims['user_id']
Go
Dependency: github.com/golang-jwt/jwt/v5
parsed, err := jwt.ParseWithClaims(token, &webTabClaims{}, func(t *jwt.Token) (interface{}, error) {
return []byte(webTabSecret), nil
})
claims := parsed.Claims.(*webTabClaims)
if claims.ExpiresAt != nil && !claims.ExpiresAt.After(time.Now()) {
return nil, ErrExpiredToken
}
if claims.TokenType != "access" {
return nil, ErrInvalidType
}
if expectedTabID != "" && expectedTabID != claims.TabID {
return nil, ErrInvalidTabID
}
// Use claims.OrganizationID and claims.UserID
Client SDK
Your app runs in an iframe inside Zoho Books or the Customer Portal. Use the Client SDK to receive JWT tokens via postMessage and request a refresh when they expire.
API
| Method | Purpose |
|---|---|
| createWebTabAuthClient({ parentOrigin, onToken, onSessionExpired }) | Create a client. parentOrigin is required and must be set to the Books or portal host. |
| init() | Start listening for ZOHO_WEBTAB_AUTH_TOKENS from the parent. |
| destroy() | Remove the listener, clear the expiry timer, and drop the in-memory token. |
| getAccessToken() | Return the current JWT string, or null. |
| requestRefresh() | Send ZOHO_WEBTAB_REQUEST_TOKEN_REFRESH to the parent so Books can issue a new token. |
| onToken({ jwt_token }) | Called when a new token arrives on initial load or after a refresh. |
| onSessionExpired() | Called when the token’s exp is reached, or the parent sends jwt_token: null. |
The SDK validates event.origin against parentOrigin, keeps a single in-memory JWT, and schedules onSessionExpired from the token’s exp claim. It does not call your backend or Zoho APIs. Your app must validate the JWT on the server.
Quick Start
<script type="module">
import { createWebTabAuthClient } from './zoho-webtab-auth-sdk.js';
const JWT_VERIFICATION_API_ENDPOINT = '/api/webtab/session'; // endpoint: your JWT Token verification endpoint
const auth = createWebTabAuthClient({
parentOrigin: 'https://books.zoho.com', // portal: your portal host origin
onToken({ jwt_token }) {
fetch(JWT_VERIFICATION_API_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jwt_token }),
});
},
onSessionExpired() {
auth.requestRefresh();
},
});
auth.init();
</script>When your API returns 401, call auth.requestRefresh(). Do not call Zoho refresh endpoints from your server.
SDK Source
const MESSAGE_AUTH_TOKENS = 'ZOHO_WEBTAB_AUTH_TOKENS';
const MESSAGE_REQUEST_REFRESH = 'ZOHO_WEBTAB_REQUEST_TOKEN_REFRESH';
function getTokenExpiryMs(token) {
try {
const segment = token.split('.')[1];
if (!segment) return null;
const payload = JSON.parse(atob(segment.replace(/-/g, '+').replace(/_/g, '/')));
if (!payload.exp) return null;
return payload.exp * 1000;
} catch {
return null;
}
}
export function createWebTabAuthClient(config) {
const { parentOrigin, onToken, onSessionExpired } = config || {};
if (!parentOrigin) throw new Error('parentOrigin is required');
let jwt_token = null;
let initialized = false;
let expiryTimerId = null;
function clearExpiryTimer() {
if (expiryTimerId) { clearTimeout(expiryTimerId); expiryTimerId = null; }
}
function scheduleExpiry(token) {
clearExpiryTimer();
const expiresAt = getTokenExpiryMs(token);
if (!expiresAt) return;
const delay = expiresAt - Date.now();
if (delay <= 0) { onSessionExpired?.(); return; }
expiryTimerId = setTimeout(() => { expiryTimerId = null; onSessionExpired?.(); }, delay);
}
function onMessage(event) {
if (event.origin !== parentOrigin) return;
const data = event.data;
if (!data || data.type !== MESSAGE_AUTH_TOKENS) return;
if (data.jwt_token === null || data.jwt_token === undefined) {
clearExpiryTimer(); jwt_token = null; onSessionExpired?.(); return;
}
jwt_token = data.jwt_token;
scheduleExpiry(jwt_token);
onToken?.({ jwt_token });
}
function init() {
if (initialized) return;
initialized = true;
window.addEventListener('message', onMessage);
}
function destroy() {
if (!initialized) return;
initialized = false;
window.removeEventListener('message', onMessage);
clearExpiryTimer();
jwt_token = null;
}
function getAccessToken() { return jwt_token; }
function requestRefresh() {
window.parent.postMessage({ type: MESSAGE_REQUEST_REFRESH }, parentOrigin);
}
return { init, destroy, getAccessToken, requestRefresh };
}
export default createWebTabAuthClient;
if (typeof window !== 'undefined') {
window.ZohoWebTabAuth = { createWebTabAuthClient };
}