Skip to main content
Sumvin uses a progressive verification model. Your verification tier determines the level of identity assurance you receive about a user and the depth of verification your integration must perform. The tier is set by the scopes granted during token exchange and communicated to you via the verification_tier claim in the . Standard integrations receive a JWT attesting to the user’s identity and KYC status. Enhanced integrations additionally receive the user’s original cryptographic authorisation, giving you independent proof of intent beyond the SIS attestation.

Tier 1: Standard (JWT Only)

As a Standard-tier integrator, you receive a single JWT in the x-sumvin-pint-token header containing the user’s identity attestation. This covers most integration scenarios where you need to verify who the user is and their KYC status. The JWT’s verification_tier claim is "standard". Typical integrations: Identity verification, KYC status checks, age-gated access, personalisation data.

What You Receive

A single header:
x-sumvin-pint-token: <jwt>

Example Request

GET /api/user-profile HTTP/1.1
Host: partner-x.example.com
x-sumvin-pint-token: eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpcy0wMDEifQ.eyJpc3MiOiJodHRwczovL3Npcy5zdW12aW4uY29tIiwic3ViIjoic3I6dXM6dXNlcjo5ZjhkN2UiLCJhdWQiOiJwYXJ0bmVyLXguZXhhbXBsZS5jb20iLCJqdGkiOiJ0a25fYWJjMTIzIiwiaWF0IjoxNzQwMDAwMDAwLCJleHAiOjE3NDAwMDM2MDAsIndhbGxldCI6IjB4RTIzYzlBNzBCQzc0OUVCZGRkOGM3OEE4NjRGZDkxMUQwNEU5ZTk5MiIsImt5Y19zdGF0dXMiOiJ2ZXJpZmllZCIsInNjb3BlcyI6WyJzcjp1czpwaW50OmlkZW50aXR5OnByb29mX29mX3BlcnNvbmhvb2QiLCJzcjp1czpwaW50OmlkZW50aXR5Omt5Y19zdGF0dXMiXSwicGludF91cmkiOiJzcjp1czpwaW50OmRlZjQ1NiIsInNpZ25lcl90eXBlIjoidXNlciIsInZlcmlmaWNhdGlvbl90aWVyIjoic3RhbmRhcmQifQ.SIGNATURE

Decoded JWT

{
  "iss": "https://sis.sumvin.com",
  "sub": "sr:us:user:9f8d7e",
  "aud": "partner-x.example.com",
  "jti": "tkn_abc123",
  "iat": 1740000000,
  "exp": 1740003600,
  "wallet": "0xE23c9A70BC749EBddd8c78A864Fd911D04E9e992",
  "kyc_status": "verified",
  "scopes": ["sr:us:pint:identity:proof_of_personhood", "sr:us:pint:identity:kyc_status"],
  "pint_uri": "sr:us:pint:def456",
  "signer_type": "user",
  "verification_tier": "standard"
}
Standard-tier JWTs do not contain a pint_signature claim. If verification_tier is "standard", you only need to verify the JWT itself.

Verification Steps

TypeScript (jose):
import * as jose from "jose";

const JWKS_URL = "https://sis.sumvin.com/v0/sis/.well-known/jwks.json";
const MY_AUDIENCE = "partner-x.example.com";
const JWKS = jose.createRemoteJWKSet(new URL(JWKS_URL));

async function verifyStandardTier(request: Request) {
  const token = request.headers.get("x-sumvin-pint-token");
  if (!token) {
    throw new Error("Missing x-sumvin-pint-token header");
  }

  // 1. Validate JWT signature against JWKS
  // 2. Confirm aud matches your registered identifier
  // 3. Check exp hasn't passed
  const { payload } = await jose.jwtVerify(token, JWKS, {
    issuer: "https://sis.sumvin.com",
    audience: MY_AUDIENCE,
    algorithms: ["ES256"],
  });

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

  return payload;
}
Python (PyJWT):
import jwt
from jwt import PyJWKClient
import requests
from urllib.parse import quote

