API reference

Decode and verify a JWT

Decode a JWT, check its claims and algorithm, and optionally verify it with a public key.

GET POST /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

Credentials
Remember these credentials

Not saved: you will paste them again next time.

No credentials yet? Get a client ID and secret. They are sent only to this site, in an Authorization: Basic header.

POST /api/jwt/decode with a JSON body

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"
    }
  }
}