{"record":{"id":"fb7865d3c5dda6bf","repo":"payloadcms/payload","slug":"verification-token-is-invalid","errorCode":null,"errorMessage":"Verification token is invalid.","messagePattern":"Verification token is invalid\\.","errorType":"exception","errorClass":"APIError","httpStatus":403,"severity":"error","filePath":"packages/payload/src/auth/operations/verifyEmail.ts","lineNumber":46,"sourceCode":"  try {\n    const shouldCommit = await initTransaction(req)\n\n    const where = appendNonTrashedFilter({\n      enableTrash: Boolean(collection.config.trash),\n      trash: false,\n      where: {\n        _verificationToken: { equals: token },\n      },\n    })\n\n    const user = await req.payload.db.findOne<any>({\n      collection: collection.config.slug,\n      req,\n      where,\n    })\n\n    if (!user) {\n      throw new APIError('Verification token is invalid.', httpStatus.FORBIDDEN)\n    }\n\n    // Ensure updatedAt date is always updated\n    user.updatedAt = new Date().toISOString()\n\n    await req.payload.db.updateOne({\n      id: user.id,\n      collection: collection.config.slug,\n      data: {\n        ...user,\n        _verificationToken: null,\n        _verified: true,\n      },\n      req,\n      returning: false,\n    })\n\n    if (shouldCommit) {","sourceCodeStart":28,"sourceCodeEnd":64,"githubUrl":"https://github.com/payloadcms/payload/blob/00c58b35c0ed348ddc22daabf467b139727214fd/packages/payload/src/auth/operations/verifyEmail.ts#L28-L64","documentation":"Thrown in `verifyEmailOperation` after `findOne({ where: { _verificationToken: { equals: token } } })` returns no user. The token matches no document — it is invalid, already used, or belongs to a trashed/filtered doc. `APIError` with HTTP 403 (FORBIDDEN).","triggerScenarios":"The user clicks an already-used verification link (Payload nulls `_verificationToken` after success); the token is malformed; the doc is trashed and the non-trash filter excludes it; the token never existed.","commonSituations":"Double-clicking the verify link (second click finds the token already cleared); URL encoding mangles the token; trash-enabled collection where the user is soft-deleted; stale link from an older verification email after re-registration.","solutions":["If already verified, treat as success and proceed to login rather than re-verifying.","Ensure the token is forwarded verbatim from the email link (watch URL encoding).","If trash is enabled, confirm the user document isn't trashed.","Request a new verification email if the token is genuinely invalid/expired."],"exampleFix":"// before\nawait payload.verifyEmail({ collection, token, req })\n// after — tolerate already-verified state\ntry {\n  await payload.verifyEmail({ collection, token, req })\n} catch (e) {\n  if (e.message.includes('invalid')) {\n    // likely already verified or expired — prompt re-send\n  }\n}","handlingStrategy":"try-catch","validationCode":"// Best-effort: confirm the token still maps to a user\nconst user = await payload.find({\n  collection,\n  where: { _verificationToken: { equals: token } },\n  req,\n  overrideAccess: true,\n})\nif (user.docs.length === 0) { return reportAlreadyVerifiedOrInvalid() }","typeGuard":"function isInvalidVerificationToken(e: unknown): e is APIError {\n  return e instanceof APIError && e.status === 403 && /invalid/.test(e.message)\n}","tryCatchPattern":"try {\n  await payload.verifyEmail({ collection, token, req })\n} catch (e) {\n  if (isInvalidVerificationToken(e)) {\n    // likely already verified — proceed to login, or re-send verification\n  } else throw e\n}","preventionTips":["Handle a second click on a verify link gracefully (likely already verified).","Forward the token verbatim from the email link.","Offer a 'resend verification' path for genuinely stale tokens."],"tags":["auth","verify-email","token","forbidden"],"backgroundTag":null,"analyzedSha":"00c58b35c0ed348ddc22daabf467b139727214fd","analyzedAt":"2026-08-12T20:45:03.758Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}