JWKS_URL = "https://sis.sumvin.com/v0/sis/.well-known/jwks.json"
MY_AUDIENCE = "partner-x.example.com"

jwks_client = PyJWKClient(JWKS_URL)

def verify_standard_tier(token: str) -> dict:
    if not token:
        raise ValueError("Missing x-sumvin-pint-token header")

    signing_key = jwks_client.get_signing_key_from_jwt(token)

    # 1. Validate JWT signature against JWKS
    # 2. Confirm aud matches your registered identifier
    # 3. Check exp hasn't passed
    payload = jwt.decode(
        token,
        signing_key.key,
        algorithms=["ES256"],
        audience=MY_AUDIENCE,
        issuer="https://sis.sumvin.com",
    )

    # 4. Optionally check revocation
    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']}")

    return payload
See Verifying a JWT for the complete implementation guide.

Tier 2: Enhanced (JWT + PINT Headers)

As an Enhanced-tier integrator, you receive the JWT plus the original cryptographic PINT signature (user- or agent-signed). This gives you independent proof of the user’s authorisation — you can verify the signed intent directly without relying solely on the SIS attestation. The JWT’s verification_tier claim is "enhanced" and includes a pint_signature claim. Typical integrations: Payment processing, spend authorisation, any action involving value transfer on behalf of the user.

What You Receive

Three headers:
HeaderDescription
x-sumvin-pint-tokenSIS-signed PINT JWT (same as Standard)
X-Pint-SignatureHex-encoded EIP-712 signature from the original PINT
X-Pint-PayloadBase64-encoded JSON of the signed PINT payload

Example Request

POST /api/execute-payment HTTP/1.1
Host: partner-x.example.com
x-sumvin-pint-token: eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpcy0wMDEifQ.eyJpc3MiOiJodHRwczovL3Npcy5zdW12aW4uY29tIiwic3ViIjoic3I6dXM6dXNlcjo5ZjhkN2UiLCJhdWQiOiJwYXJ0bmVyLXguZXhhbXBsZS5jb20iLCJqdGkiOiJ0a25fZ2hpNzg5IiwiaWF0IjoxNzQwMDAwMDAwLCJleHAiOjE3NDAwMDM2MDAsIndhbGxldCI6IjB4RTIzYzlBNzBCQzc0OUVCZGRkOGM3OEE4NjRGZDkxMUQwNEU5ZTk5MiIsImt5Y19zdGF0dXMiOiJ2ZXJpZmllZCIsInNjb3BlcyI6WyJzcjp1czpwaW50OmlkZW50aXR5OnByb29mX29mX3BlcnNvbmhvb2QiLCJzcjp1czpwaW50OnNwZW5kOmV4ZWN1dGU_bWF4PTEwMDAwMDAwJmFzc2V0PVVTRENAc2VpJmNoYWluX2lkPTEzMjkiXSwicGludF91cmkiOiJzcjp1czpwaW50OmRlZjQ1NiIsInNpZ25lcl90eXBlIjoidXNlciIsInZlcmlmaWNhdGlvbl90aWVyIjoiZW5oYW5jZWQiLCJwaW50X3NpZ25hdHVyZSI6IjB4YWJjMTIzZGVmNDU2Nzg5MGFiY2RlZjEyMzQ1Njc4OTBhYmNkZWYxMjM0NTY3ODkwYWJjZGVmMTIzNDU2Nzg5MGFiY2RlZjEyMzQ1Njc4OTBhYmNkZWYxMjM0NTY3ODkwYWJjZGVmMTIzNDU2Nzg5MGFiY2RlZjEyMzQ1Njc4OTAxYyJ9.SIGNATURE
X-Pint-Signature: 0xabc123def4567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12345678901c
X-Pint-Payload: eyJ3YWxsZXQiOiIweEUyM2M5QTcwQkM3NDlFQmRkZDhjNzhBODY0RmQ5MTFEMDRFOWU5OTIiLCJub25jZSI6NDIsInN0YXRlbWVudCI6IlB1cmNoYXNlIGF1dGhvcml6YXRpb24gZm9yIHBhcnRuZXIgWCIsInNjb3BlcyI6WyJzcjp1czpwaW50OmlkZW50aXR5OnByb29mX29mX3BlcnNvbmhvb2QiLCJzcjp1czpwaW50OnNwZW5kOmV4ZWN1dGU/bWF4PTEwMDAwMDAwJmFzc2V0PVVTRENAc2VpJmNoYWluX2lkPTEzMjkiXSwicmVzb3VyY2VzIjpbInNyOnVzOnBpbnQ6ZGVmNDU2Il0sIm1heEFtb3VudCI6MTAwMDAwMDAsIm1heEFtb3VudFRva2VuIjoiMHgwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIiwiZXhwaXJlc0F0IjoxNzQwMDAzNjAwLCJjaGFpbklkIjoxMzI5fQ==

