A JWT is a signed claims document, not a secret envelope
A JSON Web Token (JWT, RFC 7519) is a compact way to carry a small JSON object and a cryptographic signature in a single string. Anyone who sees the token can read the claims. Only someone holding the signing key can produce a valid signature.
That distinction is the one that trips people up. JWTs are signed, not encrypted. If a claim must stay confidential, it does not belong in a JWT. If you need confidentiality as well as integrity, that is JWE (JSON Web Encryption) – a different standard, and not what most APIs mean when they say “send a JWT.”
On the wire a JWT almost always travels as a bearer token:
GET /v1/orders HTTP/1.1
Host: api.partner.example
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ2VudC0xIn0.SflKxwRJ...Whoever bears that string can use it until it expires. TLS protects it in transit. The signature protects it from being altered. Neither protects it from being copied.
Three segments, two dots
A JWT is always header.payload.signature. Each segment is Base64URL-encoded JSON (the signature is Base64URL of raw bytes).
header . payload . signature
eyJhbGci . eyJzdWIiOiJhZ2VudC0xIiwiaXNzIjo . SflKxwRJSM...Decoded, those look like:
{ "alg": "RS256", "typ": "JWT", "kid": "2026-03" }{ "iss": "https://idp.example.com/", "sub": "agent-1", "aud": "https://api.partner.example", "exp": 1772006400 }The header tells the verifier how to check the signature (alg) and, for asymmetric keys, which key to use (kid). The payload is the claims. The signature is computed over base64url(header) + "." + base64url(payload) – never over the decoded JSON – so even a whitespace change in the original JSON is irrelevant; only the encoded bytes matter.
If you change a claim without re-signing, the signature check fails. If you strip the signature, it is no longer a JWT.
Claims that actually matter
The spec defines a handful of registered claims. Four of them do most of the work in API traffic:
| Claim | Name | What a verifier uses it for |
|---|---|---|
iss | Issuer | “Did this token come from the identity provider I trust?” |
aud | Audience | “Was this token minted for me, or for some other API?” |
exp | Expiry | “Is this token still in its validity window?” |
sub | Subject | “Which principal does this token represent?” |
nbf (not before) and iat (issued at) tighten the time window. jti (JWT ID) is a unique token identifier, useful if you want to reject replays.
aud is the claim teams skip and then regret. An access token minted for Microsoft Graph (aud = 00000003-0000-0000-c000-000000000000) is not a token you can verify for your own API, even if the signature is valid for Graph. Audience mismatch is a configuration error, not a cryptography error. See How to Verify a JWT for the check order.
Custom claims (roles, scope, tenant_id) are just more JSON. They have no magic; a verifier that never looks at them never enforces them. Using those claims in gateway rules is a separate step from verifying the signature – covered in How to extract and use JWT claims.
HS* vs RS* vs ES*: the algorithm is a key-management choice
alg is not a quality rating. It picks the key type.
- HS256 / HS384 / HS512 (HMAC). One shared secret signs and verifies. Anyone who can verify can also mint. Fine for a closed pair of services that already share a secret. A disaster if the verifier is a fleet of untrusted callers.
- RS256 / RS384 / RS512 (RSA) and ES256 / ES384 / ES512 (ECDSA). A private key signs; the matching public key verifies. The verifier never holds minting power. Public keys are usually published at a JWKS URL (
/.well-known/jwks.json) and selected bykid.
That is why inbound JWT verification and outbound JWT minting are different credentials in RequestRocket, not one “JWT” toggle. Presenting a static token, minting a fresh token, and verifying a caller’s token are three jobs.
Three jobs, three credential types
When a gateway sits between a caller and an upstream API, JWT work splits by direction.
Caller --(inbound JWT)--> Gateway --(outbound JWT)--> Upstream API
^ ^
jwtVerify jwt (static string)
(check signature) jwtSigned (mint per request)jwt – present a static token. The secret is a pre-issued string. Required fields are token and addToHeader. The gateway attaches it the same way it would attach a bearer token. There is no signing step at request time. Use this when a vendor has already given you a long-lived JWT and you just need to send it.
jwtSigned – mint a token on the way out. Target credentials only. You supply signingAlgorithm and claimsTemplate, plus exactly one of privateKey (RS*/ES*) or sharedSecret (HS*). If both are present the API rejects the credential: mixing key types is how teams accidentally ship a PEM file and an HMAC secret and then cannot tell which one is live. tokenLifetimeSeconds adds exp when the template does not. headerName / headerPrefix default to Authorization / Bearer.
jwtVerify – check a caller’s token on the way in. Proxy credentials only. You supply algorithms and exactly one signing source: jwksUri or sharedSecret, never both. Accepting both would leave an unused secret on the record and an ambiguous failure mode when JWKS is down. Optional issuer, audience, and subject are compared only when you set them – omitting audience means audience is not checked, which is a choice, not a default-safe behaviour.
The Auth0 walkthrough stays in Add Auth0 JWT authentication to any API. The comparison against opaque keys stays in JWT vs API key auth for machine-to-machine APIs. This post is the format itself.
When not to use a JWT
A JWT is the wrong tool when you need immediate revocation (the token stays valid until exp unless you run a denylist), when the payload would contain secrets, or when the other party only accepts an opaque API key. For the last case, start with What is an API key.
Next steps
If you need to check inbound tokens, follow How to verify a JWT. If you need to configure a credential, the field-level reference is in the credentials guide. RequestRocket is runtime access control for AI agents and apps calling APIs you don’t own – every call gets a least-privilege credential, a policy check, and an audit record, with no code changes.