Skip to main content
C
CodeUtil

Debugging APIs with Base64 and JWT Decoding

Master API debugging by learning to decode Base64 payloads and inspect JWT tokens. Troubleshoot authentication issues, read encoded error messages, and understand what your APIs are actually sending and receiving.

2025-05-1512 min
Related toolJWT Decoder

Use the tool alongside this guide for hands-on practice.

The 401 error that wasted my entire morning

I was staring at a 401 Unauthorized response for two hours. The token looked fine. The API endpoint was correct. Everything should have worked. Then I finally decoded the JWT and saw it: the token had expired 3 minutes ago. Three minutes.

That morning taught me something I now tell every developer at Šikulovi s.r.o.: when you're debugging APIs, your first move should always be decoding. JWTs, Base64 payloads, encoded error messages - there's usually critical information hiding in plain sight, just scrambled.

How I spot encoded data instantly

After years of API work, I've trained my eyes to recognize encoded data patterns. Here's what I look for:

  • Base64: Only A-Z, a-z, 0-9, +, /, and = characters. Often ends with = padding
  • JWT: Three parts separated by dots, almost always starts with eyJ (that's {"alg in Base64)
  • Data URIs: Start with data:...;base64, - you'll see these in image responses
  • Authorization headers: Bearer tokens are usually JWTs, Basic auth is Base64-encoded credentials

The debugging scenarios I hit most often

These are the situations where I reach for my decoder tools. If you recognize the pattern, you'll solve it faster:

  • 401 errors: 90% of the time it's an expired JWT. Decode and check exp claim first
  • 403 Forbidden: Token is valid but missing permissions. Check the roles/scope claims
  • Garbled response data: It's probably Base64-encoded binary. Decode it
  • Webhook failures: The payload signature verification failed. Decode and compare hashes
  • OAuth redirects failing: State parameter mismatch. Decode both and compare

My quick decode workflow

I've tried every method. Here's what I actually use day-to-day:

  • Browser console for quick checks: atob("SGVsbG8gV29ybGQ=") - instant
  • Command line when I'm already in terminal: echo "..." | base64 --decode
  • CodeUtil's Base64 tool when I need to see the result nicely formatted
  • Watch out for URL-safe Base64 (uses - and _ instead of + and /). JWTs use this

Reading JWTs like a pro

A JWT has three parts separated by dots: header.payload.signature. The first two are just Base64-encoded JSON - you can read them. The signature? That's cryptographic gibberish, don't bother.

Here's my mental model: header tells you HOW it's signed, payload tells you WHO and WHEN, signature proves it wasn't tampered with.

  • Header: Usually just {"alg":"HS256","typ":"JWT"} - check the algorithm
  • Payload: The good stuff - user ID, expiration, roles, permissions
  • Signature: You can't decode this meaningfully, but you can verify it with the right key

The JWT claims I check first

When a JWT isn't working, I have a mental checklist. In order of how often they're the problem:

  • exp (expiration): Is it in the past? This is the culprit 60% of the time
  • iss (issuer): Does it match your auth server? Typos happen
  • aud (audience): Some APIs check this strictly. Make sure it matches
  • sub (subject): Is this actually the user you think it is?
  • Custom claims (roles, permissions): Missing the required scope?

The expired token dance

Here's my exact process when I suspect expiration:

  • Decode the payload, grab the exp value
  • It's a Unix timestamp in seconds. Run: new Date(exp * 1000).toISOString()
  • Compare to now: Date.now() / 1000 < exp ? "valid" : "expired"
  • Also check iat (issued at) - sometimes tokens are issued with wrong timestamps
  • Remember clock skew - your server might be a few minutes off from the auth server

When signatures fail

Signature failures are trickier. Usually it's one of these:

  • Wrong algorithm: Your API expects RS256 but the token uses HS256
  • Key rotation: The signing key changed after this token was issued. Check the kid claim
  • Wrong secret/key: You're verifying with the wrong key entirely
  • Copy-paste errors: Whitespace or encoding issues when you copied the token. Classic

Hidden error messages

Some APIs hide detailed errors in Base64. I've found gold in headers like X-Error-Details and WWW-Authenticate. Always try decoding anything that looks like Base64 in an error response - you might find the actual stack trace or validation message.

OAuth debugging (the fun part)

OAuth flows have tokens everywhere. Here's where to look:

  • ID token: This is a JWT with user info. Decode it to see what the auth server says about them
  • Access token: Might be a JWT, might be opaque. Try decoding - worst case it's gibberish
  • State parameter: Usually Base64-encoded. Check if your state matches what came back
  • Always verify the aud claim matches your client ID - mismatches cause silent failures

My DevTools workflow

I keep browser DevTools open constantly during API work. Here's my routine:

  • Network tab → find the failing request → copy the Authorization header
  • Console tab → JSON.parse(atob(token.split(".")[1])) for quick JWT decode
  • Application tab → check localStorage/sessionStorage for cached tokens
  • Right-click any request → Copy as cURL → replay in terminal with modifications

Terminal one-liners I use constantly

When I'm already in terminal, these save time:

  • echo "encoded" | base64 --decode - basic decode
  • echo "header.payload.sig" | cut -d. -f2 | base64 --decode | jq . - pretty JWT payload
  • curl -s -H "Authorization: Bearer $TOKEN" $URL | jq . - test with token

Webhook signature debugging

Webhooks are tricky because the signature must match EXACTLY. I've wasted hours on these:

  • Make sure you're hashing the RAW body, not pretty-printed JSON
  • Check if you need the body as bytes vs string
  • Verify you have the right webhook secret - easy to mix up production/staging
  • Some providers include timestamps in signatures - check for replay protection

Common decode failures and fixes

When decoding fails, it's usually one of these:

  • Invalid character: URL-safe Base64 uses - and _ instead of + and /
  • Invalid length: Add = padding to make length a multiple of 4
  • Garbled output: The decoded bytes may not be UTF-8 text—could be binary
  • Double encoding: Data may be encoded twice—decode again if result looks like Base64
  • Line breaks: MIME Base64 includes line breaks—remove them before decoding
  • Whitespace: Trim input before decoding
  • Truncation: Partial Base64 strings fail to decode properly

My step-by-step debug checklist

When auth fails and I need to be systematic, this is my exact process:

  • 1. Capture the full request (I use browser DevTools or curl -v)
  • 2. Find the token - usually Authorization header or a cookie
  • 3. Decode header - check algorithm (alg), look for kid if using key rotation
  • 4. Decode payload - check exp first, then iss, aud, and required claims
  • 5. If everything looks right, try a fresh token to isolate if it's this specific token

Security note (I learned this the hard way)

Be careful where you paste tokens. Production JWTs contain real user data. I only use CodeUtil's decoder or local tools for production tokens - never random online decoders. Even expired tokens are sensitive.

What I wish I knew earlier

After that 2-hour 401 debugging session, I built decoding into my default workflow. Now I decode first, ask questions later. It's saved me countless hours at Šikulovi s.r.o..

Keep decoder tools readily available, understand JWT claim structure, and follow a methodical approach to troubleshooting. These skills reduce debugging time and help you resolve API issues with confidence.

FAQ

How do I decode a JWT token without using online tools?

Split the JWT by dots to get three parts. The first two parts (header and payload) are Base64URL-encoded JSON. In JavaScript: JSON.parse(atob(token.split(".")[1])) decodes the payload. In the terminal: echo "token" | cut -d. -f2 | base64 --decode. The third part (signature) cannot be meaningfully decoded—it is cryptographic data.

Why does my API return 401 even with a valid token?

Usually: expired token (check exp), wrong audience (aud), clock skew, or rotated keys. Decode the token first - 90% of the time the answer is right there in the claims.

What is the difference between Base64 and Base64URL encoding?

Base64URL replaces + with - and / with _ to make tokens URL-safe. JWTs use Base64URL. If standard decoding fails, try replacing those characters first.

How can I tell if an API response is Base64-encoded?

Look for strings with only letters, numbers, +, /, and =. Length is always divisible by 4. When in doubt, try decoding it - you'll know immediately if it works.

Why does my decoded JWT payload show "exp" as a large number?

That's a Unix timestamp (seconds since 1970). Convert with new Date(exp * 1000) in JavaScript. Compare to Date.now()/1000 to see if it's expired.

Can I modify a JWT token for debugging purposes?

You can decode and view JWT contents, but modifying any part invalidates the signature. The server will reject altered tokens. For debugging, generate new tokens with the desired claims through your authentication system. If you need to test specific claim values, use your auth server's admin tools or test token generation endpoint.

How do I debug webhook signature verification failures?

Use the exact raw body bytes - don't parse and re-serialize JSON. Check the algorithm (usually HMAC-SHA256) and verify your secret hasn't rotated.

What tools can I use to decode Base64 and JWTs locally?

Terminal: base64 --decode. Browser: atob(). For JWTs specifically, jwt-cli is handy. For sensitive tokens, always use local tools, never paste into random websites.

Martin Šikula

Founder of CodeUtil. Web developer building tools I actually use. When I'm not coding, I experiment with productivity techniques (with mixed success).

Related articles