Every developer does it. A request fails, you copy the token from the Authorization header, search for “jwt decoder” and paste it into the first result. Ten minutes later it is a minified API response, then a config file, then a certificate.
It is quick and it usually works. It is also worth a second look, because some of what we paste is effectively a password, and some of the data belongs to other people.
Why it is a risk
Some tools send your input to a server
An online tool can do its work in your browser, or send what you paste to a server. From the page you often can’t tell which. A server-side tool isn’t necessarily malicious, but its server may log requests, and you have no idea how long that is kept or who can read it.
Tokens are often still valid
A bearer token works for whoever holds it. If you copied it from a live session a minute ago, it probably still works, and many systems have no way to cancel a single JWT before it expires.
Signing secrets are worse. An HS256 secret lets anyone create tokens your API will accept, for any user, until you rotate it.
API responses contain personal data
The response you are debugging may well contain customer names, email addresses, order histories or delivery addresses. If you are responsible for that data, pasting it into a third party’s service is a disclosure you can’t take back, and under the UK GDPR you need a lawful basis to share personal data with anyone. “I wanted it indented nicely” is not a reason I would want to explain. The ICO has guidance if you need the detail.
How to tell whether a tool processes locally
A privacy claim on the page is a start, but you can check for yourself in a couple of minutes.
Watch the Network tab
Open your browser’s developer tools (F12 in most browsers), choose the Network tab, clear it, then paste some harmless test data into the tool and use it. If your input appears in a request body or URL, especially under Fetch/XHR, it has left your machine. On a site with adverts, including this one, you will see requests to ad servers as pages load. What matters is whether any of them contain what you pasted. The browser’s search box in the Network panel (Ctrl+F while it is focused) can look through request URLs and bodies for a distinctive test string.
Go offline after the page loads
Load the tool, then disconnect from the internet, or use the Offline option in the Network tab’s throttling menu. If the tool still works, the processing happens on your device. That doesn’t prove it won’t send something once you reconnect, but a server-based tool will simply stop working.
Look at the Content-Security-Policy
In the Network tab, click the page’s own document request and look at the response headers for Content-Security-Policy. A connect-src 'self' directive tells the browser to block scripts on the page from connecting to other sites. But 'self' still allows requests to the site’s own server, so it rules out third parties, not the site itself. Sites that carry adverts or analytics usually have to allow many other hosts, so a looser policy is not a red flag on its own. Use it alongside the Network tab check.
Remember the other things that can see your input
Browser extensions with permission to read every site can see what you paste into any page, and clipboard history tools keep what you copy. Third-party scripts on the page itself, such as adverts, analytics or chat widgets, run with access to that page too. That applies to every tool, local or not.
Check mine too
I built the NiftyKit tools to run entirely in your browser for exactly these reasons. Your input isn’t uploaded, logged or stored, my code never passes it to the adverts that pay for the site, and an automated test checks that no request from any tool ever contains it. The JWT Decoder and the certificate decoder carry no ads at all, and their pages send a strict connect-src 'self' policy, so the browser itself blocks any connection to another site. But you shouldn’t have to take my word for it. Run the same checks on these pages that you would on anyone else’s. For a real production secret, the offline test above is also the safest way to use any tool, mine included: once you are offline, nothing on the page can send anything anywhere.
A safer workflow
Start by reducing what you paste
Before reaching for any tool, ask whether you need the real thing. Usually you don’t.
- Use an expired token or one from a test environment. The structure is the same.
- Replace personal data with obvious placeholders before pasting.
- Trim the response to the part you are debugging.
{
"id": 48213,
"customer": { "name": "REDACTED", "email": "[email protected]" },
"status": "refunded",
"refunded_at": null
}
For anything truly sensitive, command-line tools on your own machine are the most private option.
Which tool for which job
| Job | Tool | Worth knowing |
|---|---|---|
| Read or validate a JSON response | JSON Formatter | Flags duplicate keys and integers too large for JavaScript to hold exactly |
| Compare two responses | JSON Diff | Ignores key order and formatting, lists each change by path |
| Inspect a JWT | JWT Decoder | Decoding is not verifying, so check the signature separately |
Read exp, iat and other timestamps |
Timestamp Converter | Detects seconds, milliseconds, microseconds and nanoseconds |
| Decode Base64 or Base64URL | Base64 Encoder/Decoder | Detects files such as images or PDFs in the decoded data |
| Decode a query string or redirect URL | URL Encoder/Decoder | Can treat + as a space for form data |
| Turn a response into types | JSON to TypeScript | Paste several items so optional fields are detected |
Format and validate the JSON
The JSON Formatter pretty-prints or validates, and gives the line and column of any syntax error. Two of its warnings matter for API work. Duplicate keys are valid JSON, but only the last value is kept. And integers above 9,007,199,254,740,991 can’t be represented exactly in JavaScript, so a large numeric ID can quietly change. IDs that big belong in strings.
Compare two responses
When something works in staging and not in production, the JSON Diff compares the data rather than the text, so key order and indentation don’t count as changes. Each difference is listed with a path like $.items[2].price. Types matter: 1 and "1" are different, which is exactly the kind of bug a text diff hides.
Decode the JWT, and know what that means
A JWT has three parts separated by dots, each Base64URL-encoded:
- Header: the token type and signing algorithm, such as
HS256orRS256, and often akidsaying which key signed it. - Payload: the claims, statements about the user and the token.
- Signature: proof that the header and payload were made by someone holding the right key and haven’t been changed.
The header and payload are encoded, not encrypted. Anyone can read them, which is why you can decode a token without any key. It also means decoding tells you nothing about whether the token is genuine. Your server must check the signature, the time claims, the issuer and the audience.
The registered claims, defined in RFC 7519, are:
| Claim | Name | Meaning |
|---|---|---|
iss |
Issuer | Who created and signed the token |
sub |
Subject | Who the token is about, usually a user ID |
aud |
Audience | Who the token is intended for |
exp |
Expiration time | Must not be accepted after this |
nbf |
Not before | Must not be accepted before this |
iat |
Issued at | When the token was created |
jti |
JWT ID | A unique identifier for this token |
The JWT Decoder shows the header and payload, explains each claim with dates in your local time, and tells you whether the token has expired or isn’t valid yet, judged by your device’s clock.
It can also verify the signature: paste the shared secret for HS256, HS384 or HS512, or the issuer’s public key (PEM, JWK or a JWK Set) for the RS, PS, ES and EdDSA algorithms. Where you can, verify with a public key. It is public by design, whereas an HMAC secret from production should stay in your secrets manager.
If a token has five parts instead of three, it is encrypted (JWE) and only the header is readable. Some services use opaque access tokens that aren’t JWTs at all and can’t be decoded.
Read the timestamps
JWT time claims are in seconds since 1970, not milliseconds. JavaScript’s Date.now() is in milliseconds, and mixing the two up is a classic bug. Compare exp with Date.now() directly and every token looks expired. Set exp from Date.now() when issuing and your tokens stay valid for tens of thousands of years.
"iat": 1793606400,
"exp": 1793610000
Paste either into the Timestamp Converter and it shows the date in UTC and your local time, plus how long ago or how far ahead it is. Here, the token was issued at 08:00 UTC on 2 November 2026 and expires an hour later.
Decode Base64 and URLs
Base64 is encoding, not protection. A Basic Authorization header is just username:password in Base64, so decoding one reveals the password. The Base64 Encoder/Decoder handles standard and URL-safe Base64, with or without padding, and if the result is a file it detects the type so you can preview or download it.
Query strings and OAuth redirect URLs are percent-encoded, sometimes twice. The URL Encoder/Decoder decodes them, and Use as input runs the result through again. Redirect URLs can carry authorisation codes, so the same care applies.
Generate types from a sample
JSON to TypeScript turns a response into TypeScript types, a Zod schema or a JSON Schema. Paste several items from a list so fields that are sometimes missing or null are typed correctly. The output describes structure only, since values aren’t turned into literal types, so customer data doesn’t end up in your codebase.
A quick routine for next time
- Ask whether you need the real token or data. Use an expired or test token, and redact personal data, if you can.
- Before trusting a new online tool, open the Network tab, paste test data and watch what is sent. Try it offline.
- Never paste a production signing secret into a website. Verify with a public key where possible.
- Remember that decoding a JWT isn’t verifying it, and that
expis in seconds. - If you think you pasted a live secret somewhere you shouldn’t have, rotate it.
