> ## 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.

# Verification Tiers

> Standard and Enhanced Sigil verification tiers for Sumvin identity credentials

The Sumvin <Tooltip headline="Sigil" tip="Sumvin's portable, KYC-verified identity — Proof of Personhood." cta="Glossary →" href="/glossary">Sigil</Tooltip> verification model is progressive — it comes in two tiers, **Standard** and **Enhanced**. As a verifier, your tier determines the level of identity assurance you receive about a user and the depth of verification you must perform. The tier is set by the scopes a user grants on their <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> (PINT) during token exchange, and communicated to you via the `verification_tier` claim in the <Tooltip headline="JWT" tip="JSON Web Token — the SIS-issued credential a verifier receives." cta="Glossary →" href="/glossary">JWT</Tooltip>.

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

A Stamped Mandate is delivered over the API as a `PINT` (header `x-sumvin-pint-token`). The wire vocabulary below — headers, claims, scope URIs — is identical whether you read it as a Stamped Mandate or its underlying PINT.

**Standard** verifiers receive a JWT attesting to the user's identity and KYC status. **Enhanced** verifiers 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 verifier, you receive a single PINT JWT in the `x-sumvin-pint-token` header containing the user's identity attestation. This covers most 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

```http theme={null}
GET /api/user-profile HTTP/1.1
Host: partner-x.example.com
x-sumvin-pint-token: eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpcy0wMDEifQ.eyJpc3MiOiJodHRwczovL3Npcy5zdW12aW4uY29tIiwic3ViIjoic3I6dXM6dXNlcjo5ZjhkN2UiLCJhdWQiOiJwYXJ0bmVyLXguZXhhbXBsZS5jb20iLCJqdGkiOiJ0a25fYWJjMTIzIiwiaWF0IjoxNzQwMDAwMDAwLCJleHAiOjE3NDAwMDM2MDAsIndhbGxldCI6IjB4RTIzYzlBNzBCQzc0OUVCZGRkOGM3OEE4NjRGZDkxMUQwNEU5ZTk5MiIsImt5Y19zdGF0dXMiOiJ2ZXJpZmllZCIsInNjb3BlcyI6WyJzcjp1czpwaW50OnNpZ2lsOnByb29mX29mX3BlcnNvbmhvb2QiLCJzcjp1czpwaW50OnNpZ2lsOmt5Y19zdGF0dXMiXSwicGludF91cmkiOiJzcjp1czpwaW50OmRlZjQ1NiIsInNpZ25lcl90eXBlIjoidXNlciIsInZlcmlmaWNhdGlvbl90aWVyIjoic3RhbmRhcmQifQ.SIGNATURE
```

### Decoded JWT

```json theme={null}
{
  "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:sigil:proof_of_personhood", "sr:us:pint:sigil:kyc_status"],
  "pint_uri": "sr:us:pint:def456",
  "signer_type": "user",
  "verification_tier": "standard"
}
```

<Note>
  Standard-tier JWTs do **not** contain a `pint_signature` claim. If `verification_tier` is `"standard"`, you only need to verify the JWT itself.
</Note>

### Verification Steps

**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 = "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):**

