koala73/worldmonitor · error · ApiError
Invalid API key
Error message
Invalid API key
What it means
listWebhooks requires a valid user API key. After gateway validation, the handler revalidates the credential with validateUserApiKey; if it returns null (credential present but not recognized/valid), it throws HTTP 401 'Invalid API key'. A later check also produces the same message when the key is required but never marked valid.
Solutions
- Regenerate the API key in the dashboard and update the client/env configuration
- Verify the exact header name and that the value has no surrounding whitespace or quotes
- Confirm you are using a user API key (not a project/gateway key) for the shipping v2 webhook endpoints
- Check the request environment actually loads the key (e.g. the env var is set in the deployed target, not just locally)
Example fix
// before
const client = createClient({ apiKey: process.env.OLD_KEY });
// after
const key = process.env.WORLDMONITOR_API_KEY?.trim();
if (!key) throw new Error('WORLDMONITOR_API_KEY not set');
const client = createClient({ apiKey: key }); Defensive patterns
Strategy: validation
Validate before calling
const key = process.env.WORLDMONITOR_API_KEY;
if (!key || key.trim().length === 0) throw new Error('WORLDMONITOR_API_KEY is not set'); Type guard
function hasApiKey(cfg: { apiKey?: string }): cfg is { apiKey: string } {
return typeof cfg.apiKey === 'string' && cfg.apiKey.trim().length > 0;
} Try / catch
try {
return await client.listWebhooks();
} catch (e) {
if (e.status === 401) {
throw new Error('API key rejected — regenerate the key and update secrets');
}
throw e;
} Prevention
- Rotate and update keys in one place (secret manager), never hardcode
- Trim keys and avoid shell quoting artifacts
- Use the key type (user vs gateway) appropriate for the endpoint and environment
When it happens
Trigger: Calling list-webhooks with an Authorization/X-API-Key header containing a revoked, deleted, mistyped, or malformed user API key that fails validateUserApiKey (returns null).
Common situations: Key was rotated or revoked server-side; the environment variable holding the key contains a stale or truncated value; sending a gateway-level key where a user key is required; whitespace or quoting mistakes when copying the key.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/954aafebb5d3facb.
Report an issue: GitHub.
Appendix: source
Thrown at server/worldmonitor/shipping/v2/list-webhooks.ts:41
_req: ListWebhooksRequest,
): Promise<ListWebhooksResponse> {
// Without forceKey, Clerk-authenticated pro callers reach this handler with
// no API key, callerFingerprint() returns the 'anon' fallback, and the
// ownerTag !== ownerHash defense-in-depth below collapses because both
// sides equal 'anon' — exposing every 'anon'-bucket tenant's webhooks to
// every Clerk-session holder. See registerWebhook for full rationale.
const apiKeyResult = (await validateApiKey(ctx.request, { forceKey: true })) as {
valid: boolean; required: boolean; error?: string; credential?: string;
};
if (apiKeyResult.error === USER_API_KEY_GATEWAY_VALIDATION_ERROR) {
const credential = getHeaderApiKey(ctx.request) as string;
let userKey;
try {
userKey = credential ? await validateUserApiKey(credential) : null;
} catch {
throw new ApiError(503, 'Service temporarily unavailable', '');
}
if (!userKey) throw new ApiError(401, 'Invalid API key', '');
// Revalidate the credential rather than trusting a caller-supplied user ID.
apiKeyResult.valid = true;
apiKeyResult.credential = credential;
}
if (apiKeyResult.required && !apiKeyResult.valid) {
throw new ApiError(401, apiKeyResult.error ?? 'API key required', '');
}
await requirePremiumRpcAccess(ctx.request, ApiError, 'PRO subscription required');
const ownerHash = await callerFingerprint(ctx.request, apiKeyResult.credential);
const records = await readOwnerWebhooks(ownerHash);
const webhooks: WebhookSummary[] = [];
for (const value of records) {
try {
const record = JSON.parse(value) as WebhookRecord;
if (record.ownerTag !== ownerHash) continue;
webhooks.push({View on GitHub (pinned to 7d06c8633d)