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
| Endpoint | Method | Auth | Purpose |
|---|---|---|---|
/api/auth/email/send-verification | POST | session | Email a 6-digit code to the user’s current email |
/api/auth/email/verify | POST | session | Submit the code; stamp emailVerified |
Schema
The User entity needs anemailVerified field. Pylon writes the current ISO 8601 timestamp on success:
update will reject the unknown column and /email/verify returns a 4xx with the storage layer’s error code instead of silently succeeding.
Sending a verification code
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:
PYLON_DEV_MODE=true):
| Status | Code | Reason |
|---|---|---|
| 401 | UNAUTHORIZED | No session |
| 404 | USER_NOT_FOUND | Session resolves to a user_id with no matching row |
| 400 | MISSING_EMAIL | User row has no email field |
| 429 | RATE_LIMITED | A code was requested within the throttle window |
| 500 | EMAIL_SEND_FAILED | Email transport returned an error |
Verifying
emailVerified to the current ISO 8601 timestamp (e.g. "2026-01-15T10:30:00Z").
Errors:
| Status | Code | Reason |
|---|---|---|
| 401 | UNAUTHORIZED | No session |
| 400 | MISSING_CODE | code field absent |
| 400 | INVALID_JSON | Body wasn’t JSON |
| 4xx | INVALID_CODE | Wrong code, expired, or burned |
Gating handlers on emailVerified
In your TS code:
emailVerified from the User entity and reference it directly:
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 format —
pylon_kernel::util::now_iso()produces2026-01-15T10:30:00Z. The previous<unix>Zformat was silently rejected by the storage adapter, leaving users in a perpetual unverified state. Fixed in framework — flag emails are verified iffemailVerifiedis 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