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

  1. If already verified, treat as success and proceed to login rather than re-verifying.
  2. Ensure the token is forwarded verbatim from the email link (watch URL encoding).
  3. If trash is enabled, confirm the user document isn't trashed.
  4. 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

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

Related errors


AI-assisted analysis of payloadcms/payload@00c58b35c0 (2026-08-12). Data as JSON: /api/errors/fb7865d3c5dda6bf. Report an issue: GitHub.