payloadcms/payload · error · APIError
Verification token is invalid.
Error message
Verification token is invalid.
What it means
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).
Source
Thrown at packages/payload/src/auth/operations/verifyEmail.ts:46
try {
const shouldCommit = await initTransaction(req)
const where = appendNonTrashedFilter({
enableTrash: Boolean(collection.config.trash),
trash: false,
where: {
_verificationToken: { equals: token },
},
})
const user = await req.payload.db.findOne<any>({
collection: collection.config.slug,
req,
where,
})
if (!user) {
throw new APIError('Verification token is invalid.', httpStatus.FORBIDDEN)
}
// Ensure updatedAt date is always updated
user.updatedAt = new Date().toISOString()
await req.payload.db.updateOne({
id: user.id,
collection: collection.config.slug,
data: {
...user,
_verificationToken: null,
_verified: true,
},
req,
returning: false,
})
if (shouldCommit) {View on GitHub (pinned to 00c58b35c0)
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.
Example fix
// before
await payload.verifyEmail({ collection, token, req })
// after — tolerate already-verified state
try {
await payload.verifyEmail({ collection, token, req })
} catch (e) {
if (e.message.includes('invalid')) {
// likely already verified or expired — prompt re-send
}
} Defensive patterns
Strategy: try-catch
Validate before calling
// Best-effort: confirm the token still maps to a user
const user = await payload.find({
collection,
where: { _verificationToken: { equals: token } },
req,
overrideAccess: true,
})
if (user.docs.length === 0) { return reportAlreadyVerifiedOrInvalid() } Type guard
function isInvalidVerificationToken(e: unknown): e is APIError {
return e instanceof APIError && e.status === 403 && /invalid/.test(e.message)
} Try / catch
try {
await payload.verifyEmail({ collection, token, req })
} catch (e) {
if (isInvalidVerificationToken(e)) {
// likely already verified — proceed to login, or re-send verification
} else throw e
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- Incorrect collection
- Token is either invalid or has expired.
- error:notAllowedToPerformAction
- Missing required data.
- Cannot refresh token: user not authenticated
AI-assisted analysis of payloadcms/payload@00c58b35c0 (2026-08-12).
Data as JSON: /api/errors/fb7865d3c5dda6bf.
Report an issue: GitHub.