Grantex supports two verification approaches, each with different trade-offs. This guide helps you pick the right strategy — or combine them.
Offline Verification
Offline verification validates the JWT signature locally using the JWKS endpoint, without calling the Grantex API. This is the fastest option and the recommended default.
How it works:
- Fetch the public keys from
/.well-known/jwks.json (cached automatically)
- Verify the RS256 signature against the public key
- Check
exp, iss, and optionally aud and required scopes
- Return the decoded claims
Best for: High-velocity endpoints, latency-sensitive paths, reducing API calls.
import { verifyGrantToken } from '@grantex/sdk';
const grant = await verifyGrantToken(grantToken, {
jwksUri: 'https://grantex-auth-dd4mtrt2gq-uc.a.run.app/.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...'
Trade-off: Offline verification does not check the revocation list. A revoked token will still pass offline verification until it expires.
The JWKS endpoint (/.well-known/jwks.json) is exempt from rate limits. You can fetch it as often as needed.
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).
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'
Trade-off: Adds network latency (~50-100ms) and counts against your rate limit (100 req/min global).
Hybrid Approach
The hybrid strategy uses offline verification as the fast path and online verification for sensitive operations. This gives you the best of both worlds.
import { verifyGrantToken, Grantex } from '@grantex/sdk';
const SENSITIVE_SCOPES = ['payments:initiate', 'files:delete', 'admin:write'];
async function verifyToken(grantToken: string): Promise<void> {
// Step 1: Always verify the signature offline (fast, no network)
const grant = await verifyGrantToken(grantToken, {
jwksUri: 'https://grantex-auth-dd4mtrt2gq-uc.a.run.app/.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.
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;
}
Do not cache for longer than 5 minutes. Revoked tokens must be detected within a reasonable window.
Choosing a Strategy
| Criteria | Offline | Online | Hybrid |
|---|
| Latency | ~0ms (after JWKS fetch) | ~50-100ms | Varies |
| Revocation check | No | Yes | Conditional |
| Rate limit impact | None | 1 req per verify | Reduced |
| Best for | Read-only endpoints, high-velocity | Payment, deletion, admin | Most production apps |
Recommendation: Start with the hybrid approach. Use offline verification for reads and online for writes or sensitive scopes.
Token Refresh
When a grant token expires, use the refresh token to obtain a new one without re-prompting the user:
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
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.