koala73/worldmonitor · error · ConvexError

API_ACCESS_REQUIRED

API_ACCESS_REQUIRED

Error message

API_ACCESS_REQUIRED

What it means

Thrown by createApiKey when the caller's entitlement row is missing, expired (validUntil < Date.now()), or has features.apiAccess === false. The apiAccess feature flag is catalog-driven: Pro (tier 1) has apiAccess=false; API_STARTER and above (tier 2+) have apiAccess=true. Only plans with apiAccess may mint API keys.

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 ffec79ac33)

Solutions

  1. Upgrade to an API_STARTER+ plan (tier 2 or higher) to obtain apiAccess=true.
  2. If recently upgraded, wait for the entitlement webhook to write the new row, or re-check entitlement before retrying.
  3. Surface an upgrade CTA in the UI on receipt of this error rather than retrying.
  4. Confirm the entitlement row's validUntil is in the future and features.apiAccess is true via getEntitlement before calling createApiKey.

Example fix

// before
await createApiKey({ name, keyPrefix, keyHash }); // Pro plan
// after
// upgrade to API_STARTER+, then:
const ent = await getEntitlement();
if (ent?.features.apiAccess && ent.validUntil > Date.now()) {
  await createApiKey({ name, keyPrefix, keyHash });
}
Defensive patterns

Strategy: validation

Validate before calling

// Before calling createApiKey, confirm entitlement:
const ent = await getEntitlement();
const hasApiAccess = !!ent && ent.validUntil > Date.now() && ent.features.apiAccess === true;
if (!hasApiAccess) {
  // surface upgrade CTA, do not call createApiKey
}

Type guard

function hasApiAccess(ent): boolean {
  return !!ent
    && typeof ent.validUntil === 'number' && ent.validUntil > Date.now()
    && !!ent.features && ent.features.apiAccess === true;
}

Try / catch

try {
  await createApiKey(args);
} catch (e) {
  if (e instanceof ConvexError && e.message === 'API_ACCESS_REQUIRED') {
    // route to upgrade/plan-selection flow; do not retry without a plan change
  } else throw e;
}

Prevention

When it happens

Trigger: Calling createApiKey while on a Pro (tier 1) plan; calling after entitlement expiry; calling before any entitlement row exists for the user; calling during a billing-lapse window.

Common situations: A Pro user trying to use the API without upgrading; a recently downgraded user whose entitlement row reflects the loss of apiAccess; a brand-new user who has not yet subscribed; a billing webhook delay leaving the row stale.

Related errors


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