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:
| Result | What it means |
|---|---|
| Token is valid | The 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 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 ERP 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 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 tab | The 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:
| Claim | Description |
|---|---|
| organization_id | The Zoho ERP organization ID. Map this to your tenant to load the right data. |
| user_id | The Zoho ERP 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 | 10800 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.
| Step | Who | Action |
|---|---|---|
| 1 | Your server | Detects expiry or receives a 401 from your API |
| 2 | Your server | Sends ZOHO_WEBTAB_REQUEST_TOKEN_REFRESH to the Zoho ERP app via postMessage |
| 3 | Zoho ERP app | Calls the refresh API internally |
| 4 | Zoho ERP app | Returns a new token via ZOHO_WEBTAB_AUTH_TOKENS postMessage |
| 5 | Your server | Validates 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.
| 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 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
| Method | Purpose |
|---|---|
| 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 };
}