## Documentation Index Access the complete documentation index at: https://www.zoho.com/qa/books/help/llms.txt Use this file to discover all available documentation pages before proceeding. # 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**. ![Create Web Tab](/books/help/images/web-tabs/create-web-tab.png) 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**. ![Edit Web Tab](/books/help/images/web-tabs/edit-web-tab.png) * 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. ![Validate JWT Token](/books/help/images/web-tabs/validate-jwt-token.png) **Note:** If you’re building the server integration for your web tab, see [JWT Authentication](/qa/books/help/settings/customization/web-tabs.html#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 Web Tab as Inactive](/books/help/images/web-tabs/mark-web-tab-inactive.png) * * * ## 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**. ![Mark Web Tab as Active](/books/help/images/web-tabs/mark-web-tab-active.png) * * * ## 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**. ![Delete Web Tab](/books/help/images/web-tabs/delete-web-tab.png) * 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: ```json { "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: ```json { "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: ```text 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 ```js 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 ```python 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 ```java 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 ```php $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 ```go 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** ```html ``` When your API returns 401, call auth.requestRefresh(). Do not call Zoho refresh endpoints from your server. **SDK Source** ```js 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 }; } ```