koala73/worldmonitor · warning · ConvexError
INVITE_ALREADY_USED
INVITE_ALREADY_USED
Error message
INVITE_ALREADY_USED
What it means
`acceptBusinessInvite` is single-use: it only accepts grants whose status is exactly `"pending"`. A grant already `"accepted"` or `"revoked"` is rejected with INVITE_ALREADY_USED. The status check runs before token verification, so a reused link fails fast.
Source
Thrown at convex/payments/businessSeats.ts:468
* grant to `accepted`, stamps `inviteeUserId`, and recomputes the invitee's
* entitlement. Single-use: accepted/revoked/expired tokens are rejected.
*/
export const acceptBusinessInvite = mutation({
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" });
}
View on GitHub (pinned to ffec79ac33)
Solutions
- Treat INVITE_ALREADY_USED as success if the current user already holds the Pro entitlement from that grant
- Avoid re-using accept links — they are intentionally single-use
- If the seat was revoked, ask the owner to issue a fresh invite
Defensive patterns
Strategy: try-catch
Try / catch
try {
await convex.mutation(api.payments.businessSeats.acceptBusinessInvite, { grantId, token });
} catch (err) {
if (err.data?.kind === 'INVITE_ALREADY_USED') {
// check entitlement; if user already has Pro from this grant, treat as success
} else { throw err; }
} Prevention
- Accept links are single-use — don't reuse them
- After a successful accept, redirect away so a refresh can't re-submit
- Handle email-client link pre-fetch by treating INVITE_ALREADY_USED as benign
When it happens
Trigger: Clicking an accept link a second time after a successful accept; clicking a link for a grant that has since been revoked by the owner.
Common situations: User accepts, gets Pro, then clicks the email link again; the owner revoked the seat and the invitee retries the old link; email client pre-fetches the link once.
Related errors
AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12).
Data as JSON: /api/errors/6b038c9b14950612.
Report an issue: GitHub.