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

# Check a Stamped Mandate

> Before you act for an agent, check the Stamped Mandate token it sends your site, offline on your own server.

export const contacts = {
  verifierAccess: "hello@sumvin.com"
};

export const waitlists = {
  earlyAccess: "",
  payments: "",
  openBanking: "",
  developers: "",
  verifiers: ""
};

<Note>
  **Private preview:** this is rolling out and may not be available on your account yet.{waitlists.earlyAccess ? <> <a href={waitlists.earlyAccess}>Request early access →</a></> : null}
</Note>

An agent asks your site to do something and shows you a [Stamped Mandate](/concepts/stamped-mandates) as proof that a verified person allowed it. The agent sends it as a token addressed to your site's hostname. You check it on your own server, offline, against Sumvin's published keys: the token never leaves your server, and you need no API key.

A passing check tells you the token is genuine, addressed to you and unexpired, and that the person approved exactly its [scopes](/concepts/mandates/scopes). A token addressed to a hostname carries identity attestations only: proof of personhood, verification status, over 18. It never tells you the person's name or contact details. See [What a verifier checks](/concepts/mandates/verification).

These checks use plain HTTP and a standard JWT library. The SDK has no verification function yet.

## What the agent sends

The agent sends the token in the `x-sumvin-pint-token` request header. The value is the bare token, with no `Bearer` prefix:

```http theme={null}
x-sumvin-pint-token: <token>
```

The token is addressed to one hostname, such as `reservations.example.com`. Accept only a token addressed to your own.

## Before you start

* A JWT library. This page uses [`jose`](https://github.com/panva/jose): `bun add jose` or `npm install jose`.

<Steps>
  <Step title="Read the token from the request">
    ```ts theme={null}
    const token = request.headers.get("x-sumvin-pint-token");
    if (!token) throw new Error("No Stamped Mandate presented");
    ```

    A request without the header carries no mandate. Treat it as any other unauthenticated request.
  </Step>

  <Step title="Check it offline">
    Verify the token against Sumvin's published key set, with your hostname as the audience. The keys rotate, so let your library fetch and cache them rather than copying them.

    ```ts theme={null}
    import { createRemoteJWKSet, jwtVerify } from "jose";

    const sumvinKeys = createRemoteJWKSet(
      new URL("https://jwks.sumvin.com/.well-known/jwks.json"),
    );

    await jwtVerify(token, sumvinKeys, {
      algorithms: ["ES256"],
      issuer: "https://sis.sumvin.com",
      audience: "reservations.example.com",
    });
    ```

    Pin the algorithm to `ES256`, the only one Sumvin signs with, so a token signed any other way is refused rather than trusted.

    `jwtVerify` throws if the token was altered, wasn't issued by Sumvin, isn't addressed to you, or has expired. Treat any throw as a refusal.

    <Note>
      **Coming soon:** an online check for tokens addressed to a website. Until then, the offline check is the whole check: it tells you the token is genuine, addressed to you and unexpired, but not whether the person has revoked it since.
    </Note>
  </Step>

  <Step title="Act only within its scopes">
    A token that passes its check still permits only what its scopes say. Compare them with what the agent is asking for, and act on nothing more.
  </Step>
</Steps>

<Info>
  Any refusal means don't act.
</Info>

## Services that already hold the signed request

This section is for a service that already holds the signed request a person approved, for example because the person approved it for the service's own app. It is not how an agent presents a mandate to a site: an agent sends the hostname token above. A service in this position can exchange the signed request for a token addressed to its organisation, then check online whether the mandate still stands.

It needs an API key and your Organisation ID. See [Get a key](/sdk/verify/api-key).

<Note>
  **Developer dashboard access is by invitation during the preview.** You can't sign up yourself yet.{waitlists.verifiers ? <> <a href={waitlists.verifiers}>Join the verifiers waitlist →</a></> : null}{contacts.verifierAccess ? <> For quicker access, email <a href={`mailto:${contacts.verifierAccess}`}>{contacts.verifierAccess}</a>.</> : null}{!waitlists.verifiers && !contacts.verifierAccess ? " A verifiers waitlist opens shortly." : null}
</Note>

<Steps>
  <Step title="Exchange the signed request">
    Send the signed request the person approved, unchanged, with `audience` set to your Organisation ID. You get back a token addressed only to your organisation.

    ```ts theme={null}
    const SIS = "https://sis.sumvin.com";

    const res = await fetch(`${SIS}/v0/sis/token/pint`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.SUMVIN_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ ...signedRequest, audience: process.env.SUMVIN_ORG_ID }),
    });
    if (!res.ok) throw new Error(`Exchange refused: ${res.status}`);

    const { sig: token, id: mandateId, scopes, expires_at } = await res.json();
    ```

    `201` means a new token was issued. `208` means this request was already exchanged for your organisation, and you get the same token back. Keep `mandateId`: you need it for the online check.

    During the preview, some mandates can't be exchanged yet; the exchange refuses them with `PINT-403-009`. See [Checks and errors](/sdk/verify/checks#errors).
  </Step>

  <Step title="Check the token offline">
    Run the same offline check as above, with your Organisation ID as the audience:

    ```ts theme={null}
    await jwtVerify(token, sumvinKeys, {
      algorithms: ["ES256"],
      issuer: "https://sis.sumvin.com",
      audience: process.env.SUMVIN_ORG_ID,
    });
    ```
  </Step>

  <Step title="Check it still stands">
    Ask Sumvin whether the mandate is still valid:

    ```ts theme={null}
    const statusRes = await fetch(
      `${SIS}/v0/sis/pint/${encodeURIComponent(mandateId)}/status`,
      { headers: { Authorization: `Bearer ${process.env.SUMVIN_API_KEY}` } },
    );
    if (!statusRes.ok) throw new Error(`Online check refused: ${statusRes.status}`);

    const status = await statusRes.json();
    if (!status.valid) {
      throw new Error(`Mandate no longer stands: ${status.reason}`);
    }
    ```

    `valid` is `true` when the mandate is neither revoked nor expired. Otherwise `reason` says which. See [the online check](/sdk/verify/checks#online-check).
  </Step>

  <Step title="Act only within its scopes">
    Compare the `scopes` with what is being asked for, and read any spending ceiling with [`readScopeCeiling`](/sdk/verify/reading-scopes).
  </Step>
</Steps>

<Info>
  Any refusal, from any step, means don't act.
</Info>

## Next

<Columns cols={2}>
  <Card title="Checks and errors" icon="list-checks" href="/sdk/verify/checks">
    Every response, error code and limit of the two checks.
  </Card>

  <Card title="Read a mandate's scopes" icon="scale" href="/sdk/verify/reading-scopes">
    Turn scopes into amounts you can compare.
  </Card>
</Columns>
