Skip to content

Magic Link

Authenticate with an email 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/verify

Make 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('zerodevMagicLinkOtpId', result.otpId)
          sessionStorage.setItem(
            'zerodevMagicLinkBundle',
            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('zerodevMagicLinkOtpId')
    const otpEncryptionTargetBundle = sessionStorage.getItem(
      'zerodevMagicLinkBundle',
    )
 
    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

  1. useSendMagicLink sends an email with the configured magic link and returns an otpId plus an otpEncryptionTargetBundle.
  2. The user clicks the link and lands back in your app with a code query parameter.
  3. useVerifyMagicLink verifies the otpId, otpEncryptionTargetBundle, and code.
  4. After verification succeeds, the SDK creates a session and connects the ZeroDev Wagmi connector.

Notes

  • Store the otpId and otpEncryptionTargetBundle somewhere the verify route can read them. sessionStorage is 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.

Next steps