“Token invalid” or Verification Fails
IfverifyGrantToken() 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.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://api.grantex.dev/.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.”Authorization Stuck” — Consent Flow Not Completing
If the authorization request stays inpending status:
- Check the consent URL — ensure the user was redirected to
consentUrlfrom the authorization response - Check the auth request status — query
GET /v1/consent/:idto see the current state - Check expiry — auth requests expire after the
expiresInwindow (default 24h). If expired, create a new authorization request - Check the redirect URI — if provided, the code is delivered via redirect. Ensure your callback handler is working
- Sandbox mode — in sandbox mode, consent is auto-approved and a code is returned directly in the response
Rate Limited (429)
When you receive a429 Too Many Requests response:
- Read the
Retry-Afterheader for the minimum wait time - Implement exponential backoff with jitter (see Rate Limits guide)
- Reduce request frequency:
- Use local JWKS-based verification instead of
POST /v1/tokens/verifywhen a revocation lookup is not required. The standalone helpers still fetch JWKS per call, but that 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
- Use local JWKS-based verification instead of
A route-specific Fastify limit replaces the 5,000 req/min Fastify default on that route. The Redis-backed standard developer plan remains an additional policy wherever standard API-key authentication applies.
Standard developer API-key plan exhaustion returns
429 with RATE_LIMIT_EXCEEDED. If the Redis-backed plan counter is unavailable, the request fails closed with 503 and RATE_LIMIT_UNAVAILABLE. Revoking and the emergency stop do not consume the plan budget and are not refused when the counter is unavailable; the revocation feed and status reads have a budget of their own and still answer 503 then. See Rate Limits. Commerce, the SCIM Bearer data-plane routes under /scim/v2/*, admin, and other custom-auth routes do not consume this plan bucket; the standard API-key-authenticated /v1/scim/tokens management routes do.
Plan Limit Exceeded (402)
A402 Payment Required response means you’ve hit a resource limit for your current plan:
- 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
Issuance Frozen (403)
403 with ISSUANCE_FROZEN means an emergency stop placed a lockout over
something this request would issue under. That can be your whole developer
account, the agent, the principal, or a grant above the one you are
refreshing or delegating from.
403 with access_denied. Nothing is
issued until the lockout is lifted. An authorization code or refresh token
refused this way is not used up, so it works again afterwards.
GET /v1/emergency-stops lists the lockouts in force under freezes, 50 to a
page; freezesTotal says how many there are, and ?page=2 fetches the next.
POST /v1/emergency-stop/unfreeze lifts one you placed; a lockout placed by
the platform operator can only be lifted by the operator.
503 with FREEZE_STATE_UNAVAILABLE (temporarily_unavailable on the OAuth
endpoints) means the service could not read whether a lockout applies, and
refused rather than risk issuing under one. Retry shortly.
Delegation Failed
IfPOST /v1/grants/delegate returns an error:
Refresh Token Rejected
IfPOST /v1/token/refresh returns 400:
If a refresh request times out or the connection drops, retry the same refresh token immediately. Grantex can recover the already-rotated refresh token for five minutes while the grant remains active, without extending the rotation chain or grant lifetime.
Error Codes Reference
All Grantex API errors include acode field. SDKs expose this as error.code:
Inspecting JWT Claims Locally
You can decode a Grantex grant token without verification to inspect its claims: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