Decoded JWT

{
  "iss": "https://sis.sumvin.com",
  "sub": "sr:us:user:9f8d7e",
  "aud": "partner-x.example.com",
  "jti": "tkn_ghi789",
  "iat": 1740000000,
  "exp": 1740003600,
  "wallet": "0xE23c9A70BC749EBddd8c78A864Fd911D04E9e992",
  "kyc_status": "verified",
  "scopes": ["sr:us:pint:identity:proof_of_personhood", "sr:us:pint:spend:execute?max=10000000&asset=USDC@sei&chain_id=1329"],
  "pint_uri": "sr:us:pint:def456",
  "signer_type": "user",
  "verification_tier": "enhanced",
  "pint_signature": "0xabc123def4567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12345678901c"
}

Decoded X-Pint-Payload

{
  "wallet": "0xE23c9A70BC749EBddd8c78A864Fd911D04E9e992",
  "nonce": 42,
  "statement": "Purchase authorization for partner X",
  "scopes": ["sr:us:pint:identity:proof_of_personhood", "sr:us:pint:spend:execute?max=10000000&asset=USDC@sei&chain_id=1329"],
  "resources": ["sr:us:pint:def456"],
  "maxAmount": 10000000,
  "maxAmountToken": "0x0000000000000000000000000000000000000000",
  "expiresAt": 1740003600,
  "chainId": 1329
}
chainId is an EIP-712 domain parameter, not part of the signed PurchaseIntent message. It is included in the payload for convenience so you can reconstruct the full EIP-712 domain when verifying the signature. The current default is 1329 (Sei).

Verification Steps

Enhanced verification includes all Standard steps plus cryptographic verification of the PINT signature. TypeScript (jose + viem):
import * as jose from "jose";
import { recoverTypedDataAddress } from "viem";

const JWKS_URL = "https://sis.sumvin.com/v0/sis/.well-known/jwks.json";
const MY_AUDIENCE = "partner-x.example.com";
const JWKS = jose.createRemoteJWKSet(new URL(JWKS_URL));

