Quickstart
From a JWT and a base URL to a fully validated request in five minutes.
Prerequisites
You need:
- An API key issued from the dashboard (
jws_live_<id>_<secret>). Every/v1/*call on the hosted API requires it as a Bearer token. Self-hosters skip this step. - A reachable JWTShield base URL. The hosted API lives at
https://api.jwtshield.com; self-hosters point at their own deployment. - A JWT to inspect. The examples below use a sample HS256 token; substitute your own once the flow runs.
- A trust source for validation: either an inline policy (a shared secret or PEM public key plus issuer/audiences/algs) or the ID of an issuer profile registered with the service via
ISSUER_PROFILES_JSON.
1. Set the base URL and API key
Every example honours two env vars: JWTSHIELD_URL (defaults to the hosted API at https://api.jwtshield.com) and JWTSHIELD_KEY (the API key from your dashboard). Set them once in your shell:
export JWTSHIELD_URL=https://api.jwtshield.com
export JWTSHIELD_KEY=jws_live_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
A self-hosted instance does not require JWTSHIELD_KEY. Drop the Authorization header from the examples below and point JWTSHIELD_URL at your deployment (e.g. http://localhost:8000).
2. Decode a token
POST /v1/inspect/token parses the three JWT segments and returns the decoded header, payload, and signature_segment plus a list of suspicious_warnings. It does not verify the signature or any claims.
curl -sS -X POST "$JWTSHIELD_URL/v1/inspect/token" \
-H "Authorization: Bearer $JWTSHIELD_KEY" \
-H "Content-Type: application/json" \
-d '{"token":"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3JfMSJ9.x"}' | jq .const BASE_URL = process.env.JWTSHIELD_URL || "https://api.jwtshield.com";
const KEY = process.env.JWTSHIELD_KEY;
const r = await fetch(`${BASE_URL}/v1/inspect/token`, {
method: "POST",
headers: {
"Authorization": `Bearer ${KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ token: process.argv[2] }),
});
if (!r.ok) throw new Error(`HTTP ${r.status}`);
const body = await r.json();
console.log(body.decoded.header);
console.log(body.decoded.payload);
console.log("suspicious:", body.suspicious_warnings);import os, sys, httpx
BASE_URL = os.environ.get("JWTSHIELD_URL", "https://api.jwtshield.com")
KEY = os.environ["JWTSHIELD_KEY"]
r = httpx.post(
f"{BASE_URL}/v1/inspect/token",
headers={"Authorization": f"Bearer {KEY}"},
json={"token": sys.argv[1]},
timeout=10.0,
)
r.raise_for_status()
body = r.json()
print(body["decoded"]["header"])
print(body["decoded"]["payload"])
print("suspicious:", body["suspicious_warnings"])A successful response looks like:
{
"decoded": {
"header": { "alg": "HS256", "typ": "JWT" },
"payload": { "sub": "usr_1" },
"signature_segment": "x"
},
"validated": false,
"warning": "Token was decoded only. Decoded does not mean validated: signature, issuer, audience, and claim policy were not checked.",
"suspicious_warnings": ["KID_MISSING", "EXP_MISSING"]
}
The validated field on /v1/inspect/token is always false. Treating decoded payload claims as authorization input is the JWT bug class JWTShield exists to prevent. Use /v1/validate/jwt for any decision that affects state.
3. Validate the token
POST /v1/validate/jwt runs the full check: signature verification, algorithm allowlist, issuer match, audience match, expiry/not-before, required claims. Pass either policy (inline) or issuer_profile_id (registered upstream) — exactly one is required.
curl -sS -X POST "$JWTSHIELD_URL/v1/validate/jwt" \
-H "Authorization: Bearer $JWTSHIELD_KEY" \
-H "Content-Type: application/json" \
-d @- <<JSON
{
"token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c3JfMSJ9.x",
"policy": {
"secret": "your-256-bit-secret",
"issuer": "https://issuer.example.com",
"audiences": ["api://backend"],
"allowed_algs": ["HS256"]
}
}
JSONconst policy = {
secret: "your-256-bit-secret",
issuer: "https://issuer.example.com",
audiences: ["api://backend"],
allowed_algs: ["HS256"],
};
const r = await fetch(`${process.env.JWTSHIELD_URL}/v1/validate/jwt`, {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.JWTSHIELD_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ token, policy }),
});
const body = await r.json();
if (!body.valid) {
for (const f of body.findings) {
console.error(`${f.code} (${f.severity}): ${f.message}`);
}
process.exit(1);
}policy = {
"secret": "your-256-bit-secret",
"issuer": "https://issuer.example.com",
"audiences": ["api://backend"],
"allowed_algs": ["HS256"],
}
r = httpx.post(
f"{BASE_URL}/v1/validate/jwt",
headers={"Authorization": f"Bearer {KEY}"},
json={"token": token, "policy": policy},
timeout=10.0,
)
r.raise_for_status()
body = r.json()
if not body["valid"]:
for f in body["findings"]:
print(f"{f['code']} ({f['severity']}): {f['message']}")
raise SystemExit(1)A successful response:
{
"valid": true,
"statuses": {
"signature": "pass",
"issuer": "pass",
"audience": "pass",
"algorithm": "pass",
"time": "pass",
"required_claims": "pass"
},
"findings": [],
"summary": "Token is valid: signature verified, issuer/audience/time/required-claims all passed."
}
4. Handle invalid tokens
Validation failures still return HTTP 200 with valid: false. Inspect findings[] to handle each failure case explicitly. Never short-circuit on statuses.signature == "pass" alone — check valid.
{
"valid": false,
"statuses": {
"signature": "pass",
"issuer": "pass",
"audience": "fail",
"algorithm": "pass",
"time": "pass",
"required_claims": "pass"
},
"findings": [
{
"code": "AUDIENCE_MISMATCH",
"severity": "error",
"message": "Token aud claim does not match any allowed audience.",
"evidence": { "token_aud": "api://other", "allowed_audiences": ["api://backend"] },
"remediation": "Issue tokens with aud=\"api://backend\" or add \"api://other\" to your policy."
}
],
"summary": "Token is NOT valid: audience mismatch."
}
The full list of codes is in Errors.
Next steps
- API · Inspect — POST /v1/inspect/token — full schema for the decode endpoint and the
suspicious_warningstaxonomy. - API · Validate — POST /v1/validate/jwt — every field of
VerifyPolicyandVerifyResult. - Examples — drop-in snippets for curl, Node, Python, GitHub Actions, and GitLab CI.
- Errors — every error code, what triggers it, what to do about it.