Hi everyone! 
For this challenge, I created a JWT Inspector Toolkit, plug-and-play script that helps developers decode, validate and inspect JSON Web Tokens directly within Postman. It’s not just for me but it’s meant to be used by anyone working with JWTs. It performs:
Signature checks
Schema validation (built-in or custom)
Expiration insights
Optional issuer (iss
) verification
Also comes with a documentation.
External Packages Used
jsonwebtoken
– for decoding and signature verification
joi
– for schema-based payload validation
moment
– for human-readable expiration insight All logic is encapsulated in a single request
Everything runs inside one request using test scripts and environment variables, no setup required.
Update: Added JWT Visualizer 
After seeing @danny-dainton clean UI using Postman Visualizer, I decided to bring that to my JWT Inspector and of course, sprinkle in some CSS magic of my own to make it a playful and colorful visualization 
I structured the data like this:
const visualizationData = {
header: decoded?.header || {},
payload: payload || {},
validation: validation || { valid: false },
schemaType: schemaType,
timestamp: new Date().toISOString(),
expInfo: payload?.exp ? TestUtils.getExpirationInfo(payload.exp) : null
};
pm.visualizer.set(htmlTemplate, visualizationData);
Preview:

Full HTML and CSS Code used for the visualizer
const htmlTemplate = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JWT Inspector Dashboard</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
animation: backgroundShift 10s ease-in-out infinite alternate;
}
@keyframes backgroundShift {
0% { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
50% { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); }
100% { background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); }
}
.container {
max-width: 1200px;
margin: 0 auto;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(20px);
border-radius: 20px;
padding: 30px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.header {
text-align: center;
margin-bottom: 40px;
animation: slideDown 0.8s ease-out;
}
@keyframes slideDown {
from { transform: translateY(-50px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.title {
font-size: 3rem;
background: linear-gradient(45deg, #ff6b6b, #4ecdc4, #45b7d1, #96ceb4);
background-size: 400% 400%;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
animation: gradientShift 3s ease-in-out infinite;
margin-bottom: 10px;
font-weight: bold;
}
@keyframes gradientShift {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
.subtitle {
color: rgba(255, 255, 255, 0.8);
font-size: 1.2rem;
margin-bottom: 20px;
}
.status-badge {
display: inline-block;
padding: 10px 20px;
border-radius: 25px;
font-weight: bold;
text-transform: uppercase;
letter-spacing: 1px;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
.valid {
background: linear-gradient(45deg, #56ab2f, #a8e6cf);
color: white;
box-shadow: 0 4px 15px rgba(86, 171, 47, 0.4);
}
.invalid {
background: linear-gradient(45deg, #ff416c, #ff4b2b);
color: white;
box-shadow: 0 4px 15px rgba(255, 65, 108, 0.4);
}
.dashboard {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 25px;
margin-top: 30px;
}
.card {
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(10px);
border-radius: 15px;
padding: 25px;
border: 1px solid rgba(255, 255, 255, 0.2);
transition: all 0.3s ease;
animation: fadeInUp 0.6s ease-out;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.2);
}
@keyframes fadeInUp {
from { transform: translateY(30px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.card-header {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.card-icon {
font-size: 2rem;
margin-right: 15px;
animation: rotate 4s linear infinite;
}
@keyframes rotate {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.card-title {
font-size: 1.3rem;
font-weight: bold;
color: white;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.card-content {
color: rgba(255, 255, 255, 0.9);
line-height: 1.6;
}
.property {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
padding: 8px 12px;
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
transition: background 0.3s ease;
}
.property:hover {
background: rgba(255, 255, 255, 0.2);
}
.property-key {
font-weight: bold;
color: #ffd700;
}
.property-value {
color: #e0e0e0;
text-align: right;
word-break: break-all;
}
.expiration-info {
text-align: center;
padding: 20px;
border-radius: 10px;
margin-top: 15px;
}
.expires-soon {
background: linear-gradient(45deg, #ff9a9e, #fecfef);
color: #d63384;
}
.expires-good {
background: linear-gradient(45deg, #a8edea, #fed6e3);
color: #198754;
}
.expired {
background: linear-gradient(45deg, #ff6b6b, #ee5a24);
color: white;
}
.footer {
text-align: center;
margin-top: 40px;
padding: 20px;
border-top: 1px solid rgba(255, 255, 255, 0.2);
color: rgba(255, 255, 255, 0.7);
}
.timestamp {
font-size: 0.9rem;
font-style: italic;
}
.floating-elements {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: -1;
}
.floating-element {
position: absolute;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
animation: float 6s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0px) rotate(0deg); }
50% { transform: translateY(-20px) rotate(180deg); }
}
.json-viewer {
background: rgba(0, 0, 0, 0.3);
border-radius: 8px;
padding: 15px;
margin-top: 15px;
font-family: 'Courier New', monospace;
font-size: 0.9rem;
overflow-x: auto;
border-left: 4px solid #4ecdc4;
}
.json-key {
color: #ffd700;
}
.json-string {
color: #98fb98;
}
.json-number {
color: #87ceeb;
}
.json-boolean {
color: #dda0dd;
}
</style>
</head>
<body>
<div class="floating-elements">
<div class="floating-element" style="top: 10%; left: 10%; width: 60px; height: 60px; animation-delay: 0s;"></div>
<div class="floating-element" style="top: 70%; left: 80%; width: 80px; height: 80px; animation-delay: 2s;"></div>
<div class="floating-element" style="top: 30%; left: 70%; width: 40px; height: 40px; animation-delay: 4s;"></div>
<div class="floating-element" style="top: 80%; left: 20%; width: 100px; height: 100px; animation-delay: 1s;"></div>
</div>
<div class="container">
<div class="header">
<h1 class="title">🔐 JWT Inspector</h1>
<p class="subtitle">Advanced Token Analysis Dashboard</p>
<div class="status-badge ${visualizationData.validation.valid ? 'valid' : 'invalid'}">
${visualizationData.validation.valid ? '✅ Valid Token' : '❌ Invalid Token'}
</div>
</div>
<div class="dashboard">
<!-- Token Header Card -->
<div class="card">
<div class="card-header">
<div class="card-icon">🎯</div>
<div class="card-title">Token Header</div>
</div>
<div class="card-content">
${Object.entries(visualizationData.header).map(([key, value]) => `
<div class="property">
<span class="property-key">${key}:</span>
<span class="property-value">${value}</span>
</div>
`).join('')}
</div>
</div>
<!-- Token Payload Card -->
<div class="card">
<div class="card-header">
<div class="card-icon">📦</div>
<div class="card-title">Token Payload</div>
</div>
<div class="card-content">
${Object.entries(visualizationData.payload).map(([key, value]) => `
<div class="property">
<span class="property-key">${key}:</span>
<span class="property-value">${typeof value === 'object' ? JSON.stringify(value) : value}</span>
</div>
`).join('')}
</div>
</div>
<!-- Validation Status Card -->
<div class="card">
<div class="card-header">
<div class="card-icon">${visualizationData.validation.valid ? '✅' : '❌'}</div>
<div class="card-title">Validation Status</div>
</div>
<div class="card-content">
<div class="property">
<span class="property-key">Schema Type:</span>
<span class="property-value">${visualizationData.schemaType}</span>
</div>
<div class="property">
<span class="property-key">Status:</span>
<span class="property-value">${visualizationData.validation.valid ? 'PASSED' : 'FAILED'}</span>
</div>
${!visualizationData.validation.valid && visualizationData.validation.error ? `
<div class="json-viewer">
<strong>Validation Errors:</strong><br>
${visualizationData.validation.error.map(err => `• ${err.message}`).join('<br>')}
</div>
` : ''}
</div>
</div>
<!-- Expiration Info Card -->
${visualizationData.expInfo ? `
<div class="card">
<div class="card-header">
<div class="card-icon">⏰</div>
<div class="card-title">Expiration Info</div>
</div>
<div class="card-content">
<div class="property">
<span class="property-key">Expires At:</span>
<span class="property-value">${visualizationData.expInfo.expTime}</span>
</div>
<div class="property">
<span class="property-key">Time Left:</span>
<span class="property-value">${visualizationData.expInfo.timeLeft}</span>
</div>
<div class="expiration-info ${visualizationData.expInfo.expired ? 'expired' : visualizationData.expInfo.minutesLeft <= 60 ? 'expires-soon' : 'expires-good'}">
${visualizationData.expInfo.expired ?
'🚨 TOKEN EXPIRED!' :
visualizationData.expInfo.minutesLeft <= 60 ?
'⚠️ Expires Soon!' :
'✅ Token Valid'
}
</div>
</div>
</div>
` : ''}
<!-- Raw JSON Card -->
<div class="card" style="grid-column: 1 / -1;">
<div class="card-header">
<div class="card-icon">📄</div>
<div class="card-title">Raw JSON Data</div>
</div>
<div class="card-content">
<div class="json-viewer">
<pre>${JSON.stringify(visualizationData, null, 2)}</pre>
</div>
</div>
</div>
</div>
<div class="footer">
<div class="timestamp">
🕐 Generated: ${visualizationData.timestamp}
</div>
<div style="margin-top: 10px;">
<strong>JWT Inspector Toolkit</strong> - Postman Test Script Visualizer
</div>
</div>
</div>
<script>
// Add some interactive effects
document.addEventListener('DOMContentLoaded', function() {
// Animate cards on scroll
const cards = document.querySelectorAll('.card');
cards.forEach((card, index) => {
card.style.animationDelay = (index * 0.1) + 's';
});
// Add click effects to properties
const properties = document.querySelectorAll('.property');
properties.forEach(property => {
property.addEventListener('click', function() {
this.style.transform = 'scale(1.02)';
setTimeout(() => {
this.style.transform = 'scale(1)';
}, 200);
});
});
// Status badge animation
const statusBadge = document.querySelector('.status-badge');
if (statusBadge) {
setInterval(() => {
statusBadge.style.boxShadow = statusBadge.classList.contains('valid') ?
'0 4px 20px rgba(86, 171, 47, 0.6)' :
'0 4px 20px rgba(255, 65, 108, 0.6)';
setTimeout(() => {
statusBadge.style.boxShadow = statusBadge.classList.contains('valid') ?
'0 4px 15px rgba(86, 171, 47, 0.4)' :
'0 4px 15px rgba(255, 65, 108, 0.4)';
}, 1000);
}, 2000);
}
});
</script>
</body>
</html>
`;
pm.visualizer.set(htmlTemplate, visualizationData);
What I Learned
- I challenged myself to build something reusable and helpful not just for this challenge, but for other developers who regularly work with JWTs.
- I learned how far you can push Postman’s testing environment using external packages like
jsonwebtoken
, joi
, and moment
, even with sandbox limitations.
- Creating dynamic schema validation and clean expiration tracking helped me better understand how to design test scripts for real-world team workflows.
- I learned how to create beautiful visualization in my test script, its amazing what Postman can do
- Most importantly, I learned how small tools like this, when made modular and flexible can save time and add clarity across projects and teams.
Screenshots