koala73/worldmonitor · error · ConvexError
INVALID_INVITE_TOKEN
INVALID_INVITE_TOKEN
Error message
INVALID_INVITE_TOKEN
What it means
The accept URL embeds an HMAC token bound to the grantId via `signBusinessInviteToken`. `acceptBusinessInvite` verifies it with `verifyBusinessInviteToken`; any mismatch raises INVALID_INVITE_TOKEN. This guards against forged or tampered accept links and binds the token to the specific grant row.
Source
Thrown at convex/payments/businessSeats.ts:475
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),
)
.unique();
if (!businessSub || businessSub.planKey !== "api_business" || !isCoveringAt(businessSub, now)) {View on GitHub (pinned to ffec79ac33)
Solutions
- Use the exact accept link from the original invite email without modification
- If the signing secret was rotated, have the owner resend the invite so a fresh token is minted
- If the URL was truncated by a mail client, request a resend
Defensive patterns
Strategy: try-catch
Try / catch
try {
await convex.mutation(api.payments.businessSeats.acceptBusinessInvite, { grantId, token });
} catch (err) {
if (err.data?.kind === 'INVALID_INVITE_TOKEN') {
// the link is corrupt/tampered — request the owner resend the invite
} else { throw err; }
} Prevention
- Use the exact accept link from the invite email without editing
- After a signing-secret rotation, reissue invites so tokens re-verify
- Beware mail clients that wrap/truncate long URLs
When it happens
Trigger: The token in the URL does not verify against the grantId: edited/truncated token, grantId reused with a stale token, or the HMAC signing secret was rotated after the invite was issued.
Common situations: Email client wrapped or truncated the long accept URL; user hand-edited the link; the identity-signing secret rotated and old tokens no longer verify.
Related errors
AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12).
Data as JSON: /api/errors/fe2ce99445e7f076.
Report an issue: GitHub.