async function verifyEnhancedTier(request: Request) {
  const token = request.headers.get("x-sumvin-pint-token");
  if (!token) {
    throw new Error("Missing x-sumvin-pint-token header");
  }

  // 1. All Standard steps: validate JWT signature, aud, exp
  const { payload } = await jose.jwtVerify(token, JWKS, {
    issuer: "https://sis.sumvin.com",
    audience: MY_AUDIENCE,
    algorithms: ["ES256"],
  });

  // 2. Decode X-Pint-Payload (base64)
  const pintPayloadB64 = request.headers.get("x-pint-payload");
  if (!pintPayloadB64) {
    throw new Error("Missing X-Pint-Payload header");
  }
  const pintPayload = JSON.parse(
    Buffer.from(pintPayloadB64, "base64").toString("utf-8")
  );

  // 3. Reconstruct EIP-712 typed data and verify signature
  const pintSignature = request.headers.get("x-pint-signature") as `0x${string}`;
  if (!pintSignature) {
    throw new Error("Missing X-Pint-Signature header");
  }

  const signerType = payload.signer_type as string;

  if (signerType === "user") {
    // EOA signer: recover address via ECDSA
    const recoveredAddress = await recoverTypedDataAddress({
      domain: {
        name: "Sumvin Purchase Intent",
        version: "1",
        chainId: BigInt(pintPayload.chainId || 1329),
        verifyingContract: pintPayload.wallet as `0x${string}`,
      },
      types: {
        PurchaseIntent: [
          { name: "wallet", type: "address" },
          { name: "nonce", type: "uint256" },
          { name: "statement", type: "string" },
          { name: "scopes", type: "string[]" },
          { name: "resources", type: "string[]" },
          { name: "maxAmount", type: "uint256" },
          { name: "maxAmountToken", type: "address" },
          { name: "expiresAt", type: "uint256" },
        ],
      },
      primaryType: "PurchaseIntent",
      message: {
        wallet: pintPayload.wallet as `0x${string}`,
        nonce: BigInt(pintPayload.nonce),
        statement: pintPayload.statement,
        scopes: pintPayload.scopes,
        resources: pintPayload.resources,
        maxAmount: BigInt(pintPayload.maxAmount),
        maxAmountToken: pintPayload.maxAmountToken as `0x${string}`,
        expiresAt: BigInt(pintPayload.expiresAt),
      },
      signature: pintSignature,
    });

    // 4a. Verify recovered signer matches JWT wallet
    if (recoveredAddress.toLowerCase() !== (payload.wallet as string).toLowerCase()) {
      throw new Error("PINT signature does not match JWT wallet");
    }
  } else if (signerType === "agent") {
    // Agent (smart contract) signer: use EIP-1271 on-chain verification.
    // See "Verifying PINT Signatures" for the full EIP-1271 isValidSignature flow.
    throw new Error("Agent-signed PINT verification requires EIP-1271 — see /merchant/verify-pint-signature");
  } else {
    throw new Error(`Unknown signer_type: ${signerType}`);
  }

  // 5. Cross-check pint_signature JWT claim matches header
  if (payload.pint_signature !== pintSignature) {
    throw new Error("JWT pint_signature claim does not match X-Pint-Signature header");
  }

  return payload;
}
Python (PyJWT + eth_account):
import base64
import json
import jwt
from jwt import PyJWKClient
from eth_account.messages import encode_typed_data
from eth_account import Account

JWKS_URL = "https://sis.sumvin.com/v0/sis/.well-known/jwks.json"
MY_AUDIENCE = "partner-x.example.com"

jwks_client = PyJWKClient(JWKS_URL)

def verify_enhanced_tier(token: str, pint_signature: str, pint_payload_b64: str) -> dict:
    if not token:
        raise ValueError("Missing x-sumvin-pint-token header")

    # 1. All Standard steps: validate JWT signature, aud, exp
    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",
    )

    # 2. Decode X-Pint-Payload (base64)
    pint_payload = json.loads(base64.b64decode(pint_payload_b64).decode("utf-8"))

    # 3. Reconstruct EIP-712 typed data and recover signer
    domain_data = {
        "name": "Sumvin Purchase Intent",
        "version": "1",
        "chainId": pint_payload.get("chainId", 1329),
        "verifyingContract": pint_payload["wallet"],
    }
    message_types = {
        "PurchaseIntent": [
            {"name": "wallet", "type": "address"},
            {"name": "nonce", "type": "uint256"},
            {"name": "statement", "type": "string"},
            {"name": "scopes", "type": "string[]"},
            {"name": "resources", "type": "string[]"},
            {"name": "maxAmount", "type": "uint256"},
            {"name": "maxAmountToken", "type": "address"},
            {"name": "expiresAt", "type": "uint256"},
        ]
    }
    message_data = {
        "wallet": pint_payload["wallet"],
        "nonce": pint_payload["nonce"],
        "statement": pint_payload["statement"],
        "scopes": pint_payload["scopes"],
        "resources": pint_payload["resources"],
        "maxAmount": pint_payload["maxAmount"],
        "maxAmountToken": pint_payload["maxAmountToken"],
        "expiresAt": pint_payload["expiresAt"],
    }

    signer_type = payload.get("signer_type", "user")

    if signer_type == "user":
        # EOA signer: recover address via ECDSA
        signable = encode_typed_data(domain_data, message_types, message_data, "PurchaseIntent")
        recovered = Account.recover_message(signable, signature=bytes.fromhex(pint_signature[2:]))

        # 4a. Verify recovered signer matches JWT wallet
        if recovered.lower() != payload["wallet"].lower():
            raise ValueError("PINT signature does not match JWT wallet")
    elif signer_type == "agent":
        # Agent (smart contract) signer: use EIP-1271 on-chain verification.
        # See "Verifying PINT Signatures" for the full EIP-1271 isValidSignature flow.
        raise NotImplementedError("Agent-signed PINT verification requires EIP-1271 — see /merchant/verify-pint-signature")
    else:
        raise ValueError(f"Unknown signer_type: {signer_type}")

    # 5. Cross-check pint_signature JWT claim matches header
    if payload.get("pint_signature") != pint_signature:
        raise ValueError("JWT pint_signature claim does not match X-Pint-Signature header")

    return payload
