calcom/cal.diy · error · UnauthorizedException
CustomThrottlerGuard - Invalid API Key
Error message
CustomThrottlerGuard - Invalid API Key
What it means
Thrown by CustomThrottlerGuard.getRateLimitsForApiKeyTracker when the tracker has an 'api_key_' prefix (meaning the Authorization header started with the API key prefix, default 'cal_') but the SHA-256 hash of the stripped key does not match any record in the apiKey table. The guard recognizes the key format but cannot find it in the database to load custom rate limits. This throws UnauthorizedException (HTTP 401) rather than ThrottlerException because it is an authentication failure, not a rate-limit violation.
Source
Thrown at apps/api/v2/src/lib/throttler-guard.ts:160
const cacheKey = `rate_limit:${tracker}`;
const cachedRateLimits = await this.storageService.redis.get(cacheKey);
if (cachedRateLimits) {
/*this.logger.verbose(`Tracker "${tracker}" rate limits retrieved from redis cache:
${cachedRateLimits}
`);*/
return rateLimitsSchema.parse(JSON.parse(cachedRateLimits));
}
const apiKey = tracker.replace("api_key_", "");
let rateLimits: RateLimitType[];
const apiKeyRecord = await this.dbRead.prisma.apiKey.findUnique({
where: { hashedKey: apiKey },
select: { id: true },
});
if (!apiKeyRecord) {
throw new UnauthorizedException("CustomThrottlerGuard - Invalid API Key");
}
rateLimits = await this.dbRead.prisma.rateLimit.findMany({
where: { apiKeyId: apiKeyRecord.id },
select: { name: true, limit: true, ttl: true, blockDuration: true },
});
if (!rateLimits || rateLimits.length === 0) {
rateLimits = [this.getDefaultRateLimit(tracker)];
/*this.logger.verbose(`Tracker "${tracker}" rate limits not found in database. Using default rate limits:
${JSON.stringify(rateLimits, null, 2)}`);*/
}
await this.storageService.redis.set(cacheKey, JSON.stringify(rateLimits), "EX", 3600);
return rateLimits;
}
View on GitHub (pinned to 176037d0af)
Solutions
- Verify the API key is complete, correctly formatted, and starts with the expected prefix (check API_KEY_PREFIX env on the server).
- Confirm the key still exists in the database and has not been deleted or expired.
- If the key was recently refreshed via the refresh endpoint, update the client to use the new key returned in the refresh response.
- Check for database replication lag if the key was just created: wait a few seconds and retry.
Example fix
// before: hardcoded key that may be stale
const apiKey = 'cal_stale_key_from_config';
// after: validate key existence and handle 401 gracefully
const callApi = async (apiKey: string) => {
const res = await fetch('/v2/event-types', {
headers: { Authorization: `Bearer ${apiKey}` }
});
if (res.status === 401) {
throw new Error('API key invalid or expired. Refresh the key and retry.');
}
return res.json();
}; Defensive patterns
Strategy: validation
Validate before calling
// Validate API key format and prefix before sending
const API_KEY_PREFIX = 'cal_'; // match server's API_KEY_PREFIX env
const isValidApiKeyFormat = (key: string): boolean => {
if (!key || typeof key !== 'string') return false;
if (!key.startsWith(API_KEY_PREFIX)) return false;
const stripped = key.slice(API_KEY_PREFIX.length);
return stripped.length > 10; // minimum reasonable length
};
if (!isValidApiKeyFormat(apiKey)) {
throw new Error(`Invalid API key format. Expected prefix: ${API_KEY_PREFIX}`);
} Type guard
const isValidApiKey = (key: unknown): key is string =>
typeof key === 'string' && key.startsWith('cal_') && key.length > 10; Try / catch
// Handle 401 from throttler by refreshing the key
try {
await apiClient.get('/v2/event-types');
} catch (err: any) {
if (err?.response?.status === 401 && err?.response?.data?.message?.includes('Invalid API Key')) {
// Key is stale or invalid; generate a new one
const newKey = await generateNewApiKey();
apiClient.setHeader('Authorization', `Bearer ${newKey}`);
// Retry once with the new key
return apiClient.get('/v2/event-types');
}
throw err;
} Prevention
- Store API keys in a secure secret manager (not in code or config files checked into git).
- After refreshing a key, immediately update all clients to use the new key — the old key is deleted instantly.
- Verify the API_KEY_PREFIX matches between client and server environments before deployment.
- Log key creation and deletion timestamps to diagnose stale-key issues.
When it happens
Trigger: A request sends 'Authorization: Bearer cal_<key>' where the key was deleted from the database, expired, or belongs to a different environment. The API_KEY_PREFIX env differs between the client and server (e.g. client uses 'cal_' but server expects a custom prefix), causing stripApiKey to produce the wrong hash. The key was rotated/refreshed (old key is deleted) and the client still uses the stale key.
Common situations: Stale API key after a refresh operation (refreshApiKey deletes the old key at line 76). Mismatched API_KEY_PREFIX between environments (dev vs staging vs production). Key copied with extra whitespace or truncation. Database read replica lag causing a just-created key to be temporarily invisible.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- ApiKeysService - This endpoint can only be accessed using an
- ApiKeysService - No API key provided
- ApiKeysService - provided api key is not valid.
- ApiKeysService -Cannot set both apiKeyDaysValid and apiKeyNe
- This event type requires authentication. Please provide vali
AI-assisted analysis of calcom/cal.diy@176037d0af (2026-08-12).
Data as JSON: /api/errors/879127abfe9c7a99.
Report an issue: GitHub.