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

# Token Verification Strategy

> Choose between offline, online, and hybrid token verification strategies.

Grantex supports two verification approaches, each with different trade-offs. This guide helps you pick the right strategy — or combine them.

## Offline Verification

This mode validates the JWT signature and claims locally after obtaining public
keys from the JWKS endpoint. It avoids the online token-verification/revocation
endpoint. The current standalone TypeScript, Python, and Go helpers fetch or
resolve the remote JWKS for each invocation, so do not treat them as
network-free or assume zero-latency hot-path behavior.

**How it works:**

1. Resolve the public keys from `/.well-known/jwks.json`
2. Verify the RS256 signature against the public key
3. Check `exp`, `iss`, and optionally `aud` and required scopes
4. Return the decoded claims

**Best for:** High-velocity endpoints, latency-sensitive paths, reducing API calls.

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { verifyGrantToken } from '@grantex/sdk';

  const grant = await verifyGrantToken(grantToken, {
    jwksUri: 'https://api.grantex.dev/.well-known/jwks.json',
    requiredScopes: ['calendar:read'],
    audience: 'my-service',  // optional
  });

  console.log(grant.principalId);  // 'user_abc123'
  console.log(grant.scopes);       // ['calendar:read']
  console.log(grant.grantId);      // 'grnt_01HXYZ...'
  ```

  ```python Python theme={null}
  from grantex import verify_grant_token, VerifyGrantTokenOptions

  grant = verify_grant_token(
      grant_token,
      VerifyGrantTokenOptions(
          jwks_uri="https://api.grantex.dev/.well-known/jwks.json",
          required_scopes=["calendar:read"],
          audience="my-service",  # optional
      ),
  )

  print(grant.principal_id)  # 'user_abc123'
  print(grant.scopes)        # ('calendar:read',)
  print(grant.grant_id)      # 'grnt_01HXYZ...'
  ```
</CodeGroup>

**Trade-off:** Offline verification does not check the revocation list. A revoked token will still pass offline verification until it expires.

<Note>
  The JWKS endpoint (`/.well-known/jwks.json`) is exempt from rate limits. You can fetch it as often as needed.
</Note>

<Note>
  Hosted grant tokens have `iss: https://grantex.dev`, while the stable JWKS alias
  is `https://api.grantex.dev/.well-known/jwks.json`. Current SDKs recognize this
  alias automatically. For a self-hosted issuer, configure the expected issuer
  explicitly or use the SDK's custom-JWKS issuer derivation.
</Note>

## Online Verification

Online verification calls `POST /v1/tokens/verify`, which checks the signature, expiry, **and** real-time revocation status on the server.

**Best for:** High-stakes operations (payments, data deletion, privilege escalation).

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { Grantex } from '@grantex/sdk';

  const grantex = new Grantex({ apiKey: process.env.GRANTEX_API_KEY });
  const result = await grantex.tokens.verify(grantToken);

  if (!result.valid) {
    throw new Error('Token is revoked or expired');
  }

  console.log(result.scopes);    // ['payments:initiate']
  console.log(result.principal); // 'user_abc123'
  ```

  ```python Python theme={null}
  from grantex import Grantex

  with Grantex(api_key="gx_live_...") as client:
      result = client.tokens.verify(grant_token)

      if not result.valid:
          raise ValueError("Token is revoked or expired")

      print(result.scopes)    # ('payments:initiate',)
      print(result.principal) # 'user_abc123'
  ```
</CodeGroup>

**Trade-off:** Adds network latency and counts against the applicable [rate limits](/guides/rate-limits), including the active Fastify per-IP policy (the 5,000 requests/minute default on this route) and, for standard developer API-key calls, the caller's additional plan budget.

## Hybrid Approach

The hybrid strategy uses local signature-and-claim verification first and calls
the online endpoint for sensitive operations. With the standalone helpers, the
first step can still include a JWKS network request.

```typescript theme={null}
import { verifyGrantToken, Grantex } from '@grantex/sdk';

const SENSITIVE_SCOPES = ['payments:initiate', 'files:delete', 'admin:write'];

async function verifyToken(grantToken: string): Promise<void> {
  // Step 1: Verify signature and claims locally (the helper fetches JWKS)
  const grant = await verifyGrantToken(grantToken, {
    jwksUri: 'https://api.grantex.dev/.well-known/jwks.json',
  });

  // Step 2: If the token has sensitive scopes, also verify online
  const hasSensitiveScope = grant.scopes.some(s => SENSITIVE_SCOPES.includes(s));
  if (hasSensitiveScope) {
    const grantex = new Grantex({ apiKey: process.env.GRANTEX_API_KEY });
    const result = await grantex.tokens.verify(grantToken);
    if (!result.valid) {
      throw new Error('Token revoked — blocking sensitive operation');
    }
  }
}
```

## Caching Verification Results

Per SPEC §7.4, you may cache online verification results for up to **5 minutes**. This reduces API calls while keeping revocation lag acceptable.

```typescript theme={null}
const verifyCache = new Map<string, { result: VerifyTokenResponse; cachedAt: number }>();
const MAX_CACHE_AGE_MS = 5 * 60 * 1000; // 5 minutes

async function cachedVerify(grantex: Grantex, token: string) {
  const cached = verifyCache.get(token);
  if (cached && Date.now() - cached.cachedAt < MAX_CACHE_AGE_MS) {
    return cached.result;
  }

  const result = await grantex.tokens.verify(token);
  verifyCache.set(token, { result, cachedAt: Date.now() });
  return result;
}
```

<Warning>
  Do not cache for longer than 5 minutes. Revoked tokens must be detected within a reasonable window.
</Warning>

## Choosing a Strategy

| Criteria              | Offline                                                   | Online                   | Hybrid                  |
| --------------------- | --------------------------------------------------------- | ------------------------ | ----------------------- |
| **Latency**           | Local cryptography plus JWKS retrieval when needed        | Network round-trip       | Varies                  |
| **Revocation check**  | No                                                        | Yes                      | Conditional             |
| **Rate limit impact** | None                                                      | 1 req per verify         | Reduced                 |
| **Best for**          | Signature-only decisions where revocation is not required | Payment, deletion, admin | Mixed-risk applications |

**Recommendation:** Choose based on revocation requirements. Use local
signature-and-claim verification for lower-risk decisions and the online endpoint
for writes or sensitive scopes. For high-volume hot paths, do not assume the
standalone helper provides a persistent JWKS cache; design key caching and
rotation refresh explicitly.

## Token Refresh

When a grant token expires, use the refresh token to obtain a new one without re-prompting the user:

<CodeGroup>
  ```typescript TypeScript theme={null}
  const newToken = await grantex.tokens.refresh({
    refreshToken: previousResponse.refreshToken,
    agentId: agent.id,
  });

  // newToken.grantToken — new JWT
  // newToken.refreshToken — new refresh token (old one is invalidated)
  // newToken.grantId — same grant, new token
  ```

  ```python Python theme={null}
  from grantex import RefreshTokenParams

  new_token = client.tokens.refresh(RefreshTokenParams(
      refresh_token=previous_response.refresh_token,
      agent_id="agt_abc123",
  ))

  # new_token.grant_token — new JWT
  # new_token.refresh_token — new refresh token (old one is invalidated)
  # new_token.grant_id — same grant, new token
  ```
</CodeGroup>

Refresh tokens are single-use. Each refresh returns a new refresh token, forming a rotation chain. If a refresh token is used twice, the second attempt is rejected — this detects token theft.
