How to decode a JWT token and inspect its claims
Paste a JWT and read its header and payload. Walks through the Base64URL structure, what exp and iat mean, and why decoding alone does not verify the signature.
Decoding is not verifying — keep the two apart
The first thing to be clear about: decoding a JWT and verifying its signature are two different operations. Decoding takes the first two segments of the header.payload.signature dot-separated string, runs them through Base64URL, and shows you the JSON inside. It tells you nothing about whether the token came from a legitimate issuer. Verifying takes the signature segment and runs it against the issuer’s public key (or shared secret) to confirm the token has not been tampered with. If you just want to see what is in a token, jwt-decode is the right tool; if you are about to grant access based on the token’s contents, you also need jwt-verify.
The day-to-day need for decoding shows up everywhere. You want to see what claims your auth provider is actually emitting. You are debugging a frontend and need to know whether exp (expiry) and iat (issued-at) line up with what you expect. A log captured a stray token and you want to read out the user ID or scope. A test environment issued a JWT and you want to confirm the right custom claims rode along. None of that requires a signing key — it only requires reading the payload.
Pulling a token apart in the browser
Open jwt-decode and paste the JWT into the input. You will see the familiar dot-separated form: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIi.... The tool decodes it on the fly and shows three panels — Header, Payload, and Signature.
The Header is a small JSON object identifying the signing algorithm (alg: "HS256" / "RS256" / "ES256" and friends) and the token type (typ: "JWT"). The Payload carries the standard registered claims — iss (issuer), sub (subject), aud (audience), exp (expiration), iat (issued-at), nbf (not-before), jti (token ID) — plus whatever custom claims (role, email, scope) the issuing system added. jwt-decode automatically renders Unix-second timestamps like exp, iat, and nbf next to a human-readable form (“2026-06-13 14:30, expires in 3 hours”) so you do not have to translate them mentally. The Signature is shown as the raw Base64URL string — it is not decoded, because verifying it requires a key and that is the job of jwt-verify.
Watch the DevTools Network tab while pasting and you will see no requests fire. The decode happens entirely in the page’s JavaScript heap.
Base64URL and JSON parsing under the hood
A JWT is three Base64URL strings joined by periods. Base64URL differs from regular Base64 in two ways: + is replaced with -, / with _, and the trailing = padding is stripped. The browser’s atob() does not accept this URL-safe form directly, so jwt-decode rewrites - back to +, _ back to /, and pads the string back to a multiple of four with = before calling atob. The resulting binary string is wrapped in a Uint8Array and pushed through TextDecoder("utf-8") to recover a JSON string that is correct for non-ASCII payloads (skip this step and any non-ASCII claim turns into garbage).
The decoded JSON is then parsed with JSON.parse and rendered as a structured tree with per-claim styling. The error handling distinguishes failure modes: an input that does not split into three segments, or one whose segments fail Base64URL, is reported as “not a JWT-shaped string”, while a successful Base64URL decode that then fails JSON.parse is reported separately as “looked like a JWT but the body is not JSON”. The split matters because it helps you tell a broken token apart from a JWT-shaped-but-different format like Branca or PASETO. If you need to go in the other direction and produce a token, jwt-encode builds the header.payload.signature form for you using SubtleCrypto.sign.
Why pasting tokens into a remote decoder is a category mistake
Search for “JWT decoder” or “JWT debugger” and a non-trivial number of results are sites that ship the token to a backend before showing you the contents. There is no technical reason to send the token away — the decode is pure local computation — but plenty of services do it anyway, and that means the token you paste ends up in someone else’s logs.
A JWT is not ordinary text. It is the credential. Inside its expiration window, anyone holding an access token can call the API as the user that owns it. Most auth systems can revoke leaked tokens, but only after someone notices the leak, and a token pasted into a debugger during a five-minute investigation is not the kind of thing teams routinely audit afterwards. That is exactly why the debugging case is the one where staying in-browser pays off the most.
jwt-decode keeps both the input and the decoded result inside the tab. The Base64URL decode runs through atob + TextDecoder, the JSON parse runs through JSON.parse, and neither involves any network I/O. The source is open on GitHub, and the DevTools Network tab is the on-the-spot proof that nothing went out. Being able to confirm with your own eyes that the token did not leave the page is a reasonable default for any workflow that touches credentials.