> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sumvin.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Verifying a JWT

> Step-by-step guide to verifying SIS-issued JWTs

This guide walks a **verifier** through the complete <Tooltip headline="JWT" tip="JSON Web Token — the SIS-issued credential a verifier receives." cta="Glossary →" href="/glossary">JWT</Tooltip> verification flow for merchants and service providers receiving Sumvin identity credentials. Each JWT is issued against a <Tooltip headline="Stamped Mandate" tip="A signed authorisation a user grants for specific scoped actions — delivered on the wire as a PINT." cta="Glossary →" href="/glossary">Stamped Mandate</Tooltip> — the cryptographic authorisation the user signed.

<Snippet file="product-term-disambiguation.mdx" />

A Stamped Mandate is delivered over the API as a `PINT` (header `x-sumvin-pint-token`), so the JWT you verify carries `PINT`-prefixed claims and the source mandate is identified by its `pint_uri`.

## Prerequisites

* Your registered audience identifier (provided during partner onboarding)
* A JWT verification library with JWKS support
* (Optional) An SIS API key for revocation checks

## Step-by-Step Verification

### 1. Extract the Token

Pull the Stamped Mandate's JWT from the `x-sumvin-pint-token` header. The value is the raw JWT — there is no `Bearer` prefix to strip.

```typescript theme={null}
const token = request.headers.get("x-sumvin-pint-token");
if (!token) {
  throw new Error("Missing x-sumvin-pint-token header");
}
```

```python theme={null}
token = request.headers.get("x-sumvin-pint-token", "")
if not token:
    raise ValueError("Missing x-sumvin-pint-token header")
```

### 2. Fetch the JWKS and Verify the Signature

The SIS publishes its public keys at the [<Tooltip headline="JWKS" tip="JSON Web Key Set — the public-key document used to validate PINT JWTs." cta="Glossary →" href="/glossary">JWKS</Tooltip> endpoint](/merchant/jwks). Use your JWT library's built-in JWKS support to fetch and cache the keys.

**TypeScript (jose):**

```typescript theme={null}
import * as jose from "jose";

const JWKS_URL = "https://sis.sumvin.com/v0/sis/.well-known/jwks.json";
const MY_AUDIENCE = "your-registered-identifier.example.com";

const JWKS = jose.createRemoteJWKSet(new URL(JWKS_URL));

const { payload } = await jose.jwtVerify(token, JWKS, {
  issuer: "https://sis.sumvin.com",
  audience: MY_AUDIENCE,
  algorithms: ["ES256"],
});
```

**Python (PyJWT):**

```python theme={null}
import jwt
from jwt import PyJWKClient

JWKS_URL = "https://sis.sumvin.com/v0/sis/.well-known/jwks.json"
MY_AUDIENCE = "your-registered-identifier.example.com"

jwks_client = PyJWKClient(JWKS_URL)
signing_key = jwks_client.get_signing_key_from_jwt(token)

payload = jwt.decode(
    token,
    signing_key.key,
    algorithms=["ES256"],
    audience=MY_AUDIENCE,
    issuer="https://sis.sumvin.com",
)
```

Both examples validate:

* The JWT signature against the SIS public key
* The `iss` claim is `"https://sis.sumvin.com"`
* The `aud` claim matches your registered identifier
* The `exp` claim hasn't passed

### 3. Read the Claims

After verification, read the Sumvin-specific claims to understand what the user has authorised:

```typescript theme={null}
const {
  sub,               // User's SRI
  wallet,            // User's Safe wallet address
  kyc_status,        // "verified", etc.
  scopes,            // ["sr:us:pint:identity:proof_of_personhood", ...]
  pint_uri,          // SRI of the source PINT
  signer_type,       // "user" or "agent"
  verification_tier, // "standard" or "enhanced"
} = payload;
```

See the [JWT Structure](/identity/jwt) for the complete claims reference.

### 4. Check Revocation (Recommended)

Even if the JWT hasn't expired, the source Stamped Mandate may have been revoked. Call the revocation check endpoint to confirm it's still valid:

```typescript theme={null}
const pintId = payload.pint_uri; // e.g., "sr:us:pint:abc123"

const response = await fetch(
  `https://sis.sumvin.com/v0/sis/pint/${encodeURIComponent(pintId)}/status`,
  { headers: { Authorization: `Bearer ${SIS_API_KEY}` } }
);

const status = await response.json();
if (!status.valid) {
  throw new Error(`PINT revoked: ${status.reason}`);
}
```

```python theme={null}
import requests
from urllib.parse import quote

pint_id = payload["pint_uri"]

response = requests.get(
    f"https://sis.sumvin.com/v0/sis/pint/{quote(pint_id, safe='')}/status",
    headers={"Authorization": f"Bearer {SIS_API_KEY}"},
)

status = response.json()
if not status["valid"]:
    raise ValueError(f"PINT revoked: {status['reason']}")
```

See [Revocation Checking](/merchant/revocation) for caching guidance and response details.

### 5. Check the Verification Tier

If the JWT's `verification_tier` is `"enhanced"`, you must also verify the Stamped Mandate's EIP-712 signature. See [Verifying PINT Signatures](/merchant/verify-pint-signature).

```typescript theme={null}
if (payload.verification_tier === "enhanced") {
  // Enhanced tier — verify X-Pint-Signature and X-Pint-Payload headers
  verifyPintSignature(request, payload);
}
```

## Complete Example

```typescript theme={null}
import * as jose from "jose";

const JWKS_URL = "https://sis.sumvin.com/v0/sis/.well-known/jwks.json";
const MY_AUDIENCE = "your-registered-identifier.example.com";
const SIS_API_KEY = process.env.SIS_API_KEY;

const JWKS = jose.createRemoteJWKSet(new URL(JWKS_URL));

async function verifySumvinCredentials(request: Request) {
  // 1. Extract the PINT JWT from the x-sumvin-pint-token header
  const token = request.headers.get("x-sumvin-pint-token");
  if (!token) {
    throw new Error("Missing x-sumvin-pint-token header");
  }

  // 2. Verify JWT signature, issuer, audience, and expiry
  const { payload } = await jose.jwtVerify(token, JWKS, {
    issuer: "https://sis.sumvin.com",
    audience: MY_AUDIENCE,
    algorithms: ["ES256"],
  });

  // 3. Check KYC status
  if (payload.kyc_status !== "verified") {
    throw new Error(`KYC not verified: ${payload.kyc_status}`);
  }

  // 4. Check revocation (optional but recommended)
  if (SIS_API_KEY) {
    const pintId = payload.pint_uri as string;
    const statusRes = await fetch(
      `https://sis.sumvin.com/v0/sis/pint/${encodeURIComponent(pintId)}/status`,
      { headers: { Authorization: `Bearer ${SIS_API_KEY}` } }
    );
    const status = await statusRes.json();
    if (!status.valid) {
      throw new Error(`PINT revoked: ${status.reason}`);
    }
  }

  // 5. Handle enhanced tier if needed
  if (payload.verification_tier === "enhanced") {
    // See: /merchant/verify-pint-signature
    verifyPintSignature(request, payload);
  }

  return payload;
}
```
