Skip to main content

“Token invalid” or Verification Fails

If verifyGrantToken() or tokens.verify() returns invalid results, work through this checklist:
1

Check token expiry

Decode the JWT and inspect the exp claim. If exp is in the past, the token has expired.
# Decode a JWT (base64 payload)
echo 'YOUR_JWT' | cut -d. -f2 | base64 -d 2>/dev/null | python3 -m json.tool
2

Check revocation status

Call POST /v1/tokens/verify with the token. If valid: false, the token or its grant has been revoked.
3

Verify the JWKS URI

Make sure you’re using the correct JWKS endpoint for your environment:
  • Production: https://grantex-auth-dd4mtrt2gq-uc.a.run.app/.well-known/jwks.json
  • Self-hosted: https://your-domain/.well-known/jwks.json
4

Check clock skew

JWT verification compares exp and iat against your server’s clock. If your server clock is more than a few seconds off, tokens may fail verification. Use NTP to keep clocks synchronized.
5

Check audience mismatch

If you set audience in VerifyGrantTokenOptions, the JWT’s aud claim must match. Remove the audience check or ensure the authorization request included the same audience.
If the authorization request stays in pending status:
  1. Check the consent URL — ensure the user was redirected to consentUrl from the authorization response
  2. Check the auth request status — query GET /v1/consent/:id to see the current state
  3. Check expiry — auth requests expire after the expiresIn window (default 24h). If expired, create a new authorization request
  4. Check the redirect URI — if provided, the code is delivered via redirect. Ensure your callback handler is working
  5. Sandbox mode — in sandbox mode, consent is auto-approved and a code is returned directly in the response

Rate Limited (429)

When you receive a 429 Too Many Requests response:
{
  "message": "Rate limit exceeded, retry in 42 seconds",
  "code": "BAD_REQUEST",
  "requestId": "a1b2c3d4-e5f6-..."
}
What to do:
  1. Read the Retry-After header for the minimum wait time
  2. Implement exponential backoff with jitter (see Rate Limits guide)
  3. Reduce request frequency:
    • Use offline verification instead of POST /v1/tokens/verify — the JWKS endpoint is exempt from rate limits
    • Use webhooks instead of polling for grant status changes
    • Cache tokens — don’t re-exchange or re-verify tokens unnecessarily
Rate limits by endpoint:
EndpointLimit
Global (all endpoints)100 req/min
POST /v1/authorize10 req/min
POST /v1/token20 req/min
POST /v1/token/refresh20 req/min
GET /.well-known/jwks.jsonExempt

Plan Limit Exceeded (402)

A 402 Payment Required response means you’ve hit a resource limit for your current plan:
{
  "message": "Agent limit reached (free plan: 3)",
  "code": "PLAN_LIMIT_EXCEEDED",
  "requestId": "..."
}
Options:
  • Upgrade your plan — use the billing portal to switch to Pro or Enterprise
  • Clean up unused resources — delete inactive agents, revoke expired grants
  • Check your current usage via the developer dashboard

Delegation Failed

If POST /v1/grants/delegate returns an error:
ErrorCauseFix
”Scopes must be a subset”Sub-agent scopes exceed parent grant scopesNarrow the requested scopes
”Delegation depth exceeded”Too many levels of delegationReduce the chain or increase the limit
”Parent grant expired”The parent grant token has expiredObtain a new grant token first
”Parent grant revoked”The parent grant was revokedRe-authorize the parent agent
”Invalid parent grant token”JWT signature verification failedCheck the token is well-formed

Refresh Token Rejected

If POST /v1/token/refresh returns 400:
Error messageCauseFix
”Refresh token already used”Token was already used (single-use rotation)Use the new refresh token from the previous refresh response
”Refresh token expired”Token’s 30-day TTL has elapsedRe-authorize to get a fresh token pair
”Grant has been revoked”The underlying grant was revokedRe-authorize the agent
”Agent mismatch”agentId doesn’t match the grant’s agentUse the correct agent ID
”Invalid refresh token”Token ID not foundCheck you’re passing the correct refresh token value

Error Codes Reference

All Grantex API errors include a code field. SDKs expose this as error.code:
import { GrantexApiError } from '@grantex/sdk';

try {
  await grantex.tokens.exchange({ code, agentId });
} catch (err) {
  if (err instanceof GrantexApiError) {
    console.log(err.code);       // 'BAD_REQUEST'
    console.log(err.statusCode); // 400
    console.log(err.message);    // 'Invalid code'
    console.log(err.requestId);  // 'a1b2c3d4-...'
  }
}
CodeHTTP StatusDescription
BAD_REQUEST400Invalid input, missing fields, or validation failure
UNAUTHORIZED401Invalid or missing API key
FORBIDDEN403Valid API key but insufficient permissions
NOT_FOUND404Resource does not exist
CONFLICT409Resource already exists (e.g., duplicate agent name)
GONE410Resource was deleted
VALIDATION_ERROR422Schema validation failure
PLAN_LIMIT_EXCEEDED402Resource count exceeds plan threshold
POLICY_DENIED403An access policy blocked the operation
SSO_ERROR400SSO/OIDC configuration or callback error
SERVICE_UNAVAILABLE503Service temporarily unavailable
INTERNAL_ERROR500Unexpected server error

Inspecting JWT Claims Locally

You can decode a Grantex grant token without verification to inspect its claims:
# Using jq (if installed)
echo 'YOUR_JWT' | cut -d. -f2 | base64 -d 2>/dev/null | jq .

# Using Python
python3 -c "import sys, json, base64; print(json.dumps(json.loads(base64.urlsafe_b64decode(sys.argv[1].split('.')[1] + '==')), indent=2))" 'YOUR_JWT'
Grant token claims:
ClaimDescription
issIssuer (Grantex auth service URL)
subPrincipal ID (the user who authorized)
agtAgent DID
devDeveloper ID
scpScopes array
grntGrant record ID
jtiToken ID (unique per token)
iatIssued at (Unix timestamp)
expExpires at (Unix timestamp)
audAudience (optional)
parentAgtParent agent DID (delegation only)
parentGrntParent grant ID (delegation only)
delegationDepthDelegation depth (delegation only)

Getting Help

If none of the above resolves your issue:
  • Check the GitHub Issues for known problems
  • Open a new issue with your error message, requestId, and reproduction steps
  • For Enterprise support, use the contact form