# Magic Link (/wallets/auth/magic-link)

> For the complete documentation index, see [llms.txt](/llms.txt)



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.

<div className="fd-steps">
  <div className="fd-step">
    ## Configure the magic-link URL [#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:

    ```txt
    http://localhost:3000/verify
    ```

    Make sure the redirect origin is also added to the project's ACL allowlist in the ZeroDev dashboard.
  </div>

  <div className="fd-step">
    ## Send the magic link [#send-the-magic-link]

    Call `useSendMagicLink` from your login page. Store the returned `otpId` and `otpEncryptionTargetBundle` so the verify page can complete authentication.

    ```tsx
    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>
      )
    }
    ```
  </div>

  <div className="fd-step">
    ## Verify the link [#verify-the-link]

    On your verify route, read the `code` query parameter and the stored `otpId` and `otpEncryptionTargetBundle`, then call `useVerifyMagicLink`.

    ```tsx
    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>
    }
    ```
  </div>
</div>

## How it works [#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 [#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.
* After verification, the authenticated user's email is available from `useAuthenticators`: read `data?.emailContacts?.[0]?.email` after the wallet is connected.

## Next steps [#next-steps]

* [Send a transaction](/wallets/wallet-api/send-transaction)
* [Sign a message](/wallets/wallet-api/sign-message)
* Integrate other login methods: [Passkeys](/wallets/auth/passkeys), [Email OTP](/wallets/auth/email-otp), or [Google OAuth](/wallets/auth/google-oauth)