The examples above only implement ECDSA recovery for user-signed PINTs (signer_type: "user"). Agent-signed PINTs (signer_type: "agent") use smart contract wallets and require EIP-1271 isValidSignature on-chain verification instead. See Verifying PINT Signatures for the complete EIP-1271 flow.
See Verifying PINT Signatures for additional detail on EIP-712 reconstruction and agent-signed PINTs.

How the Tier Is Determined

Scope-to-Tier Mapping

Scopes are SRI-format capability strings. In the MVP catalog, only sr:us:pint:spend:execute triggers Enhanced tier — all 13 other scopes are Standard.
ScopeTier
sr:us:pint:identity:kyc_statusStandard
sr:us:pint:identity:kyc_readStandard
sr:us:pint:identity:proof_of_personhoodStandard
sr:us:pint:identity:age_over_18Standard
sr:us:pint:spend:executeEnhanced
sr:us:pint:spend:rampStandard
sr:us:pint:perpetual:searchStandard
sr:us:pint:accounts:readStandard
sr:us:pint:accounts:linkStandard
sr:us:pint:accounts:transferStandard
sr:us:pint:transactions:readStandard
sr:us:pint:personalization:readStandard
sr:us:pint:cards:readStandard
sr:us:pint:cards:manageStandard
If any scope in the PINT maps to Enhanced, the entire JWT is issued at Enhanced tier. In practice, sr:us:pint:spend:execute is the only scope that triggers Enhanced tier.

What This Means for You

The tier is determined server-side during token exchange — you don’t compute it yourself. Your integration receives the tier as the verification_tier claim in the JWT, which tells you what level of verification to perform:
  • "standard" — verify the JWT only
  • "enhanced" — verify the JWT and the PINT signature headers
If a PINT requests scopes that the signer isn’t authorised for, the token exchange is rejected before any JWT is issued. You will only ever receive JWTs for authorised scope combinations.
The scopes available to your integration depend on your partner agreement. Contact your account manager to discuss scope access.

Choosing Your Implementation

Which Tier Applies to Your Integration

Your Integration TypeTierWhat You Verify
Identity verification (proof of personhood, age, KYC)StandardJWT only
Personalisation or marketing data accessStandardJWT only
Payment processing or spend authorisationEnhancedJWT + PINT signature
Compliance-critical actions requiring cryptographic proof of user intentEnhancedJWT + PINT signature
If your integration processes payments or authorises spending on behalf of users, you must implement Enhanced tier verification. Standard-tier JWT verification alone is not sufficient for spend authorisation.

Implementation Guides

  1. Verify a JWT — Standard tier. Validate JWT signature, audience, and expiry against the SIS JWKS endpoint.
  2. Verify PINT signatures — Enhanced tier. Verify the original EIP-712 PINT signature for independent cryptographic proof.