knadh/listmonk · error

captcha token already used

Error message

captcha token already used

What it means

This error is returned by verifyAltcha when a client submits an ALTCHA proof-of-work captcha token that has already been verified. The manager keeps a short-lived store (tmptokens) of previously used tokens; if Check() finds the token present, it means the exact challenge solution was already consumed and verified, so verification is refused to prevent replay attacks.

Source

Thrown at internal/captcha/captcha.go:199

	}

	return nil, true
}

// verifyAltcha verifies an Altcha response.
func (c *Captcha) verifyAltcha(payload string) (error, bool) {
	valid, err := altcha.VerifySolution(payload, c.altcha.HMACKey, true)
	if err != nil {
		return fmt.Errorf("failed to verify captcha solution: %w", err), false
	}

	if !valid {
		return fmt.Errorf("captcha verification failed"), false
	}

	// Disallow token reuse.
	if _, err := tmptokens.Check(payload); err == nil {
		return fmt.Errorf("captcha token already used"), false
	}
	tmptokens.Set(payload, 5*time.Minute, nil)

	return nil, true
}

View on GitHub (pinned to 670c01717d)

Solutions

  1. Generate a fresh ALTCHA challenge and solve it for each submission instead of reusing a captured token
  2. Make the client-side submit handler idempotent: disable the button and do not resend on retry; re-solve the captcha for the retry
  3. Shorten frontend retry logic or re-request a challenge on 4xx responses before retrying
  4. If tokens expire too aggressively in tests, mint a new token per test case rather than sharing one

Example fix

// before
await verify(oldPayload) // second call: 'captcha token already used'
// after
const challenge = await fetchNewChallenge()
const payload = await solveAltcha(challenge) // fresh solve per submission
await verify(payload)
Defensive patterns

Strategy: validation

Validate before calling

// Client-side: always solve a fresh challenge per submission
const challenge = await fetch('/api/captcha/challenge').then(r => r.json())
const payload = await solveAltcha(challenge) // never cache/reuse across requests

Prevention

When it happens

Trigger: A Verify call is made with a payload/token that was already successfully verified within the previous 5 minutes (the token's stored TTL). This happens on double-submits, form resubmissions, browser back-button resends, or a client retrying a request that actually succeeded the first time.

Common situations: Users double-clicking a submit button, frontend code retrying a failed HTTP request (timeouts) where the first request actually succeeded, automated tests reusing a captured token across multiple verification calls, or proxy/load-balancer request duplication.

Related errors


AI-assisted analysis of knadh/listmonk@670c01717d (2026-09-01). Data as JSON: /api/errors/b0f4ee90a6ae63cd. Report an issue: GitHub.