Skip to main content
After a user signs up (via password, OAuth-with-unverified-email, etc.), you usually want to confirm they own the email address. Pylon’s email-verification flow is the same primitive as magic-code sign-in, gated on an authenticated session — it sends a 6-digit code to the user’s email field and stamps emailVerified on successful verify. The OAuth + SSO sign-in paths automatically stamp emailVerified because the upstream IdP just vouched for the email. Use this flow when the email wasn’t IdP-verified — manual signup, edit-email flow, etc.

Endpoints

Schema

The User entity needs an emailVerified field. Pylon writes the current ISO 8601 timestamp on success:
If your schema lacks this field, update rejects the unknown column and /email/verify returns 500 PERSIST_FAILED (with a hint to add an emailVerified datetime field to the User entity) instead of silently succeeding.

Sending a verification code

Pylon looks up the caller’s User row, reads email, mints a 6-digit code, and sends an email with subject "Verify your email address" via the configured email transport. Body: "Your email verification code is: <code>\n\nThis code will expire in 10 minutes.". Response in production:
Response in dev (PYLON_DEV_MODE=true):
Errors: The throttle is shared with magic-code sends — 1 code per email per minute. The 10-minute TTL is shared too.

Verifying

Response on success:
Pylon stamps emailVerified to the current ISO 8601 timestamp and echoes it back in the response. Errors:

Gating handlers on emailVerified

In your TS code:
In policies, project emailVerified from the User entity and reference it directly:
Pylon doesn’t bake an opinionated auth.emailVerified into the policy DSL today — the field lives on your User row and you reference it via your own handlers / queries.

Security guarantees

  • Code generation, comparison, throttling shared with magic-codes — 6-digit numeric, 10-minute TTL, burn-after-5-wrong-attempts, constant-time comparison.
  • Session-gated — only the authenticated user can request + verify their own email. There’s no admin override here; if you need to mark a verified-via-out-of-band-process, write the User row directly with admin auth.
  • ISO 8601 timestamp formatpylon_kernel::util::now_iso() produces 2026-01-15T10:30:00Z. The previous <unix>Z format was silently rejected by the storage adapter, leaving users in a perpetual unverified state. Fixed in framework — flag emails are verified iff emailVerified is non-null.

Where to go next

  • Magic codes — the same 6-digit code primitive as a primary sign-in flow
  • Password — the register flow that produces an unverified email