> ## 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>
  From the releases after `grantex` 0.6.0 and `@grantex/sdk` 0.7.0, the Python
  and TypeScript SDKs can bound the JWKS fetch. With `boundedJwksFetch: true` /
  `bounded_jwks_fetch=True`, the key set must be an HTTP 200 served as
  `application/json` or `application/jwk-set+json`, at most 64 KiB and 128 keys,
  within 5 seconds, or the token is refused, and an `issuerDid` / `issuer_did`
  must be a `did:web` identifier for a public, fully qualified domain. The option
  is off by default in these releases; a later major release turns it on by
  default, with `false` / `False` as the opt-out. See
  [JWKS fetch limits](/sdks/typescript/offline-verification#jwks-fetch-limits).
</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

Before a grant expires, use the refresh token to rotate credentials and obtain a fresh JWT for the same active grant:

<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
  // newToken.expiresAt — same underlying grant expiration
  ```

  ```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
  # new_token.expires_at — same underlying grant expiration
  ```
</CodeGroup>

Refresh tokens are single-use. Each refresh returns a new refresh token, forming a rotation chain. Refresh does not extend the underlying grant's `expiresAt`; after the grant expires, the caller must re-authorize. If a refresh response is lost after the server commits, retry the same previous refresh token with the same persisted idempotency key. Grantex can return the already-rotated refresh token during a five-minute (300-second) window while the grant remains active. The cached access token is encrypted at rest with `VAULT_ENCRYPTION_KEY` and erased at expiry. After that window, or once the rotated child token has been used, reuse is rejected.

## Ownership

Grantex is owned by Orchestrum Technologies LLP. Inventor and owner: Sanjeev Kumar. Ownership contact: [sanjeev@orchestrum.in](mailto:sanjeev@orchestrum.in) or [mishra.sanjeev@gmail.com](mailto:mishra.sanjeev@gmail.com).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.