Magic Link
Passwordless sign-in through a single-use emailed link. Mirrors the TS magicLink() plugin.
Enable
python
from better_auth import BetterAuth
from better_auth.plugins_ext import MagicLinkPlugin
async def send_magic_link(email, url, token, request):
... # mail the url
auth = BetterAuth(
secret="a-strong-32-character-minimum-secret",
plugins=[MagicLinkPlugin(send_magic_link=send_magic_link)],
)Options
| Option | Type | Default | Description |
|---|---|---|---|
send_magic_link | callable | required | (email, url, token, request) -> None, delivers the link. |
expires_in | int | 300 | Link lifetime in seconds. |
allowed_attempts | int | None | None | Deprecated in TS; tokens are single-use regardless. Any value other than 1 logs a warning. |
disable_sign_up | bool | False | Only sign in existing users; never create one from a link. |
rate_limit | dict[str, int] | None | None | Rate-limit overrides. |
generate_token | callable | None | None | Custom token generator, (email) -> str. |
store_token | str | dict | "plain" | "plain", "hashed", or a custom-hasher config for the stored token. |
Endpoints
| Method | Path |
|---|---|
| POST | /sign-in/magic-link |
| GET | /magic-link/verify |
Schema
No extra tables: tokens live in the core verification table, keyed by the stored token with value JSON({email, name?}), byte-compatible with TS.
Notes
- Verification consumes the token atomically: N racing verifies mint at most one session; invalid/expired tokens redirect to
errorCallbackURL?error=INVALID_TOKEN. - Each callback URL is origin-checked before redirecting.
- Signing in an existing unverified user deletes every account linked to it (the password and any OAuth links) and all of its sessions, then marks the email verified. Only then is the new session created.
- When
user.validate_user_infois set, it runs before a new user is created, withsourceset to{"action": "create-user", "method": "magic-link"}. - If user creation is refused (by
validate_user_infoor a database hook), the verify endpoint redirects toerrorCallbackURLwith the error code inerrorand the message inerror_description.
Changed in 1.1
Before 1.1, only the password account was removed when an unverified user signed in with a magic link. OAuth links are now deleted too. See Upgrade from 1.0.