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
- Upgrade to an API_STARTER+ plan (tier 2 or higher) to obtain apiAccess=true.
- If recently upgraded, wait for the entitlement webhook to write the new row, or re-check entitlement before retrying.
- Surface an upgrade CTA in the UI on receipt of this error rather than retrying.
- 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
- GATE the createApiKey UI behind an entitlement check that verifies features.apiAccess === true and validUntil is in the future.
- After plan upgrades, await the entitlement webhook write before enabling the API keys panel.
- Surface an upgrade CTA (not a retry loop) on API_ACCESS_REQUIRED.
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.