```python theme={null}
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](/merchant/verify-jwt) for the complete implementation guide.

***

## Tier 2: Enhanced (JWT + PINT Headers)

As an Enhanced-tier verifier, you receive the JWT plus the user's original cryptographic signature over the Stamped Mandate (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:

| Header                | Description                                          |
| --------------------- | ---------------------------------------------------- |
| `x-sumvin-pint-token` | SIS-signed PINT JWT (same as Standard)               |
| `X-Pint-Signature`    | Hex-encoded EIP-712 signature from the original PINT |
| `X-Pint-Payload`      | Base64-encoded JSON of the signed PINT payload       |

### Example Request

```http theme={null}
POST /api/execute-payment HTTP/1.1
Host: partner-x.example.com
x-sumvin-pint-token: eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InNpcy0wMDEifQ.eyJpc3MiOiJodHRwczovL3Npcy5zdW12aW4uY29tIiwic3ViIjoic3I6dXM6dXNlcjo5ZjhkN2UiLCJhdWQiOiJwYXJ0bmVyLXguZXhhbXBsZS5jb20iLCJqdGkiOiJ0a25fZ2hpNzg5IiwiaWF0IjoxNzQwMDAwMDAwLCJleHAiOjE3NDAwMDM2MDAsIndhbGxldCI6IjB4RTIzYzlBNzBCQzc0OUVCZGRkOGM3OEE4NjRGZDkxMUQwNEU5ZTk5MiIsImt5Y19zdGF0dXMiOiJ2ZXJpZmllZCIsInNjb3BlcyI6WyJzcjp1czpwaW50OnNpZ2lsOnByb29mX29mX3BlcnNvbmhvb2QiLCJzcjp1czpwaW50OnNwZW5kOmV4ZWN1dGU_bWF4PTEwMDAwMDAwJmFzc2V0PVVTRENAc2VpJmNoYWluX2lkPTEzMjkiXSwicGludF91cmkiOiJzcjp1czpwaW50OmRlZjQ1NiIsInNpZ25lcl90eXBlIjoidXNlciIsInZlcmlmaWNhdGlvbl90aWVyIjoiZW5oYW5jZWQiLCJwaW50X3NpZ25hdHVyZSI6IjB4YWJjMTIzZGVmNDU2Nzg5MGFiY2RlZjEyMzQ1Njc4OTBhYmNkZWYxMjM0NTY3ODkwYWJjZGVmMTIzNDU2Nzg5MGFiY2RlZjEyMzQ1Njc4OTBhYmNkZWYxMjM0NTY3ODkwYWJjZGVmMTIzNDU2Nzg5MGFiY2RlZjEyMzQ1Njc4OTAxYyJ9.SIGNATURE
X-Pint-Signature: 0xabc123def4567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12345678901c
X-Pint-Payload: eyJ3YWxsZXQiOiIweEUyM2M5QTcwQkM3NDlFQmRkZDhjNzhBODY0RmQ5MTFEMDRFOWU5OTIiLCJub25jZSI6NDIsInN0YXRlbWVudCI6IlB1cmNoYXNlIGF1dGhvcml6YXRpb24gZm9yIHBhcnRuZXIgWCIsInNjb3BlcyI6WyJzcjp1czpwaW50OnNpZ2lsOnByb29mX29mX3BlcnNvbmhvb2QiLCJzcjp1czpwaW50OnNwZW5kOmV4ZWN1dGU/bWF4PTEwMDAwMDAwJmFzc2V0PVVTRENAc2VpJmNoYWluX2lkPTEzMjkiXSwicmVzb3VyY2VzIjpbInNyOnVzOnBpbnQ6ZGVmNDU2Il0sIm1heEFtb3VudCI6MTAwMDAwMDAsIm1heEFtb3VudFRva2VuIjoiMHgwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwIiwiZXhwaXJlc0F0IjoxNzQwMDAzNjAwLCJjaGFpbklkIjoxMzI5fQ==
```

### Decoded JWT

```json theme={null}
{
  "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:sigil: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

```json theme={null}
{
  "wallet": "0xE23c9A70BC749EBddd8c78A864Fd911D04E9e992",
  "nonce": 42,
  "statement": "Purchase authorization for partner X",
  "scopes": ["sr:us:pint:sigil: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
}
```

<Note>
  `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).
</Note>

### Verification Steps

Enhanced verification includes all Standard steps plus cryptographic verification of the PINT signature.

**TypeScript (jose + viem):**

```typescript theme={null}
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):**

```python theme={null}
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
```

<Warning>
  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](/merchant/verify-pint-signature) for the complete EIP-1271 flow.
</Warning>

See [Verifying PINT Signatures](/merchant/verify-pint-signature) 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](/identity/scopes). Enhanced tier is reserved for the `spend` scopes that move value; every other scope in the catalog is Standard.

| Scope                                                              | Tier         |
| ------------------------------------------------------------------ | ------------ |
| `sr:us:pint:spend:execute`                                         | **Enhanced** |
| `sr:us:pint:spend:visa_checkout`                                   | **Enhanced** |
| `sr:us:pint:spend:card`                                            | **Enhanced** |
| Every other scope in the [catalog](/identity/scopes#scope-catalog) | Standard     |

If **any** scope in the PINT maps to Enhanced, the entire JWT is issued at Enhanced tier.

<Warning>
  Do not hard-code a scope-to-tier map on the verifier side. Read the `verification_tier` claim on the JWT — new scopes are added to the catalog over time, and a stale local map can under-verify a value-moving mandate.
</Warning>

### What This Means for You

The tier is determined server-side during token exchange — you don't compute it yourself. As a verifier you receive 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 Stamped Mandate 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.

<Info>
  The scopes available to you depend on your partner agreement. Contact your account manager to discuss scope access.
</Info>

***

## Choosing Your Implementation

### Which Tier Applies to You

| What You Verify For                                                      | Tier     | What You Check       |
| ------------------------------------------------------------------------ | -------- | -------------------- |
| Identity verification (proof of personhood, age, KYC)                    | Standard | JWT only             |
| Personalisation or marketing data access                                 | Standard | JWT only             |
| Payment processing or spend authorisation                                | Enhanced | JWT + PINT signature |
| Compliance-critical actions requiring cryptographic proof of user intent | Enhanced | JWT + PINT signature |

<Warning>
  If you process payments or authorise spending on behalf of users, you **must** implement Enhanced tier verification. Standard-tier JWT verification alone is not sufficient for spend authorisation.
</Warning>

### Implementation Guides

<ol className="counter-style">
  <li>**[Verify a JWT](/merchant/verify-jwt)** — Standard tier. Validate JWT signature, audience, and expiry against the SIS JWKS endpoint.</li>
  <li>**[Verify PINT signatures](/merchant/verify-pint-signature)** — Enhanced tier. Verify the original EIP-712 PINT signature for independent cryptographic proof.</li>
</ol>
