Emails, sign-in links and passwords
Verify users' email addresses, sign them in with a one-time email link, let them reset or change their password, and choose the address Ploy Auth sends from.
Emails, sign-in links and passwords
Ploy Auth sends these emails for your app, through Ploy Email:
| Sent when | |
|---|---|
| Verify your email | A user signs up with a password, or asks for the link again |
| Sign-in link | A user asks for a sign-in link instead of using a password |
| Reset your password | A user asks for a reset on the Forgot password? page |
| Password changed | A password was reset, changed, or added, as a notice to the account owner |
The emails show your project name and work without any code in your app.
Auth emails are off until email sending is enabled for your organization under Dashboard → Email. Until then, sign-in links and password reset are unavailable and email verification is not enforced. Everything else keeps working.
Email verification
After a password sign-up, Ploy Auth emails a verification link. It is valid for 24 hours. Opening it marks the account as verified (emailVerified: true) and sends the user back to your app with ?ploy_auth_status=email_verified; an invalid or expired link adds ?ploy_auth_error=invalid_token instead.
Users who sign in with Google, Apple, or GitHub are verified when the provider confirms their email, and users who sign in with a sign-in link are verified by opening it, so they never get this email.
Require a verified email
Turn on Require email verification under Auth → Settings → Emails to keep unverified password users out:
- Sign-up still creates the account and sends the link, but responds with
emailVerificationRequired: trueand no session. - Password and passkey sign-in for an unverified password account fail with
403and codeemail_not_verified. The error only appears after the correct password, so it does not reveal which emails have accounts. - The hosted sign-in page and
SignInFormthen offer Resend verification email. - A sign-in link always works: opening it proves the address.
The setting has no effect while auth emails are off, so users are never locked out by an email that can't be sent.
Sign-in links
A sign-in link (often called a magic link) lets users sign in with just their email address. Turn on Magic link (email sign-in link) under Auth → Settings → Sign-in methods. It counts as a sign-in method, so it can be the only one, but it only works while auth emails are enabled; the dashboard shows a hint when they are not.
When it is on, the hosted sign-in page and SignInForm offer Email me a sign-in link. Without password sign-in it becomes the main action of the email form.
- The link is valid for 15 minutes and works once. Only a hash of it is stored.
- Opening the link shows a confirmation page with a Continue as button. The link itself never signs anyone in, so email security scanners that open links ahead of the user don't use it up.
- For an address without an account, the link creates the account when sign-ups are allowed. Require name applies: send
metadata.namewith the request, or the request fails with codename_requiredand the forms ask for the name. - The response is the same whether or not an account exists, and no email is sent for unknown addresses while sign-ups are off.
- Opening the link marks the email as verified. If the account was never verified, its password and passkeys are removed first, so someone who registered an address they don't own loses access. The owner can then set a password.
- After confirming, the user lands on the
redirectpassed with the request, signed in. An expired or used link returns there with?ploy_auth_error=invalid_token.
await fetch("/_ploy/auth/magic-link", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "user@example.com", redirect: "/dashboard" }),
});Password reset
When password sign-in is on and auth emails are enabled, the hosted sign-in page and SignInForm show Forgot password?. It opens /_ploy/auth/forgot-password, which emails a link to /_ploy/auth/reset-password.
- The link is valid for one hour and works once: it stops working as soon as the password changes.
- Setting the new password signs the user out on every device and marks their email as verified.
- The forgot-password page responds the same whether or not an account exists, so it can't be used to find out who has an account. Accounts that only use passkeys, social sign-in, or sign-in links have no password and get no email.
After the reset the user lands on the sign-in page with a confirmation and continues to the redirectTo you passed to /forgot-password.
Change or set a password
Signed-in users can change their password, or add one to an account created with a sign-in link, a passkey, or a social provider. UserProfile has a Password section for this; from your own UI, POST /password/change with the session cookie or an Authorization: Bearer header:
await fetch("/_ploy/auth/password/change", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ currentPassword, newPassword }),
});currentPasswordis required when the account already has a password; a wrong one fails with401and codeinvalid_credentials.GET /mereturnshasPasswordso you know whether to ask for it.- The new password must meet the same rules as at sign-up (code
weak_passwordotherwise). - Every other session of the user is signed out; the current one stays signed in.
- The owner gets a notice email when auth emails are enabled.
Password sign-in must be on for the project.
Sender settings
Configure the sender under Auth → Settings → Emails:
| Setting | Description |
|---|---|
| Send from | Your organization's <name>.ploy.email domain (the default) or one of its verified custom domains |
| Address | The part before the @, for example noreply (the default) or hello |
| Display name | The name shown in the inbox. Defaults to the project name |
The settings show a preview of the From address, for example Acme <noreply@acme.ploy.email>. If a custom domain stops being verified, emails are sent from the ploy.email domain until it is verified again. See Email to add a custom domain.
Rate limits
To protect your users' inboxes, verification, sign-in link, and reset emails are limited to 3 per hour per email address and 10 per hour per IP address. Further requests return 429 with code: "rate_limited" and a Retry-After header. Password changes share the sign-in limits. Emails also count toward your project's daily email limit.
Endpoints
| Endpoint | Method | Body | Description |
|---|---|---|---|
/email/verify/request | POST | { email, redirect? } | Send the verification email again. Always returns 200 |
/email/verify | GET | ?token=&redirect= | The link in the verification email |
/magic-link | POST | { email, redirect?, metadata? } | Email a sign-in link. Always returns 200 |
/magic-link/verify | GET | ?token=&redirect= | The link in the email: a page to confirm the sign-in |
/magic-link/verify | POST | { token } | Use the link and start a session. Returns { user } |
/password/forgot | POST | { email, redirect? } | Send a password reset email. Always returns 200 |
/password/reset | POST | { token, password } | Set a new password with the token from the reset link |
/password/change | POST | { currentPassword?, newPassword } | Change or set the signed-in user's password |
/forgot-password | GET | ?redirectTo= | Hosted page to request a reset link |
/reset-password | GET | ?token=&redirectTo= | Hosted page to choose a new password |
redirect and redirectTo accept a path or a URL on your app's origin, like everywhere else in Ploy Auth. /email/verify/request, /magic-link, and /password/forgot return 403 with code email_unavailable while auth emails are off, and /magic-link returns magic_link_disabled while sign-in links are off. GET /config reports emailVerificationRequired, passwordResetEnabled, and magicLink so you can adapt your own UI.
With @meetploy/auth-react, use the hook instead of calling the endpoints:
import { PloyAuthError, useAuth } from "@meetploy/auth-react";
const {
user,
signIn,
resendVerification,
sendMagicLink,
requestPasswordReset,
resetPassword,
changePassword,
} = useAuth();
try {
await signIn(email, password);
} catch (error) {
if (error instanceof PloyAuthError && error.code === "email_not_verified") {
await resendVerification(email);
}
}
await sendMagicLink(email, { redirectTo: "/dashboard" });
await requestPasswordReset(email, { redirectTo: "/account" });
await resetPassword(token, newPassword);
await changePassword({
currentPassword: user?.hasPassword ? currentPassword : undefined,
newPassword,
});Local development
Under ploy dev nothing is sent. Every auth email lands in a local inbox instead: open the dev dashboard and choose Auth → Emails to read it and click its links. The terminal also prints each email's link.
Sign-in links are always on locally, next to passwords and passkeys. Email verification is not required locally unless you turn on Require email verification in the dev dashboard's auth settings.
How is this guide?
Last updated on