Skip to main content
This guide walks through the complete verification flow for merchants and service providers receiving Sumvin identity credentials.

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 PINT JWT from the x-sumvin-pint-token header. The value is the raw JWT — there is no Bearer prefix to strip.
const token = request.headers.get("x-sumvin-pint-token");
if (!token) {
  throw new Error("Missing x-sumvin-pint-token header");
}
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 endpoint. Use your JWT library’s built-in JWKS support to fetch and cache the keys. TypeScript (jose):
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):
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:
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 for the complete claims reference. Even if the JWT hasn’t expired, the source PINT may have been revoked. Call the revocation check endpoint to confirm it’s still valid:
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}`);
}
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 for caching guidance and response details.

5. Check the Verification Tier

If the JWT’s verification_tier is "enhanced", you must also verify the PINT signature. See Verifying PINT Signatures.
if (payload.verification_tier === "enhanced") {
  // Enhanced tier — verify X-Pint-Signature and X-Pint-Payload headers
  verifyPintSignature(request, payload);
}

Complete Example

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;
}