Magic Link
Magic links let users sign in by clicking a link in their email. The link returns the user to your app, where you verify the redirect and connect the wallet.
Use magic links when you want email login without asking the user to copy a code.
Configure the magic-link URL
Magic links need a route in your app that can receive the redirect and call useVerifyMagicLink. Configure the magic-link URL template for your project in the ZeroDev dashboard. For local development, this might be:
http://localhost:3000/verifyMake sure the redirect origin is also added to the project's ACL allowlist in the ZeroDev dashboard.
Send the magic link
Call useSendMagicLink from your login page. Store the returned otpId and otpEncryptionTargetBundle so the verify page can complete authentication.
import { useSendMagicLink } from '@zerodev/wallet-react'
import { useState } from 'react'
export function MagicLinkLogin() {
const [email, setEmail] = useState('')
const sendMagicLink = useSendMagicLink()
return (
<div>
<input
type="email"
autoComplete="email"
placeholder="you@example.com"
value={email}
onChange={(event) => setEmail(event.target.value)}
/>
<button
type="button"
disabled={sendMagicLink.isPending || !email}
onClick={async () => {
const result = await sendMagicLink.mutateAsync({ email })
sessionStorage.setItem('magicLinkOtpId', result.otpId)
sessionStorage.setItem(
'magicLinkBundle',
result.otpEncryptionTargetBundle,
)
}}
>
{sendMagicLink.isPending ? 'Sending link...' : 'Send magic link'}
</button>
{sendMagicLink.isSuccess ? <p>Check your email</p> : null}
{sendMagicLink.error ? <p>{sendMagicLink.error.message}</p> : null}
</div>
)
}Verify the link
On your verify route, read the code query parameter and the stored otpId and otpEncryptionTargetBundle, then call useVerifyMagicLink.
import { useVerifyMagicLink } from '@zerodev/wallet-react'
import { useEffect } from 'react'
import { useAccount } from 'wagmi'
export function VerifyMagicLink() {
const { address, isConnected } = useAccount()
const verifyMagicLink = useVerifyMagicLink()
useEffect(() => {
const params = new URLSearchParams(window.location.search)
const code = params.get('code')
const otpId = sessionStorage.getItem('magicLinkOtpId')
const otpEncryptionTargetBundle = sessionStorage.getItem(
'magicLinkBundle',
)
if (
code &&
otpId &&
otpEncryptionTargetBundle &&
!isConnected &&
!verifyMagicLink.isPending
) {
verifyMagicLink.mutate({
otpId,
otpEncryptionTargetBundle,
code,
})
}
}, [isConnected, verifyMagicLink])
if (isConnected) {
return <p>Connected: {address}</p>
}
if (verifyMagicLink.isPending) {
return <p>Verifying link...</p>
}
if (verifyMagicLink.error) {
return <p>{verifyMagicLink.error.message}</p>
}
return <p>Waiting for verification...</p>
}How it works
useSendMagicLinksends an email with the configured magic link and returns anotpIdplus anotpEncryptionTargetBundle.- The user clicks the link and lands back in your app with a
codequery parameter. useVerifyMagicLinkverifies theotpId,otpEncryptionTargetBundle, andcode.- After verification succeeds, the SDK creates a session and connects the ZeroDev Wagmi connector.
Notes
- Store the
otpIdandotpEncryptionTargetBundlesomewhere the verify route can read them.sessionStorageis enough for a same-browser flow. - If users open the link on a different device or browser, you need an app-specific way to recover both values.
- Magic link email behavior depends on your project email configuration in the dashboard.
- For full hook options and return values, see
useSendMagicLinkanduseVerifyMagicLink.
Next steps
- Send a transaction
- Sign a message
- Integrate other login methods: Passkeys, Email OTP, or Google OAuth