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

  1. Have the owner check Manage Billing: renew or restart the api_business subscription, then send a fresh invite (an expired grant stays dead)
  2. Owner verifies the grant's businessSubscriptionId still matches the current active subscription's dodoSubscriptionId; if stale, revoke and re-create the invite
  3. Catch err.data.kind === "BUSINESS_NOT_ACTIVE" and tell the invitee to contact their admin rather than retrying
  4. 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

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


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)