> You are reading Better Auth documentation for `v1.8 (Beta)`. This is not the current stable release. APIs may differ from the latest stable version.

# Magic link (/docs/beta/plugins/magic-link)

Magic link plugin



Magic link or email link is a way to authenticate users without a password. When a user enters their email, a link is sent to their email. When the user clicks on the link, they are authenticated.

## Installation [#installation]

<Steps>
  <Step>
    ### Add the server Plugin [#add-the-server-plugin]

    Add the magic link plugin to your server:

    ```ts title="server.ts"
    import { betterAuth } from "better-auth";
    import { magicLink } from "better-auth/plugins";  // [!code highlight]

    export const auth = betterAuth({
        plugins: [
            magicLink({ // [!code highlight]
                sendMagicLink: async ({ email, token, url, metadata }, ctx) => { // [!code highlight]
                    // send email to user // [!code highlight]
                } // [!code highlight]
            }) // [!code highlight]
        ]
    })
    ```
  </Step>

  <Step>
    ### Add the client Plugin [#add-the-client-plugin]

    Add the magic link plugin to your client:

    ```ts title="auth-client.ts"
    import { createAuthClient } from "better-auth/client";
    import { magicLinkClient } from "better-auth/client/plugins"; // [!code highlight]

    export const authClient = createAuthClient({
        plugins: [
            magicLinkClient() // [!code highlight]
        ]
    });
    ```
  </Step>
</Steps>

## Usage [#usage]

### Sign In with Magic Link [#sign-in-with-magic-link]

To sign in with a magic link, you need to call `signIn.magicLink` with the user's email address. The `sendMagicLink` function is called to send the magic link to the user's email.

**Endpoint:** `POST /sign-in/magic-link`

### Client Side

```ts
const { data, error } = await authClient.signIn.magicLink({
    email: "user@email.com", // required, Email address to send the magic link.
    name: "my-name", // User display name. Only used if the user is registering for the first time.
    callbackURL: "/dashboard", // URL to redirect after magic link verification.
    newUserCallbackURL: "/welcome", // URL to redirect after new user signup.
    errorCallbackURL: "/error", // URL to redirect if an error happens on verification If only callbackURL is provided but without an `errorCallbackURL` then they will be redirected to the callbackURL with an `error` query parameter.
    metadata: { inviteId: "123" }, // Additional metadata forwarded to the sendMagicLink callback.
});
```

### Server Side

```ts
const data = await auth.api.signInMagicLink({
    body: {
        email: "user@email.com", // required, Email address to send the magic link.
        name: "my-name", // User display name. Only used if the user is registering for the first time.
        callbackURL: "/dashboard", // URL to redirect after magic link verification.
        newUserCallbackURL: "/welcome", // URL to redirect after new user signup.
        errorCallbackURL: "/error", // URL to redirect if an error happens on verification If only callbackURL is provided but without an `errorCallbackURL` then they will be redirected to the callbackURL with an `error` query parameter.
        metadata: { inviteId: "123" }, // Additional metadata forwarded to the sendMagicLink callback.
    },
    // Pass the current request headers so Better Auth can read and set cookies.
    headers: await headers(),
});
```

### Type Definition

```ts
type signInMagicLink = {
    /**
     * Email address to send the magic link. 
     */
    email: string = "user@email.com"
    /**
     * User display name. Only used if the user is registering for the first time. 
     */
    name?: string = "my-name"
    /**
     * URL to redirect after magic link verification. 
     */
    callbackURL?: string = "/dashboard"
    /**
     * URL to redirect after new user signup.
     */
    newUserCallbackURL?: string = "/welcome"
    /**
     * URL to redirect if an error happens on verification
     * If only callbackURL is provided but without an `errorCallbackURL` then they will be 
     * redirected to the callbackURL with an `error` query parameter.
     */
    errorCallbackURL?: string = "/error"
    /**
     * Additional metadata forwarded to the sendMagicLink callback.
     */
    metadata?: Record<string, any> = { inviteId: "123" }
}
```

<Callout>
  If the user has not signed up, unless `disableSignUp` is set to `true`, the user will be signed up automatically.
</Callout>

### Verify Magic Link [#verify-magic-link]

When you send the URL generated by the `sendMagicLink` function to a user, clicking the link will authenticate them and redirect them to the `callbackURL` specified in the `signIn.magicLink` function. If an error occurs, the user will be redirected to the `callbackURL` with an error query parameter.

<Callout type="warn">
  If no `callbackURL` is provided, the user will be redirected to the root URL.
</Callout>

<Callout>
  When the link verifies a pre-existing account whose email was never confirmed, any existing password on that account is removed and its sessions are revoked. The user is signed in through the link and can set a new password through password reset. This keeps email ownership, proven by the link, as the source of truth for the account.
</Callout>

If you want to handle the verification manually, (e.g, if you send the user a different URL), you can use the `verify` function.

**Endpoint:** `GET /magic-link/verify`

