/api/jwt/decode
MCP tool jwt_decode Decodes a compact JWS (RFC 7515/7519): header, payload, and the registered claims, with exp, nbf and iat as ISO times and seconds relative to at. Flags expired, not-yet-valid, alg: none, weak or unusual algorithms, and a missing exp. A compact JWE (5 segments) is recognised and its header shown, but it cannot be decrypted. With key, verifies the signature using a PUBLIC key only: a JWK, a JWKS (selected by the token kid), a PUBLIC KEY PEM, or a CERTIFICATE PEM. HS256/HS384/HS512 cannot be verified here because they need a shared secret, which this service never accepts. The server never fetches keys: pass the JWKS JSON itself, not its URL. Decoding is not verification: never trust claims from a token whose verification.outcome is not valid.
Browser version (runs locally): JWT Inspector.
Try it
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
token required | string (≤ 65536 chars) | — | The compact JWT (header.payload.signature, or 5 segments for JWE). A leading Bearer , surrounding quotes and embedded whitespace are removed, with a warning. |
key | string (≤ 65536 chars) | — | Optional PUBLIC key to verify the signature: a JWK JSON object, a JWKS JSON ({"keys":[...]}), a PUBLIC KEY PEM, or a CERTIFICATE PEM. Private keys and symmetric (kty: oct) keys are refused. URLs are refused: fetch the JWKS yourself and pass its JSON. |
at | string | — | Reference time for exp/nbf/iat, ISO 8601, e.g. 2026-01-01T00:30:00Z. Default: now. |
clock_tolerance | integer (0–86400) | 0 | Leeway in seconds for clock skew when evaluating exp, nbf and iat. |
Also accepted on every endpoint: format (json | text).
Parameters may come from the query string, a JSON object body, an urlencoded or multipart form,
or (for input) a text/plain body.
Global parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
format | json | text | json | json returns the documented object. text returns only the primary value as text/plain (see "Text output"). |
Response 200 application/json
| Field | Type | Description |
|---|---|---|
format | "JWS" | "JWE" | JWS (signed, 3 segments) or JWE (encrypted, 5 segments). |
header | object | The decoded (protected) JOSE header, exactly as sent. |
payload | object | null | The decoded claims set. Null for a JWE or when the payload is not a JSON object. |
payloadText | string | null | The payload as text when it is UTF-8 but not a JSON object (a JWS that is not a JWT). |
algorithm | object | |
algorithm.alg | string | null | The header alg. |
algorithm.family | "HMAC" | "RSASSA-PKCS1-v1_5" | "RSASSA-PSS" | "ECDSA" | "EdDSA" | "ML-DSA" | "none" | "unknown" | Algorithm family. unknown for JWE or unregistered values. |
algorithm.registered | boolean | True when alg is a registered JWS algorithm known to this tool. |
algorithm.symmetric | boolean | True for HS256/HS384/HS512 (shared-secret HMAC). |
algorithm.enc | string | null | JWE content encryption (enc), for encrypted tokens. |
segments | object | |
segments.count | integer | Number of dot-separated segments: 3 or 5. |
segments.headerBytes | integer | Decoded header length in bytes. |
segments.payloadBytes | integer | null | Decoded payload length in bytes (JWS only). |
segments.signatureBytes | integer | null | Decoded signature length in bytes (JWS only). |
claims | object | RFC 7519 §4.1 registered claims. Null when absent or of the wrong type (a warning explains). |
claims.iss | string | null | Issuer. |
claims.sub | string | null | Subject. |
claims.aud | array of string | null | Audience, always as an array (a single string becomes one item). |
claims.jti | string | null | JWT ID. |
claims.exp | object | null | A registered time claim, or null when absent or not a number. |
claims.exp.value | number | The NumericDate as given: seconds since 1970-01-01T00:00:00Z. |
claims.exp.iso | string | null | The same instant as ISO 8601 UTC (null if out of range). |
claims.exp.secondsFromAt | number | value minus the reference time at, in seconds. Negative: in the past. Positive: in the future. |
claims.nbf | object | null | A registered time claim, or null when absent or not a number. |
claims.nbf.value | number | The NumericDate as given: seconds since 1970-01-01T00:00:00Z. |
claims.nbf.iso | string | null | The same instant as ISO 8601 UTC (null if out of range). |
claims.nbf.secondsFromAt | number | value minus the reference time at, in seconds. Negative: in the past. Positive: in the future. |
claims.iat | object | null | A registered time claim, or null when absent or not a number. |
claims.iat.value | number | The NumericDate as given: seconds since 1970-01-01T00:00:00Z. |
claims.iat.iso | string | null | The same instant as ISO 8601 UTC (null if out of range). |
claims.iat.secondsFromAt | number | value minus the reference time at, in seconds. Negative: in the past. Positive: in the future. |
at | string | Reference time used for the time checks, ISO 8601 UTC. |
clockTolerance | integer | Seconds of leeway applied to exp, nbf and iat. |
status | object | Machine-readable flags. Each true flag also has a sentence in warnings. |
status.expired | boolean | exp is at or before at (minus tolerance), per RFC 7519 §4.1.4. |
status.notYetValid | boolean | nbf is after at (plus tolerance). |
status.issuedInFuture | boolean | iat is after at (plus tolerance): clock skew or forgery. |
status.missingExp | boolean | The token is a JWT but has no exp. |
status.algNone | boolean | alg is none: unsecured, anyone can forge it. |
status.weakAlgorithm | boolean | alg is none or names SHA-1/MD5 (e.g. RS1, HS1). |
status.unusualAlgorithm | boolean | alg is unregistered (including wrong case), or rarely deployed (ES256K, Ed448, ML-DSA). |
status.symmetric | boolean | HMAC: verification needs a shared secret, never accepted here. |
status.encrypted | boolean | The token is a JWE. |
status.signatureVerified | boolean | null | Result of signature verification; null when no key was given. |
verification | object | |
verification.outcome | "not-requested" | "valid" | "invalid-signature" | "unverifiable" | not-requested (no key), valid, invalid-signature (the key does not match), or unverifiable (see error). |
verification.valid | boolean | True only when the signature verified. |
verification.keySource | one of (by kind) | How key was read; null when no key was given. |
verification.keySource (kind = "undefined") | variant | |
verification.keySource (kind = "undefined") | variant | |
verification.kid | string | null | kid of the JWK that was used (JWK/JWKS only). |
verification.keyType | string | null | The key used, e.g. EC P-256, RSA 2048-bit, Ed25519. |
verification.candidates | integer | Number of keys that were compatible and tried. |
verification.error | string | null | Why verification failed or could not run, and how to fix it. |
warnings | array of string | Problems and observations, most important first. |
Text output (format=text, text/plain): The decoded payload as pretty-printed JSON (null for a JWE).
Errors
Errors are JSON: {"error": "<message>", "code": "<code>", "param": "<name>"?}.
| Status | Code | When |
|---|---|---|
400 | invalid_parameter | at is not a date; or key is malformed, a URL, an unsupported format, a private key, or a symmetric secret (all refused). |
422 | unprocessable_input | The token does not have 3 or 5 segments, a segment is not base64url, or the header is not a JSON object. The message names the segment and the fix. |
400 | missing_parameter | A required parameter is absent. |
400 | unknown_parameter | A parameter name is not in the table above (typos are rejected, not ignored). |
413 | payload_too_large | The request body exceeds 1 MiB. |
405 | method_not_allowed | A method other than GET, POST or OPTIONS. |
Behaviour
- Idempotent: the same arguments always return the same result.
-
Requires a client ID and secret (authentication). Each call is
recorded as one metadata row against your client ID; inputs and outputs are never stored or
logged. Responses carry
cache-control: no-store. - CORS: any origin may call this endpoint from a browser, using your credentials.
Examples
Responses below were produced by running each request through this code at build time. Long strings are shortened.
Decode and verify an ES256 token with a JWKS
curl -X POST 'https://util.caruana.me/api/jwt/decode' \
-H 'content-type: application/json' \
-d '{"token":"eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImV4YW1wbGUtMjAyNiJ9.eyJzY29wZSI6InJlYWQ6cmVwb3J0cyIsImVtYWlsIjoiYWRhQGV4YW1wbGUuY29tIiwiaXNzIjoiaHR0cHM6Ly9hdXRoLmV4YW1wbGUuY29tLyIsInN1YiI6InVzZXItNDIiLCJhdWQiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSIsImlhdCI6MTc2NzIyNTYwMCwibmJmIjoxNzY3MjI1NjAwLCJleHAiOjE3NjcyMjkyMDAsImp0aSI6ImI3ZTRjMmEwLTVmMWQtNGU4YS05YzNiLTJkNmY4YTFlMGM0NyJ9.I65iVa8TuYZWzTQrLDMQtUTiAvBx5ucG_bloyYh-4cmJGpXyB-gHHaEzUsvpf1P1FGIYEkropakR5Gi_cDoIGQ","key":"{\"keys\":[{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"KvQwz-fAGPce4KhfOnfsQLYyMSQIyvO-gjZoneQ_QC4\",\"y\":\"hqZ-ZkRarQ1A-f0W87Kb-77GubouttkHeB4LhfUDKW0\",\"kid\":\"example-2026\",\"alg\":\"ES256\",\"use\":\"sig\"}]}","at":"2026-01-01T00:30:00Z"}'
Response 200
{
"format": "JWS",
"header": {
"alg": "ES256",
"typ": "JWT",
"kid": "example-2026"
},
"payload": {
"scope": "read:reports",
"email": "[email protected]",
"iss": "https://auth.example.com/",
"sub": "user-42",
"aud": "https://api.example.com",
"iat": 1767225600,
"nbf": 1767225600,
"exp": 1767229200,
"jti": "b7e4c2a0-5f1d-4e8a-9c3b-2d6f8a1e0c47"
},
"payloadText": null,
"algorithm": {
"alg": "ES256",
"family": "ECDSA",
"registered": true,
"symmetric": false,
"enc": null
},
"segments": {
"count": 3,
"headerBytes": 48,
"payloadBytes": 228,
"signatureBytes": 64
},
"claims": {
"iss": "https://auth.example.com/",
"sub": "user-42",
"aud": [
"https://api.example.com"
],
"jti": "b7e4c2a0-5f1d-4e8a-9c3b-2d6f8a1e0c47",
"exp": {
"value": 1767229200,
"iso": "2026-01-01T01:00:00.000Z",
"secondsFromAt": 1800
},
"nbf": {
"value": 1767225600,
"iso": "2026-01-01T00:00:00.000Z",
"secondsFromAt": -1800
},
"iat": {
"value": 1767225600,
"iso": "2026-01-01T00:00:00.000Z",
"secondsFromAt": -1800
}
},
"at": "2026-01-01T00:30:00.000Z",
"clockTolerance": 0,
"status": {
"expired": false,
"notYetValid": false,
"issuedInFuture": false,
"missingExp": false,
"algNone": false,
"weakAlgorithm": false,
"unusualAlgorithm": false,
"symmetric": false,
"encrypted": false,
"signatureVerified": true
},
"verification": {
"outcome": "valid",
"valid": true,
"keySource": "jwks",
"kid": "example-2026",
"keyType": "EC P-256",
"candidates": 1,
"error": null
},
"warnings": []
}
Same call as an MCP tools/call
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "jwt_decode",
"arguments": {
"token": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImV4YW1wbGUtMjAyNiJ9.eyJzY29wZSI6InJlYWQ6cmVwb3J0cyIsImVtYWlsIjoiYWRhQGV4YW1wbGUuY29tIiwiaXNzIjoiaHR0cHM6Ly9hdXRoLmV4YW1wbGUuY29tLyIsInN1YiI6InVzZXItNDIiLCJhdWQiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSIsImlhdCI6MTc2NzIyNTYwMCwibmJmIjoxNzY3MjI1NjAwLCJleHAiOjE3NjcyMjkyMDAsImp0aSI6ImI3ZTRjMmEwLTVmMWQtNGU4YS05YzNiLTJkNmY4YTFlMGM0NyJ9.I65iVa8TuYZWzTQrLDMQtUTiAvBx5ucG_bloyYh-4cmJGpXyB-gHHaEzUsvpf1P1FGIYEkropakR5Gi_cDoIGQ",
"key": "{\"keys\":[{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"KvQwz-fAGPce4KhfOnfsQLYyMSQIyvO-gjZoneQ_QC4\",\"y\":\"hqZ-ZkRarQ1A-f0W87Kb-77GubouttkHeB4LhfUDKW0\",\"kid\":\"example-2026\",\"alg\":\"ES256\",\"use\":\"sig\"}]}",
"at": "2026-01-01T00:30:00Z"
}
}
}Decode only (HS256: cannot be verified here)
curl 'https://util.caruana.me/api/jwt/decode?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c&at=2026-01-01T00%3A00%3A00Z'
Response 200
{
"format": "JWS",
"header": {
"alg": "HS256",
"typ": "JWT"
},
"payload": {
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
},
"payloadText": null,
"algorithm": {
"alg": "HS256",
"family": "HMAC",
"registered": true,
"symmetric": true,
"enc": null
},
"segments": {
"count": 3,
"headerBytes": 27,
"payloadBytes": 55,
"signatureBytes": 32
},
"claims": {
"iss": null,
"sub": "1234567890",
"aud": null,
"jti": null,
"exp": null,
"nbf": null,
"iat": {
"value": 1516239022,
"iso": "2018-01-18T01:30:22.000Z",
"secondsFromAt": -250986578
}
},
"at": "2026-01-01T00:00:00.000Z",
"clockTolerance": 0,
"status": {
"expired": false,
"notYetValid": false,
"issuedInFuture": false,
"missingExp": true,
"algNone": false,
"weakAlgorithm": false,
"unusualAlgorithm": false,
"symmetric": true,
"encrypted": false,
"signatureVerified": null
},
"verification": {
"outcome": "not-requested",
"valid": false,
"keySource": null,
"kid": null,
"keyType": null,
"candidates": 0,
"error": null
},
"warnings": [
"No \"exp\" claim: the token never expires unless the verifier enforces a lifetime.",
"HS256 is HMAC: verification needs the shared secret, and anyone who can verify can also forge tokens. This service never… (46 more characters)"
]
}
Same call as an MCP tools/call
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "jwt_decode",
"arguments": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c",
"at": "2026-01-01T00:00:00Z"
}
}
}