koala73/worldmonitor · error · ApiError

API key required

Error message

API key required

What it means

listWebhooks calls validateApiKey with forceKey: true, so an explicit shipping v2 API key is mandatory even for Clerk-authenticated PRO callers. Without a key, callerFingerprint() would collapse to the shared 'anon' bucket and the ownerTag !== ownerHash defense would degenerate (both sides 'anon'), exposing every anon-bucket tenant's webhooks — hence the up-front 401.

Source

Thrown at server/worldmonitor/shipping/v2/list-webhooks.ts:35

  ownerIndexKey,
  callerFingerprint,
  type WebhookRecord,
} from './webhook-shared';

export async function listWebhooks(
  ctx: ServerContext,
  _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.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 smembersResult = await runRedisPipeline([['SMEMBERS', ownerIndexKey(ownerHash)]]);
  const memberIds = (smembersResult[0]?.result as string[] | null) ?? [];

  if (memberIds.length === 0) {
    return { webhooks: [] };
  }

  const getResults = await runRedisPipeline(memberIds.map(id => ['GET', webhookKey(id)]));
  const webhooks: WebhookSummary[] = [];
  for (const r of getResults) {
    if (!r.result || typeof r.result !== 'string') continue;
    try {
      const record = JSON.parse(r.result) as WebhookRecord;

View on GitHub (pinned to eeab0a219f)

Solutions

  1. Send the shipping v2 API key header (X-WorldMonitor-Key, per docs/api-shipping-v2.mdx) on every ListWebhooks and RegisterWebhook call
  2. Confirm the key is active — re-issue from the dashboard if revoked or expired
  3. Keep sending the key even when already Clerk-authenticated; the session alone is intentionally insufficient for this handler

Example fix

// before
await shippingClient.listWebhooks({}); // 401: API key required
// after
await shippingClient.listWebhooks({}, {
  headers: { 'X-WorldMonitor-Key': process.env.WORLDMONITOR_API_KEY! },
});
Defensive patterns

Strategy: validation

Validate before calling

if (!process.env.WORLDMONITOR_API_KEY) throw new Error('X-WorldMonitor-Key missing — issue and configure a shipping v2 key before calling listWebhooks');

Try / catch

catch (e) { if (e?.status === 401) { halt and prompt for API key provisioning; do not retry with the same credentials } else throw e; }

Prevention

When it happens

Trigger: Calling ListWebhooks with only a Clerk session and no X-WorldMonitor-Key header; sending an invalid, revoked, or expired API key; a server-side script that never loaded the key. The 401 fires before requirePremiumRpcAccess and before the SMEMBERS owner-index read.

Common situations: Integration reusing the browser session token instead of the issued API key; key rotated or revoked server-side; local dev .env missing the key; curl testing without the header; assuming PRO subscription alone authorizes webhook RPCs.

Understand the failure class

Background: "API key is required" / "API key not found" / "No API key was set": the missing-api-key error family across 16 libraries — this error's family across 16 libraries.

Related errors


AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21). Data as JSON: /api/errors/09ee74792990d896. Report an issue: GitHub.