/

Web Tabs

Web Tabs are similar to browser tabs, but accessible within Zoho ERP. You can open any web page or application that provides an embed URL directly inside Zoho ERP, 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 ERP 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 ships orders to customers across the country and needs to check carrier status daily. Brandon, the IT admin, adds Zylker’s shipping carrier portal as a web tab so the warehouse team can track shipments without leaving Zoho ERP. Without JWT authentication, the portal has no way to confirm which organization opened it. Brandon enables JWT authentication when setting up the web tab. Zoho ERP now signs each request with a token that includes Zylker’s organization and user details, so the portal loads only Zylker’s shipment data.

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 ERP 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.
  • To verify that requests to your external app came from Zoho ERP for the right organization and user, enable JWT Authentication.
    • Enter a Secret Key between 32 and 500 characters. Zoho ERP uses this key to sign each token it sends to your server. 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.
  • 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 ERP.

Note: When JWT authentication is enabled, Zoho ERP attaches a signed token to each request that opens the web tab. Your server verifies this token to confirm the request came from Zoho ERP 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 server received from Zoho ERP. Use it to confirm the token details or diagnose issues before making changes to your server.

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 ERP verifies the token against the Secret Key saved on this web tab and shows one of these results:
ResultWhat it means
Token is validThe signature is valid and the token has not expired. Zoho ERP 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 invalidThe 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 ERP shows the signature as Invalid and the payload as Unverified, since it cannot be trusted.
Token has expiredThe token’s expiry time has passed. Zoho ERP 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 tabThe tab_id in the payload of this JWT Token doesn’t match your web tab. Zoho ERP 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 ERP 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 ERP Developer Portal. You can enable JWT authentication on any of these web tabs so your external app can verify that requests came from Zoho ERP 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. The Zoho ERP app delivers it to your server via postMessage from the parent window after the web tab loads in the iframe.

When a user opens the web tab, Zoho ERP:

  • Loads your server’s URL in the iframe.
  • Posts the JWT token to your server using the ZOHO_WEBTAB_AUTH_TOKENS message type.

Your server receives the message and validates the token before rendering any data.

The message your server receives:

{
  "type": "ZOHO_WEBTAB_AUTH_TOKENS",
  "jwt_token": "eyJhbGciOiJIUzI1NiJ9..."
}

When a session ends, Zoho ERP 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:

ClaimDescription
organization_idThe Zoho ERP organization ID. Map this to your tenant to load the right data.
user_idThe Zoho ERP user ID for web tabs accessed by organization users, or the contact/customer ID for Customer Portal web tabs.
token_typeAlways access. Reject any token where this value differs.
tab_idThe ID of the web tab this token was issued for.
iatIssued-at time as a Unix timestamp in seconds.
expExpiry time as a Unix timestamp in seconds. Reject the token after this time.

Token settings:

SettingValue
AlgorithmHS256 (HMAC-SHA256)
Signing keyThe Secret Key configured on the web tab, as UTF-8 bytes
FormatStandard JWT (header.payload.signature)

Token validity options:

Token Validity settingLifetime
10 minutes600 seconds
30 minutes1800 seconds
1 hour3600 seconds
3 hours10800 seconds

What JWT Validation Protects

Validating the JWT token confirms that:

  • The web tab request was generated by Zoho ERP.
  • 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. Your server does not call Zoho APIs directly to get a new token. Instead, the Zoho ERP app sends a postMessage to the parent window, and the Zoho ERP app returns a new token.

StepWhoAction
1Your serverDetects expiry or receives a 401 from your API
2Your serverSends ZOHO_WEBTAB_REQUEST_TOKEN_REFRESH to the Zoho ERP app via postMessage
3Zoho ERP appCalls the refresh API internally
4Zoho ERP appReturns a new token via ZOHO_WEBTAB_AUTH_TOKENS postMessage
5Your serverValidates the new token

The refresh request your server sends:

{
  "type": "ZOHO_WEBTAB_REQUEST_TOKEN_REFRESH"
}

Rules:

  • Only the Zoho ERP app calls the refresh API. Do not call Zoho refresh endpoints from your server.
  • 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 ERP resolves placeholders before loading your server. 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 ERP and your server.

RuleDetail
Minimum length32 characters
Maximum length500 characters
StorageEnvironment 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 server. 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

The web tab runs in an iframe inside Zoho ERP or the Customer Portal. Use the Client SDK to receive JWT tokens via postMessage and request a refresh when they expire.

API

MethodPurpose
createWebTabAuthClient({ parentOrigin, onToken, onSessionExpired })Create a client. parentOrigin is required and must be set to the Zoho ERP app 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 the Zoho ERP app 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 server or Zoho APIs. Your server must validate the JWT.

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://erp.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 server 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 };
}