### Client Side

```ts
const { data, error } = await authClient.magicLink.verify({
    query: {
        token: "123456", // required, Verification token.
        callbackURL: "/dashboard", // URL to redirect after magic link verification, if not provided will return the session.
        newUserCallbackURL: "/welcome", // URL to redirect after new user signup.
        errorCallbackURL: "/error", // URL to redirect if an error happens on verification. If only callbackURL is provided but without an `errorCallbackURL` then they will be redirected to the callbackURL with an `error` query parameter.
    },
});
```

### Server Side

```ts
const data = await auth.api.magicLinkVerify({
    query: {
        token: "123456", // required, Verification token.
        callbackURL: "/dashboard", // URL to redirect after magic link verification, if not provided will return the session.
        newUserCallbackURL: "/welcome", // URL to redirect after new user signup.
        errorCallbackURL: "/error", // URL to redirect if an error happens on verification. If only callbackURL is provided but without an `errorCallbackURL` then they will be redirected to the callbackURL with an `error` query parameter.
    },
    // Pass the current request headers so Better Auth can read and set cookies.
    headers: await headers(),
});
```

### Type Definition

```ts
type magicLinkVerify = {
    /**
     * Verification token. 
     */
    token: string = "123456"
    /**
     * URL to redirect after magic link verification, if not provided will return the session. 
     */
    callbackURL?: string = "/dashboard"
    /**
     * URL to redirect after new user signup.
     */
    newUserCallbackURL?: string = "/welcome"
    /**
     * URL to redirect if an error happens on verification.
     * If only callbackURL is provided but without an `errorCallbackURL` then they will be
     * redirected to the callbackURL with an `error` query parameter.
     */
    errorCallbackURL?: string = "/error"
}
```

## Configuration Options [#configuration-options]

**sendMagicLink**: The `sendMagicLink` function is called when a user requests a magic link. It takes an object with the following properties:

* `email`: The email address of the user.
* `url`: The URL to be sent to the user. This URL contains the token.
* `token`: The token if you want to send the token with custom URL.
* `metadata`: Additional request metadata passed from `signIn.magicLink`.

and a `ctx` context object as the second parameter.

**expiresIn**: specifies the time in seconds after which the magic link will expire. The default value is `300` seconds (5 minutes).

**allowedAttempts** (deprecated): Each verification call now consumes the token atomically on the first attempt, so retries always fail with `?error=INVALID_TOKEN` regardless of this setting. The option is kept for source compatibility but ignored; multi-attempt redemption is no longer supported. Setting it to any value other than `1` emits a `console.warn` at startup (including `0`, which previously rejected immediately and now has no effect).

**disableSignUp**: If set to `true`, the user will not be able to sign up using the magic link. The default value is `false`.

**rateLimit**: Rate limit for the `/sign-in/magic-link` and `/magic-link/verify` endpoints. The default value is `{ window: 60, max: 5 }`. See [Rate Limit](/docs/beta/concepts/rate-limit) for details.

**generateToken**: The `generateToken` function is called to generate a token which is used to uniquely identify the user. The default value is a random string. There is one parameter:

* `email`: The email address of the user.

<Callout type="warn">
  When using `generateToken`, ensure that the returned string is hard to guess
  because it is used to verify who someone actually is in a confidential way. By
  default, we return a long and cryptographically secure string.
</Callout>

**storeToken**: Controls how Better Auth transforms the Magic Link token before creating its verification identifier. The default value is `"plain"`.

The `storeToken` option can be one of the following:

* `"plain"`: The token is not transformed before Better Auth creates its verification identifier.
* `"hashed"`: The token is hashed using the default hasher before the identifier is created.
* `{ type: "custom-hasher", hash: (token: string) => Promise<string> }`: Your hasher transforms the token before the identifier is created.

Better Auth adds `magic-link:` to the transformed token before applying the global [`verification.storeIdentifier`](/docs/beta/reference/options#verification) setting. The token value sent in the link and to `sendMagicLink` is unchanged. Custom `storeToken` hashers receive the original token; they do not need to add the prefix.

The storage backend itself is controlled by the global [`verification`](/docs/beta/reference/options#verification) config. If you configure `secondaryStorage`, magic link verification records can be stored there instead of the database.

When upgrading to purpose-prefixed verification identifiers, request new Magic Links for any links issued before the upgrade. Previously issued links cannot be redeemed by the new version and fail with `INVALID_TOKEN`. Existing users and accounts do not need a data migration. Upgrade all servers that share verification storage together so they agree on which links can be redeemed.

<Callout type="warn">
  When `secondaryStorage` backs verification (`verification.storeInDatabase: false`), the atomic single-use guarantee requires your secondary storage to expose `getAndDelete` (Redis `GETDEL`, KV `getAndDelete`). Better Auth does not fall back to separate `get` and `delete` operations for verification consumes. Multi-instance deployments using secondary-storage verification must configure a backend that implements `getAndDelete`.
</Callout>

