koala73/worldmonitor · error · ConvexError
BUSINESS_NOT_ACTIVE
BUSINESS_NOT_ACTIVE
Error message
BUSINESS_NOT_ACTIVE
What it means
Thrown by acceptBusinessInvite when the Business subscription backing the seat grant is unusable: the subscriptions row looked up by grant.businessSubscriptionId is missing, its planKey is not "api_business", or isCoveringAt(businessSub, now) is false (subscription expired or no longer covering the current instant). The seat cannot be attached because there is no active Business plan to draw it from.
Solutions
- Have the owner check Manage Billing: renew or restart the api_business subscription, then send a fresh invite (an expired grant stays dead)
- Owner verifies the grant's businessSubscriptionId still matches the current active subscription's dodoSubscriptionId; if stale, revoke and re-create the invite
- Catch err.data.kind === "BUSINESS_NOT_ACTIVE" and tell the invitee to contact their admin rather than retrying
- If the subscription is active and the id matches, check isCoveringAt inputs (current_period end / status) for a webhook that never landed
Example fix
// before — invitee retries a dead grant forever
await accept(ctx, { grantId, token });
// after — degrade gracefully for the invitee
try {
await accept(ctx, { grantId, token });
} catch (e) {
if (kindOf(e) === "BUSINESS_NOT_ACTIVE") return showContactAdmin();
throw e;
} Defensive patterns
Strategy: try-catch
Type guard
function isBusinessNotActive(e: unknown): boolean {
return e instanceof ConvexError && (e.data as { kind?: string } | undefined)?.kind === "BUSINESS_NOT_ACTIVE";
} Try / catch
try {
await acceptInvite({ grantId, token });
} catch (e) {
if (isBusinessNotActive(e)) return showContactAdmin("The Business plan is no longer active");
throw e;
} Prevention
- Check invite age client-side and warn before accepting long-lived invites (the sub may have lapsed)
- Owners: keep grants short-lived so a lapsed subscription invalidates them quickly
- Monitor webhook-driven subscription cancellations and proactively revoke outstanding invites
When it happens
Trigger: The owner's api_business subscription was cancelled, failed renewal, or expired between invite creation and acceptance; the grant references a dodoSubscriptionId that no longer maps to any subscriptions row (e.g. after a plan migration created a new Dodo subscription).
Common situations: Trial expired before the invitee clicked the link; owner's card failed during the invite window; subscription replaced/migrated leaving stale grants pointing at the old dodoSubscriptionId.
Related errors
- HTTP ( )
- MCP_INTERNAL_HMAC_SECRET not configured
- Account changed while creating the embed key. Try again.
- Account changed while revoking the embed key. Try again.
- ALREADY_REVOKED
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/451266076803d943.
Report an issue: GitHub.
Appendix: source
Thrown at convex/payments/businessSeats.ts:490
}
if (grant.inviteeEmail !== inviteeEmail) {
throw new ConvexError({ kind: "INVITE_EMAIL_MISMATCH" });
}
if (!sameDomain(grant.inviteeEmail, inviteeEmail)) {
throw new ConvexError({ kind: "INVITE_EMAIL_MISMATCH" });
}
if (!isCorporateDomain(inviteeEmail)) {
throw new ConvexError({ kind: "INVITEE_DOMAIN_NOT_CORPORATE" });
}
const businessSub = await ctx.db
.query("subscriptions")
.withIndex("by_dodoSubscriptionId", (q) =>
q.eq("dodoSubscriptionId", grant.businessSubscriptionId),
)
.unique();
if (!businessSub || !isBusinessPlan(businessSub.planKey) || !isCoveringAt(businessSub, now)) {
throw new ConvexError({ kind: "BUSINESS_NOT_ACTIVE" });
}
await ctx.db.patch(args.grantId, {
status: "accepted",
inviteeUserId: userId,
acceptedAt: now,
});
await ctx.runMutation(
internal.payments.subscriptionHelpers.recomputeEntitlementForUser,
{ userId, eventTimestamp: now },
);
return { ok: true as const };
},
});
View on GitHub (pinned to 7d06c8633d)