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

  1. Regenerate the API key in the dashboard and update the client/env configuration
  2. Verify the exact header name and that the value has no surrounding whitespace or quotes
  3. Confirm you are using a user API key (not a project/gateway key) for the shipping v2 webhook endpoints
  4. 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

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

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)