koala73/worldmonitor · warning · ConvexError
INVITE_EXPIRED
INVITE_EXPIRED
Error message
INVITE_EXPIRED
What it means
Each grant carries a 14-day TTL (`INVITE_TTL_MS = 14 * 24 * 60 * 60 * 1000`). `acceptBusinessInvite` rejects any grant whose `expiresAt <= now`. Expired grants no longer count against the seat cap and cannot be revived — a new grant must be issued.
Source
Thrown at convex/payments/businessSeats.ts:472
args: { grantId: v.id("businessProGrants"), token: v.string() },
handler: async (ctx, args) => {
const userId = await requireUserId(ctx);
const identity = await resolveUserIdentity(ctx);
const inviteeEmail = identity?.email?.trim().toLowerCase();
if (!inviteeEmail) {
throw new ConvexError({ kind: "INVITEE_EMAIL_UNAVAILABLE" });
}
const grant = await ctx.db.get(args.grantId);
if (!grant) {
throw new ConvexError({ kind: "GRANT_NOT_FOUND" });
}
if (grant.status !== "pending") {
throw new ConvexError({ kind: "INVITE_ALREADY_USED" });
}
const now = Date.now();
if (grant.expiresAt <= now) {
throw new ConvexError({ kind: "INVITE_EXPIRED" });
}
if (!(await verifyBusinessInviteToken(args.grantId, args.token))) {
throw new ConvexError({ kind: "INVALID_INVITE_TOKEN" });
}
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),View on GitHub (pinned to ffec79ac33)
Solutions
- Ask the Business owner to resend the invite, which creates a fresh grant with a new 14-day window
- Owner calls `inviteSeats` again for the same email (dedup logic frees the expired slot and issues a new pending grant)
Defensive patterns
Strategy: try-catch
Try / catch
try {
await convex.mutation(api.payments.businessSeats.acceptBusinessInvite, { grantId, token });
} catch (err) {
if (err.data?.kind === 'INVITE_EXPIRED') {
// prompt owner to resend the invite (fresh 14-day grant)
} else { throw err; }
} Prevention
- Accept invites promptly within the 14-day window
- Surface the invite-expiry reason clearly and offer a 'request re-invite' action
When it happens
Trigger: Clicking an accept link more than 14 days after the invite was sent (`grant.expiresAt <= Date.now()`).
Common situations: User delayed accepting; invite email buried in inbox; user switched jobs and returned to the email weeks later.
Related errors
- INVITEE_EMAIL_UNAVAILABLE
- INVITE_ALREADY_USED
- INVALID_INVITE_TOKEN
- INVITE_EMAIL_MISMATCH
- SEAT_CAP_REACHED
AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12).
Data as JSON: /api/errors/5cdf4e32da75e33d.
Report an issue: GitHub.