koala73/worldmonitor · error · ConvexError

API_ACCESS_REQUIRED

API_ACCESS_REQUIRED

Error message

API_ACCESS_REQUIRED

What it means

Entitlement gate on API-key creation: the user has no entitlement row, their entitlement has expired (validUntil in the past), or the plan's feature catalog does not include apiAccess (Pro tier 1 lacks it; API_STARTER tier 2+ has it). The mutation refuses to issue a key because API access is a paid, catalog-driven feature.

Solutions

  1. Upgrade to an API-enabled plan (API_STARTER or higher) before creating keys
  2. Check the entitlements row (existence, validUntil, features.apiAccess) to see which gate failed
  3. If the entitlement looks wrong, verify the plan catalog's apiAccess flags and re-sync the entitlement
Defensive patterns

Strategy: validation

When it happens

Trigger: Thrown at convex/apiKeys.ts:64 when the library encounters an invalid state.

Common situations: See trigger scenarios.


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

Appendix: source

Thrown at convex/apiKeys.ts:64

    keyHash: v.string(),
    scopes: v.optional(v.array(v.string())),
  },
  handler: async (ctx, args) => {
    const userId = await requireUserId(ctx);

    // Entitlement gate: only users with apiAccess may create API keys.
    // This is catalog-driven — Pro (tier 1) has apiAccess=false;
    // API_STARTER+ (tier 2+) have apiAccess=true.
    const entitlement = await ctx.db
      .query("entitlements")
      .withIndex("by_userId", (q) => q.eq("userId", userId))
      .first();
    if (
      !entitlement ||
      entitlement.validUntil < Date.now() ||
      !entitlement.features.apiAccess
    ) {
      throw new ConvexError("API_ACCESS_REQUIRED");
    }

    const scopes = normalizeCompanyMonitoringScopes(args.scopes);
    // Issuing a scoped key is a first-use entry point, so it provisions the
    // root. Requesting no scopes must stay entirely off Company Monitoring.
    const companyMonitoringAccount = scopes
      ? await ensureActiveAccount(ctx, userId, entitlement)
      : null;
    if (scopes && !companyMonitoringAccount) {
      throw new ConvexError("COMPANY_MONITORING_ACCESS_DENIED");
    }

    if (!args.name.trim()) {
      throw new ConvexError("INVALID_NAME");
    }
    if (!/^wm_[a-f0-9]{5}$/.test(args.keyPrefix)) {
      throw new ConvexError("INVALID_PREFIX");
    }

View on GitHub (pinned to eeab0a219f)