Skip to main content
After a user signs up (via password, OAuth with an unverified email, etc.), you usually want to confirm they own the email address. The email-verification flow uses the same primitive as magic-code sign-in, but gated on an authenticated session. It sends a code (6 digits by default) to the user’s email field and stamps emailVerified on a successful verify. The OAuth and SSO sign-in paths stamp emailVerified automatically, because the upstream IdP already verified the email. Use this flow when the IdP did not verify the email (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, creates a code, and sends an email with subject "Verify your email address" through the configured email transport. Body: "Your email verification code is: <code>\n\nThis code will expire in 10 minutes.". Override both with PYLON_EMAIL_TEMPLATE_EMAIL_VERIFY_SUBJECT and PYLON_EMAIL_TEMPLATE_EMAIL_VERIFY_BODY. The response includes codeLength. Response in production:
Response in dev (PYLON_DEV_MODE=true) to a request from the same machine:
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 does not add auth.emailVerified to the policy DSL today. The field lives on your User row. You reference it through your own handlers or queries.

Security guarantees

  • Code generation, comparison, throttling shared with magic-codes — numeric (6 digits by default, PYLON_AUTH_CODE_LENGTH), 10-minute TTL, burn-after-5-wrong-attempts, the same daily wrong-guess limit per email (429 CODE_LOCKED), constant-time comparison.
  • Session-gated — only the authenticated user can request and verify their own email. There is no admin override. To mark an email verified from another process, write the User row directly with admin auth.
  • ISO 8601 timestamp format — pylon_kernel::util::now_iso() produces 2026-01-15T10:30:00Z. An email is verified only when emailVerified is non-null.

Where to go next

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