---
title: Set Up Passwordless Authentication
description: Integrate email or SMS passwordless authentication with magic links, OTPs, or both using prebuilt or custom UI.
sidebar:
  label: Initial Setup
  order: 20
---

## Passwordless integration summary

- Configure the Passwordless and Session recipes on both the frontend and backend, then add the selected UI and authentication routes.
- Set `contactMethod` to email, phone, or email-or-phone delivery.
- Set `flowType` to `MAGIC_LINK`, `USER_INPUT_CODE` for OTPs, or `USER_INPUT_CODE_AND_MAGIC_LINK` for both.
- Configure email or SMS delivery. Test sign-in, resend, expired or invalid credentials, and session creation.

<Prompt
  description="Implement passwordless authentication with the right contact and flow options."
  actions={["copy"]}
>
Implement SuperTokens passwordless authentication in this application. Inspect the existing frontend, backend, recipes, and routing first. Ask whether users should authenticate through email, SMS, or both, and whether the flow should use magic links, OTPs, or both. Configure the frontend and backend Passwordless and Session recipes, the selected UI, auth routes, and email or SMS delivery. Preserve existing conventions, keep credentials in environment variables, and validate sign-in, resend, expiry, and session behavior.
</Prompt>

## Overview

This page shows you how to add the **Passwordless** `recipe` to your project.
The tutorial creates a login flow, rendered by either the **Prebuilt UI** components or by your own **Custom UI**.

### Terminology

Before going into the actual steps lets first talk about two terms that influence how you configure the **Passwordless** recipe.
- **Contact Method**: This defines how the user receives the credentials from your app. You can choose between `email`, `phone number` or both (the user has to choose one during the login flow).
- **Flow Type**: This is the credential type used for authentication. You can choose **Magic Link**, **OTP** (One-Time Password), or both. The combined flow sends both credentials and the user can complete authentication with either one.

## Steps

<UITypeSwitch />

<VariantContent storageKey="ui-type" value="prebuilt">

### 1. Initialize the frontend SDK

<DependentContent passive group="frontend-prebuilt-ui">
<ContentOption title="Reactjs" value="reactjs">
#### 1.1 Add the `Passwordless` recipe in your main configuration file.
</ContentOption>
<ContentOption title="Angular" value="angular">
Add the `Passwordless` recipe in your `AuthComponent`.
</ContentOption>
<ContentOption title="Vue" value="vue">
Add the `Passwordless` recipe in your `AuthView` file.
</ContentOption>
</DependentContent>

<CodeGroup group="frontend-prebuilt-ui">
<Tab title="Reactjs" value="reactjs">
```tsx
import React from "react";

import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react";
import Passwordless from "supertokens-auth-react/recipe/passwordless";
import Session from "supertokens-auth-react/recipe/session";

SuperTokens.init({
  appInfo: {
    // learn more about this on https://supertokens.com/docs/references/frontend-sdks/reference#sdk-configuration
    appName: "<YOUR_APP_NAME>",
    apiDomain: "<YOUR_API_DOMAIN>",
    websiteDomain: "<YOUR_WEBSITE_DOMAIN>",
    apiBasePath: "/auth",
    websiteBasePath: "/auth",
  },
  recipeList: [
    Passwordless.init({
      contactMethod: "EMAIL",
    }),
    Session.init(),
  ],
});
```
</Tab>
<Tab title="Angular" value="angular">
```tsx title="/app/auth/auth.component.ts" check=false reason="This example omits surrounding application and SuperTokens configuration."
import { Component, OnDestroy, AfterViewInit, Renderer2, Inject } from "@angular/core";
import { DOCUMENT } from "@angular/common";

@Component({
  selector: "app-auth",
  template: '<div id="supertokensui"></div>',
})
export class AuthComponent implements OnDestroy, AfterViewInit {
  constructor(
    private renderer: Renderer2,
    @Inject(DOCUMENT) private document: Document,
  ) {}

  ngAfterViewInit() {
    this.loadScript("https://cdn.jsdelivr.net/gh/supertokens/prebuiltui@vX.Y.Z/build/static/js/main.test.js");
  }

  ngOnDestroy() {
    // Remove the script when the component is destroyed
    const script = this.document.getElementById("supertokens-script");
    if (script) {
      script.remove();
    }
  }

  private loadScript(src: string) {
    const script = this.renderer.createElement("script");
    script.type = "text/javascript";
    script.src = src;
    script.id = "supertokens-script";
    script.onload = () => {
      supertokensUIInit({
        appInfo: {
          // learn more about this on https://supertokens.com/docs/references/frontend-sdks/reference#sdk-configuration
          appName: "<YOUR_APP_NAME>",
          apiDomain: "<YOUR_API_DOMAIN>",
          websiteDomain: "<YOUR_WEBSITE_DOMAIN>",
          apiBasePath: "/auth",
          websiteBasePath: "/auth",
        },
        recipeList: [
          supertokensUIPasswordless.init({
            contactMethod: "EMAIL",
          }),
          supertokensUISession.init(),
        ],
      });
    };
    this.renderer.appendChild(this.document.body, script);
  }
}
```
</Tab>
<Tab title="Vue" value="vue">
```html
<script lang="ts">
  import { defineComponent, onMounted, onUnmounted } from "vue";
  export default defineComponent({
    setup() {
      const loadScript = (src: string) => {
        const script = document.createElement("script");
        script.type = "text/javascript";
        script.src = src;
        script.id = "supertokens-script";
        script.onload = () => {
          supertokensUIInit({
            appInfo: {
              // learn more about this on https://supertokens.com/docs/references/frontend-sdks/reference#sdk-configuration
              appName: "<YOUR_APP_NAME>",
              apiDomain: "<YOUR_API_DOMAIN>",
              websiteDomain: "<YOUR_WEBSITE_DOMAIN>",
              apiBasePath: "/auth",
              websiteBasePath: "/auth",
            },
            recipeList: [
              supertokensUIPasswordless.init({
                contactMethod: "EMAIL",
              }),
              supertokensUISession.init(),
            ],
          });
        };
        document.body.appendChild(script);
      };

      onMounted(() => {
        loadScript("https://cdn.jsdelivr.net/gh/supertokens/prebuiltui@vX.Y.Z/build/static/js/main.test.js");
      });

      onUnmounted(() => {
        const script = document.getElementById("supertokens-script");
        if (script) {
          script.remove();
        }
      });
    },
  });
</script>

<template>
  <div id="supertokensui" />
</template>
```
</Tab>
</CodeGroup>

<DependentContent passive group="frontend-prebuilt-ui">
<ContentOption title="Reactjs" value="reactjs">
#### 1.2 Include the pre-built UI components in your application.

To render the **Pre-Built UI** inside your application, you need to specify which routes show the authentication components.
The **React SDK** uses [**React Router**](https://reactrouter.com/en/main) under the hood to achieve this.
Based on whether you already use this package or not in your project, there are two different ways of configuring the routes.
</ContentOption>
</DependentContent>

<CodeGroup passive group="frontend-prebuilt-ui" secondaryControls="react-router">
<Tab title="Reactjs" value="reactjs">
<DependentContent group="react-router" label="Do you use react-router-dom?">
<ContentOption title="With React Router" value="yes">
```tsx
import React from "react";
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";

import { PasswordlessPreBuiltUI } from "supertokens-auth-react/recipe/passwordless/prebuiltui";
import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react";
import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui";
import * as reactRouterDom from "react-router-dom";

class App extends React.Component {
  render() {
    return (
      <SuperTokensWrapper>
        <BrowserRouter>
          <Routes>
            {/*This renders the login UI on the /auth route*/}
            {getSuperTokensRoutesForReactRouterDom(reactRouterDom, [PasswordlessPreBuiltUI])}
            {/*Your app routes*/}
          </Routes>
        </BrowserRouter>
      </SuperTokensWrapper>
    );
  }
}
```
</ContentOption>
<ContentOption title="Without React Router" value="no">
```tsx
import React from "react";
import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react";
import { PasswordlessPreBuiltUI } from "supertokens-auth-react/recipe/passwordless/prebuiltui";
import { canHandleRoute, getRoutingComponent } from "supertokens-auth-react/ui";

class App extends React.Component {
  render() {
    if (canHandleRoute([PasswordlessPreBuiltUI])) {
      // This renders the login UI on the /auth route
      return getRoutingComponent([PasswordlessPreBuiltUI]);
    }

    return <SuperTokensWrapper>{/*Your app*/}</SuperTokensWrapper>;
  }
}
```
</ContentOption>
</DependentContent>
</Tab>
</CodeGroup>

<DependentContent passive group="frontend-prebuilt-ui">
<ContentOption title="Reactjs" value="reactjs">
<DependentContent passive group="react-router">
<ContentOption title="With React Router" value="yes">
:::note[If you are using `useRoutes`, `createBrowserRouter` or have routes defined in a different file, you need to adjust the code sample.]
Please see [this issue](https://github.com/supertokens/supertokens-auth-react/issues/581#issuecomment-1246998493) for further details.
</ContentOption>
</DependentContent>
</ContentOption>
</DependentContent>

<CodeGroup passive group="frontend-prebuilt-ui">
<Tab title="Reactjs" value="reactjs">
<DependentContent group="react-router" label="Do you use react-router-dom?">
<ContentOption title="With React Router" value="yes">
```tsx
import React from "react";

import { BrowserRouter, useRoutes } from "react-router-dom";
import SuperTokens, { SuperTokensWrapper } from "supertokens-auth-react";
import { getSuperTokensRoutesForReactRouterDom } from "supertokens-auth-react/ui";
import * as reactRouterDom from "react-router-dom";

function AppRoutes() {
  const authRoutes = getSuperTokensRoutesForReactRouterDom(reactRouterDom, [
    /* Add your UI recipes here e.g. EmailPasswordPrebuiltUI, PasswordlessPrebuiltUI, ThirdPartyPrebuiltUI */
  ]);

  const routes = useRoutes([
    ...authRoutes.map((route) => route.props),
    // Include the rest of your app routes
  ]);

  return routes;
}

function App() {
  return (
    <SuperTokensWrapper>
      <BrowserRouter>
        <AppRoutes />
      </BrowserRouter>
    </SuperTokensWrapper>
  );
}
```
</ContentOption>
</DependentContent>
</Tab>
</CodeGroup>

<DependentContent passive group="frontend-prebuilt-ui">
<ContentOption title="Reactjs" value="reactjs">
<DependentContent passive group="react-router">
<ContentOption title="With React Router" value="yes">
:::
</ContentOption>
</DependentContent>
</ContentOption>
</DependentContent>

### 2. Initialize the backend SDK

You need to initialize the **Backend SDK** alongside the code that starts your server.
The init call includes [configuration details](/references/backend-sdks/reference#sdk-configuration) for your app. It specifies how the backend connects to the **SuperTokens Core**, as well as the **Recipes** used in your setup.

For the **Passwordless** recipe, you also need to specify the `flowType` and `contactMethod`.
Click one of the options from the next form and the code snippet updates.

<CodeGroup group="backend-language">
<Tab title="Node.js" value="nodejs">
```tsx title="Backend SDK Init"
import supertokens from "supertokens-node";
import Session from "supertokens-node/recipe/session";
import Passwordless from "supertokens-node/recipe/passwordless";

supertokens.init({
  // Replace this with the framework you are using
  framework: "express",
  supertokens: {
    // We use try.supertokens for demo purposes.
    // At the end of the tutorial we will show you how to create
    // your own SuperTokens core instance and then update your config.
    connectionURI: "https://try.supertokens.io",
    // apiKey: <YOUR_API_KEY>
  },
  appInfo: {
    // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration
    appName: "<YOUR_APP_NAME>",
    apiDomain: "<YOUR_API_DOMAIN>",
    websiteDomain: "<YOUR_WEBSITE_DOMAIN>",
    apiBasePath: "/auth",
    websiteBasePath: "/auth",
  },
  recipeList: [
    Passwordless.init({
      flowType: "MAGIC_LINK",
      contactMethod: "EMAIL",
    }),
    Session.init(),
  ],
});
```
</Tab>
<Tab title="Python" value="python">
```python title="Backend SDK Init"
from supertokens_python import init, InputAppInfo, SupertokensConfig
from supertokens_python.recipe import passwordless, session
from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig

init(
    app_info=InputAppInfo(
        app_name="<YOUR_APP_NAME>",
        api_domain="<YOUR_API_DOMAIN>",
        website_domain="<YOUR_WEBSITE_DOMAIN>",
        api_base_path="/auth",
        website_base_path="/auth"
    ),
    supertokens_config=SupertokensConfig(
        # We use try.supertokens for demo purposes.
        # At the end of the tutorial we will show you how to create
        # your own SuperTokens core instance and then update your config.
        connection_uri="https://try.supertokens.io",
        # api_key: <YOUR_API_KEY>
    ),
    framework='fastapi',
    recipe_list=[
        session.init(), # initializes session features
        passwordless.init(
            flow_type="MAGIC_LINK",
            contact_config=ContactEmailOnlyConfig()
        )
    ],
    mode='asgi' # use wsgi if you are running using gunicorn
)
```
</Tab>
<Tab title="Go" value="go">
```go title="Backend SDK Init"
import (
	"github.com/supertokens/supertokens-golang/recipe/session"
	"github.com/supertokens/supertokens-golang/recipe/passwordless"
	"github.com/supertokens/supertokens-golang/recipe/passwordless/plessmodels"
	"github.com/supertokens/supertokens-golang/supertokens"
)

func main() {
  apiBasePath := "/auth"
  websiteBasePath := "/auth"
  err := supertokens.Init(supertokens.TypeInput{
    Supertokens: &supertokens.ConnectionInfo{
          // We use try.supertokens for demo purposes.
          // At the end of the tutorial we will show you how to create
          // your own SuperTokens core instance and then update your config.
          ConnectionURI: "https://try.supertokens.io",
          // APIKey: <YOUR_API_KEY>
    },
    AppInfo: supertokens.AppInfo{
            AppName: "<YOUR_APP_NAME>",
            APIDomain: "<YOUR_API_DOMAIN>",
            WebsiteDomain: "<YOUR_WEBSITE_DOMAIN>",
            APIBasePath: &apiBasePath,
            WebsiteBasePath: &websiteBasePath,
    },
    RecipeList: []supertokens.Recipe{
      passwordless.Init(plessmodels.TypeInput{
                FlowType: "MAGIC_LINK",
                ContactMethodEmail: plessmodels.ContactMethodEmailConfig{Enabled: true},
            }),
      session.Init(nil), // initializes session features
    },
  })

	if err != nil {
		panic(err.Error())
	}
}
```
</Tab>
</CodeGroup>

</VariantContent>

<VariantContent storageKey="ui-type" value="custom">

### 1. Initialize the frontend SDK

Call the SDK init function at the start of your application.
The invocation includes the [main configuration details](/references/frontend-sdks/reference#sdk-configuration), as well as the **recipes** that you use in your setup.

<DependentContent passive group="frontend-custom-ui">
<ContentOption title="Web" value="web">
<DependentContent passive group="install-method">
<ContentOption title="Script tag" value="script-tag">
First, you need to add the recipe script tag.
</ContentOption>
</DependentContent>
</ContentOption>
<ContentOption title="Mobile" value="mobile">
<DependentContent passive group="mobile-frameworks">
<ContentOption title="Android" value="android">
Add the `SuperTokens.init` function call at the start of your application.
</ContentOption>
</DependentContent>
</ContentOption>
</DependentContent>

<CodeGroup group="frontend-custom-ui">
<Tab title="Web" value="web">
<DependentContent group="install-method" label="Installation method">
<ContentOption title="npm" value="npm">
```tsx
import SuperTokens from "supertokens-web-js";
import Session from "supertokens-web-js/recipe/session";
import Passwordless from "supertokens-web-js/recipe/passwordless";

SuperTokens.init({
  appInfo: {
    apiDomain: "<YOUR_API_DOMAIN>",
    apiBasePath: "/auth",
    appName: "...",
  },
  recipeList: [Session.init(), Passwordless.init()],
});
```
</ContentOption>
<ContentOption title="Script tag" value="script-tag">
```html
<script src="https://cdn.jsdelivr.net/gh/supertokens/supertokens-web-js@vX.Y.Z/bundle/passwordless.test.js"></script>
```
</ContentOption>
</DependentContent>
</Tab>
<Tab title="Mobile" value="mobile">
<DependentContent group="mobile-frameworks" label="Mobile framework">
<ContentOption title="ReactNative" value="reactnative">
```tsx
import SuperTokens from "supertokens-react-native";

SuperTokens.init({
  apiDomain: "<YOUR_API_DOMAIN>",
  apiBasePath: "/auth",
});
```
</ContentOption>
<ContentOption title="Android" value="android">
```kotlin
import android.app.Application
import com.supertokens.session.SuperTokens

class MainApplication: Application() {
    override fun onCreate() {
        super.onCreate()

        SuperTokens.Builder(this, "<YOUR_API_DOMAIN>")
            .apiBasePath("/auth")
            .build()
    }
}
```
</ContentOption>
<ContentOption title="iOS" value="ios">
```swift
import UIKit
import SuperTokensIOS

fileprivate class ApplicationDelegate: UIResponder, UIApplicationDelegate {

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        do {
            try SuperTokens.initialize(
                apiDomain: "<YOUR_API_DOMAIN>",
                apiBasePath: "/auth"
            )
        } catch SuperTokensError.initError(let message) {
            // TODO: Handle initialization error
        } catch {
            // Some other error
        }

        return true
    }

}
```
</ContentOption>
<ContentOption title="Flutter" value="flutter">
```dart
import 'package:supertokens_flutter/supertokens.dart';

void main() {
    SuperTokens.init(
        apiDomain: "<YOUR_API_DOMAIN>",
        apiBasePath: "/auth",
    );
}
```
</ContentOption>
</DependentContent>
</Tab>
</CodeGroup>

<DependentContent passive group="frontend-custom-ui">
<ContentOption title="Web" value="web">
<DependentContent passive group="install-method">
<ContentOption title="Script tag" value="script-tag">
You can initialize the SDK
</ContentOption>
</DependentContent>
</ContentOption>
</DependentContent>

<CodeGroup passive group="frontend-custom-ui">
<Tab title="Web" value="web">
<DependentContent group="install-method" label="Installation method">
<ContentOption title="Script tag" value="script-tag">
```tsx check=false reason="This example omits surrounding application and SuperTokens configuration."
supertokens.init({
  appInfo: {
    apiDomain: "<YOUR_API_DOMAIN>",
    apiBasePath: "/auth",
    appName: "...",
  },
  recipeList: [supertokensSession.init(), supertokensPasswordless.init()],
});
```
</ContentOption>
</DependentContent>
</Tab>
</CodeGroup>

### 2. Add the login UI

Follow the section that matches your configured `flowType`. For `USER_INPUT_CODE_AND_MAGIC_LINK`, one create-code request sends both a magic link and an OTP. Use the shared create and resend behavior from steps 2.1 and 2.2, then implement both consumption paths so the user can complete either one.

#### Magic Link

The following section shows you what aspects you need to cover to implement the UI for a `Magic Link` flow.
The same flow applies during either sign up or sign in.
This guide shows you how to determine if the system creates a new user in the next steps.

##### 2.1 Sending the Magic link

You need to add a form that asks the user for their email address or phone number.
When the user submits the form, you need to call the following API to create and send them a **Magic Link**.

:::info[You configure the contact method on the next page, where you discuss the process of adding the `SDK` to your backend app.]

:::

<DependentContent passive group="frontend-custom-ui">
<ContentOption title="Mobile" value="mobile">
For email based login
</ContentOption>
</DependentContent>

<CodeGroup group="frontend-custom-ui">
<Tab title="Web" value="web">
<DependentContent group="install-method" label="Installation method">
<ContentOption title="npm" value="npm">
```tsx
import { createCode } from "supertokens-web-js/recipe/passwordless";

async function sendMagicLink(email: string) {
  try {
    let response = await createCode({
      email,
    });
    /**
         * For phone number, use this:

            let response = await createCode({
                phoneNumber: "+1234567890"
            });

        */

    if (response.status === "SIGN_IN_UP_NOT_ALLOWED") {
      // the reason string is a user friendly message
      // about what went wrong. It can also contain a support code which users
      // can tell you so you know why their sign in / up was not allowed.
      window.alert(response.reason);
    } else {
      // Magic link sent successfully.
      window.alert("Please check your email for the magic link");
    }
  } catch (err: any) {
    if (err.isSuperTokensGeneralError === true) {
      // this may be a custom error message sent from the API by you,
      // or if the input email / phone number is not valid.
      window.alert(err.message);
    } else {
      window.alert("Oops! Something went wrong.");
    }
  }
}
```
</ContentOption>
<ContentOption title="Script tag" value="script-tag">
```tsx check=false reason="This example omits surrounding application and SuperTokens configuration."
async function sendMagicLink(email: string) {
  try {
    let response = await supertokensPasswordless.createCode({
      email,
    });
    /**
         * For phone number, use this:

            let response = await supertokensPasswordless.createCode({
                phoneNumber: "+1234567890"
            });

        */

    if (response.status === "SIGN_IN_UP_NOT_ALLOWED") {
      // the reason string is a user friendly message
      // about what went wrong. It can also contain a support code which users
      // can tell you so you know why their sign in / up was not allowed.
      window.alert(response.reason);
    } else {
      // Magic link sent successfully.
      window.alert("Please check your email for the magic link");
    }
  } catch (err: any) {
    if (err.isSuperTokensGeneralError === true) {
      // this may be a custom error message sent from the API by you,
      // or if the input email / phone number is not valid.
      window.alert(err.message);
    } else {
      window.alert("Oops! Something went wrong.");
    }
  }
}
```
</ContentOption>
</DependentContent>
</Tab>
<Tab title="Mobile" value="mobile">
```bash
curl --location --request POST '<YOUR_API_DOMAIN>/auth/public/signinup/code' \
--header 'Content-Type: application/json; charset=utf-8' \
--data-raw '{
  "email": "johndoe@gmail.com"
}'
```
</Tab>
</CodeGroup>

<DependentContent passive group="frontend-custom-ui">
<ContentOption title="Mobile" value="mobile">
For phone number based login
</ContentOption>
</DependentContent>

<CodeGroup passive group="frontend-custom-ui">
<Tab title="Mobile" value="mobile">
```bash
curl --location --request POST '<YOUR_API_DOMAIN>/auth/public/signinup/code' \
--header 'Content-Type: application/json; charset=utf-8' \
--data-raw '{
  "phoneNumber": "+1234567890"
}'
```
</Tab>
</CodeGroup>

<DependentContent passive group="frontend-custom-ui">
<ContentOption title="Mobile" value="mobile">
The response body from the API call has a `status` property in it:

- `status: "OK"`: This means that the magic link was successfully sent.
- `status: "GENERAL_ERROR"`: This is possible if you have overridden the backend API to send back a custom error message which should display on the frontend, or if the input email or password failed the backend validation logic.
- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during multi-factor authentication (MFA). The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed.

The response from the API call is the following object (in case of `status: "OK"`):
</ContentOption>
</DependentContent>

<CodeGroup passive group="frontend-custom-ui">
<Tab title="Mobile" value="mobile">
```typescript check=false reason="This block documents the response shape rather than executable code."
{
    status: "OK";
    deviceId: string;
    preAuthSessionId: string;
    flowType: "USER_INPUT_CODE" | "MAGIC_LINK" | "USER_INPUT_CODE_AND_MAGIC_LINK";
}
```
</Tab>
</CodeGroup>

<DependentContent passive group="frontend-custom-ui">
<ContentOption title="Mobile" value="mobile">
You want to save the `deviceId` and `preAuthSessionId` on the frontend storage. These are useful to:

- Resend a new magic link.
- Detect if the user has already sent a magic link before or if this is an entirely new login attempt. This distinction can be important if you have different UI for these two states. For example, if this info already exists, you do not want to show the user an input box to enter their email / phone, and instead want to show them the resend link button.
</ContentOption>
</DependentContent>

##### 2.2 Resending a magic link

After sending the initial magic link to the user, you may want to display a resend button to them.
When the user clicks on this button, you should call the following API

<CodeGroup group="frontend-custom-ui">
<Tab title="Web" value="web">
<DependentContent group="install-method" label="Installation method">
<ContentOption title="npm" value="npm">
```tsx
import { resendCode, clearLoginAttemptInfo } from "supertokens-web-js/recipe/passwordless";

async function resendMagicLink() {
  try {
    let response = await resendCode();

    if (response.status === "RESTART_FLOW_ERROR") {
      // this can happen if the user has already successfully logged in into
      // another device whilst also trying to login to this one.

      // we clear the login attempt info that was added when the createCode function
      // was called - so that if the user does a page reload, they will now see the
      // enter email / phone UI again.
      await clearLoginAttemptInfo();
      window.alert("Login failed. Please try again");
      window.location.assign("/auth");
    } else {
      // Magic link resent successfully.
      window.alert("Please check your email for the magic link");
    }
  } catch (err: any) {
    if (err.isSuperTokensGeneralError === true) {
      // this may be a custom error message sent from the API by you.
      window.alert(err.message);
    } else {
      window.alert("Oops! Something went wrong.");
    }
  }
}
```
</ContentOption>
<ContentOption title="Script tag" value="script-tag">
```tsx check=false reason="This example omits surrounding application and SuperTokens configuration."
async function resendMagicLink() {
  try {
    let response = await supertokensPasswordless.resendCode();

    if (response.status === "RESTART_FLOW_ERROR") {
      // this can happen if the user has already successfully logged in into
      // another device whilst also trying to login to this one.

      // we clear the login attempt info that was added when the createCode function
      // was called - so that if the user does a page reload, they will now see the
      // enter email / phone UI again.
      await supertokensPasswordless.clearLoginAttemptInfo();
      window.alert("Login failed. Please try again");
      window.location.assign("/auth");
    } else {
      // Magic link resent successfully.
      window.alert("Please check your email for the magic link");
    }
  } catch (err: any) {
    if (err.isSuperTokensGeneralError === true) {
      // this may be a custom error message sent from the API by you.
      window.alert(err.message);
    } else {
      window.alert("Oops! Something went wrong.");
    }
  }
}
```
</ContentOption>
</DependentContent>
</Tab>
<Tab title="Mobile" value="mobile">
```bash
curl --location --request POST '<YOUR_API_DOMAIN>/auth/public/signinup/code/resend' \
--header 'Content-Type: application/json; charset=utf-8' \
--data-raw '{
  "deviceId": "...",
  "preAuthSessionId": "...."
}'
```
</Tab>
</CodeGroup>

<DependentContent passive group="frontend-custom-ui">
<ContentOption title="Mobile" value="mobile">
The response body from the API call has a `status` property in it:

- `status: "OK"`: This means that the magic link was successfully sent.
- `status: "RESTART_FLOW_ERROR"`: This can happen if the user has already successfully logged in into another device whilst also trying to login to this one. You want to take the user back to the login screen where they can enter their email / phone number again. Be sure to remove the stored `deviceId` and `preAuthSessionId` from the frontend storage.
- `status: "GENERAL_ERROR"`: This is possible if you have overridden the backend API to send back a custom error message which should display on the frontend.
</ContentOption>
</DependentContent>

##### How to detect if the initial OTP has been sent

If you are building the send and enter OTP interfaces on the same page, you might run into an issue when the user refreshes the page.
To prevent this you need a way to know which UI to show.

<DependentContent passive group="frontend-custom-ui">
<ContentOption title="Mobile" value="mobile">
Since you save the `preAuthSessionId` and `deviceId` after sending the initial magic link, you can know if the user is on either **step 2.1** or **step 2.2**. Check if these tokens are on the device.

If they aren't, you should follow **step 2.1**, else follow **step 2.2**.

:::note[You need to clear these tokens if:]

- the user navigates away from the **step 2.2** page
- you get a `RESTART_FLOW_ERROR` at any point in time from an API call
- the user has successfully logged in.
:::
</ContentOption>
</DependentContent>

<CodeGroup group="frontend-custom-ui">
<Tab title="Web" value="web">
<DependentContent group="install-method" label="Installation method">
<ContentOption title="npm" value="npm">
```tsx
import { getLoginAttemptInfo } from "supertokens-web-js/recipe/passwordless";

async function hasInitialMagicLinkBeenSent() {
  return (await getLoginAttemptInfo()) !== undefined;
}
```
</ContentOption>
<ContentOption title="Script tag" value="script-tag">
```tsx check=false reason="This example omits surrounding application and SuperTokens configuration."
async function hasInitialMagicLinkBeenSent() {
  return (await supertokensPasswordless.getLoginAttemptInfo()) !== undefined;
}
```
</ContentOption>
</DependentContent>
</Tab>
<Tab title="Mobile" value="mobile">

</Tab>
</CodeGroup>

<DependentContent passive group="frontend-custom-ui">
<ContentOption title="Web" value="web">
If `hasInitialMagicLinkBeenSent` returns `true`, it means that the user has already sent the initial magic link to themselves, and you can show the resend link UI. Else show a form asking them to enter their email / phone number.
</ContentOption>
</DependentContent>

##### 2.3 Consuming the magic link

When a user clicks on a magic link, you first need to know if the action came from the same browser/device as the one that started the flow.
To do this you ca use this code sample.

<DependentContent passive group="frontend-custom-ui">
<ContentOption title="Mobile" value="mobile">
Since you save the `preAuthSessionId` and `deviceId`, you can check if they exist on the app. If they do, then it's the same device that the user has opened the link on, else it's a different device.
</ContentOption>
</DependentContent>

<CodeGroup group="frontend-custom-ui">
<Tab title="Web" value="web">
<DependentContent group="install-method" label="Installation method">
<ContentOption title="npm" value="npm">
```tsx
import { getLoginAttemptInfo } from "supertokens-web-js/recipe/passwordless";

async function isThisSameBrowserAndDevice() {
  return (await getLoginAttemptInfo()) !== undefined;
}
```
</ContentOption>
<ContentOption title="Script tag" value="script-tag">
```tsx check=false reason="This example omits surrounding application and SuperTokens configuration."
async function isThisSameBrowserAndDevice() {
  return (await supertokensPasswordless.getLoginAttemptInfo()) !== undefined;
}
```
</ContentOption>
</DependentContent>
</Tab>
<Tab title="Mobile" value="mobile">

</Tab>
</CodeGroup>

:::note[Add a intermediate step if the user came from a different device.]

:::

If the user clicked on a link from a different device, you need to show some kind of an intermediate UI.
This is to protect against email clients opening the magic link on their servers and consuming the link.

The page should require additional user interaction before consuming the magic link.
For example, you could show a button with the following text: `Click here to login into this device`.
On click, you can consume the magic link to log the user into that device.

With this understanding of how to avoid potential errors, proceed with the actual instructions on how to authenticate with the magic link.

<DependentContent passive group="frontend-custom-ui">
<ContentOption title="Mobile" value="mobile">
You need to remove the `linkCode` and `preAuthSessionId` from the Magic link. For example, if the Magic link is
</ContentOption>
</DependentContent>

<CodeGroup group="frontend-custom-ui">
<Tab title="Web" value="web">
<DependentContent group="install-method" label="Installation method">
<ContentOption title="npm" value="npm">
```tsx
import { consumeCode, clearLoginAttemptInfo } from "supertokens-web-js/recipe/passwordless";

async function handleMagicLinkClicked() {
  try {
    let response = await consumeCode();

    if (response.status === "OK") {
      // we clear the login attempt info that was added when the createCode function
      // was called since the login was successful.
      await clearLoginAttemptInfo();
      if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) {
        // user sign up success
      } else {
        // user sign in success
      }
      window.location.assign("/home");
    } else {
      // this can happen if the magic link has expired or is invalid
      // or if it was denied due to security reasons in case of automatic account linking

      // we clear the login attempt info that was added when the createCode function
      // was called - so that if the user does a page reload, they will now see the
      // enter email / phone UI again.
      await clearLoginAttemptInfo();
      window.alert("Login failed. Please try again");
      window.location.assign("/auth");
    }
  } catch (err: any) {
    if (err.isSuperTokensGeneralError === true) {
      // this may be a custom error message sent from the API by you.
      window.alert(err.message);
    } else {
      window.alert("Oops! Something went wrong.");
    }
  }
}
```
</ContentOption>
<ContentOption title="Script tag" value="script-tag">
```tsx check=false reason="This example omits surrounding application and SuperTokens configuration."
async function handleMagicLinkClicked() {
  try {
    let response = await supertokensPasswordless.consumeCode();

    if (response.status === "OK") {
      // we clear the login attempt info that was added when the createCode function
      // was called since the login was successful.
      await supertokensPasswordless.clearLoginAttemptInfo();
      if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) {
        // user sign up success
      } else {
        // user sign in success
      }
      window.location.assign("/home");
    } else {
      // this can happen if the magic link has expired or is invalid
      // or if it was denied due to security reasons in case of automatic account linking

      // we clear the login attempt info that was added when the createCode function
      // was called - so that if the user does a page reload, they will now see the
      // enter email / phone UI again.
      await supertokensPasswordless.clearLoginAttemptInfo();
      window.alert("Login failed. Please try again");
      window.location.assign("/auth");
    }
  } catch (err: any) {
    if (err.isSuperTokensGeneralError === true) {
      // this may be a custom error message sent from the API by you.
      window.alert(err.message);
    } else {
      window.alert("Oops! Something went wrong.");
    }
  }
}
```
</ContentOption>
</DependentContent>
</Tab>
<Tab title="Mobile" value="mobile">
```text
https://example.com/auth/verify?preAuthSessionId=PyIwyA6VjdjNF5ggMV960rs3QXupRP2PEg2KcN5oi8s=#s4hxpBPnRC3xwBsCkFU228lh_CWe5HUBMRPowajsrgs=
```
</Tab>
</CodeGroup>

<DependentContent passive group="frontend-custom-ui">
<ContentOption title="Mobile" value="mobile">
Then the `preAuthSessionId` is the value of the query parameter `preAuthSessionId` (`PyIwyA6VjdjNF5ggMV960rs3QXupRP2PEg2KcN5oi8s=` in the example), and the `linkCode` is the part after the `#` (`s4hxpBPnRC3xwBsCkFU228lh_CWe5HUBMRPowajsrgs=` in the example).

We can then use these to call the consume API
</ContentOption>
</DependentContent>

<CodeGroup passive group="frontend-custom-ui">
<Tab title="Mobile" value="mobile">
```bash
curl --location --request POST '<YOUR_API_DOMAIN>/auth/<TENANT_ID>/signinup/code/consume' \
--header 'Content-Type: application/json; charset=utf-8' \
--data-raw '{
  "linkCode": "s4hxpBPnRC3xwBsCkFU228lh_CWe5HUBMRPowajsrgs=",
  "preAuthSessionId": "PyIwyA6VjdjNF5ggMV960rs3QXupRP2PEg2KcN5oi8s="
}'
```
</Tab>
</CodeGroup>

<DependentContent passive group="frontend-custom-ui">
<ContentOption title="Mobile" value="mobile">
:::info[Multi Tenancy]
Use the `tenantId` query parameter from the magic link as `<TENANT_ID>`. If the link has no `tenantId`, use `public`. The create, resend, and OTP-consume endpoints must use the same tenant path; replace `public` in those examples when authenticating another tenant.
:::

The response body from the API call has a `status` property in it:

- `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user.
- `status: "INCORRECT_USER_INPUT_CODE_ERROR" | "EXPIRED_USER_INPUT_CODE_ERROR" | "RESTART_FLOW_ERROR"`: These responses indicate that the Magic link was invalid or expired.
- `status: "GENERAL_ERROR"`: This is possible if you have overridden the backend API to send back a custom error message which should display on the frontend.
- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during multi-factor authentication (MFA). The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed.
</ContentOption>
</DependentContent>

#### OTP

The following section shows you what aspects you need to cover to implement the UI for a `OTP`, One-Time Password, flow.
The same flow applies during either sign up or sign in.
This guide shows you how to determine if you create a new user in the next steps.

##### 2.1 Creating and sending the OTP

You have to add a form that asks the user for their email address or phone number.
When the users submit the form you have to call the following API to create and send them an OTP.

<DependentContent passive group="frontend-custom-ui">
<ContentOption title="Mobile" value="mobile">
For email based login
</ContentOption>
</DependentContent>

<CodeGroup group="frontend-custom-ui">
<Tab title="Web" value="web">
<DependentContent group="install-method" label="Installation method">
<ContentOption title="npm" value="npm">
```tsx
import { createCode } from "supertokens-web-js/recipe/passwordless";

async function sendOTP(email: string) {
  try {
    let response = await createCode({
      email,
    });
    /**
         * For phone number, use this:

            let response = await createCode({
                phoneNumber: "+1234567890"
            });

        */

    if (response.status === "SIGN_IN_UP_NOT_ALLOWED") {
      // the reason string is a user friendly message
      // about what went wrong. It can also contain a support code which users
      // can tell you so you know why their sign in / up was not allowed.
      window.alert(response.reason);
    } else {
      // OTP sent successfully.
      window.alert("Please check your email for an OTP");
    }
  } catch (err: any) {
    if (err.isSuperTokensGeneralError === true) {
      // this may be a custom error message sent from the API by you,
      // or if the input email / phone number is not valid.
      window.alert(err.message);
    } else {
      window.alert("Oops! Something went wrong.");
    }
  }
}
```
</ContentOption>
<ContentOption title="Script tag" value="script-tag">
```tsx check=false reason="This example omits surrounding application and SuperTokens configuration."
async function sendOTP(email: string) {
  try {
    let response = await supertokensPasswordless.createCode({
      email,
    });
    /**
         * For phone number, use this:

            let response = await supertokensPasswordless.createCode({
                phoneNumber: "+1234567890"
            });

        */

    if (response.status === "SIGN_IN_UP_NOT_ALLOWED") {
      // the reason string is a user friendly message
      // about what went wrong. It can also contain a support code which users
      // can tell you so you know why their sign in / up was not allowed.
      window.alert(response.reason);
    } else {
      // OTP sent successfully.
      window.alert("Please check your email for an OTP");
    }
  } catch (err: any) {
    if (err.isSuperTokensGeneralError === true) {
      // this may be a custom error message sent from the API by you,
      // or if the input email / phone number is not valid.
      window.alert(err.message);
    } else {
      window.alert("Oops! Something went wrong.");
    }
  }
}
```
</ContentOption>
</DependentContent>
</Tab>
<Tab title="Mobile" value="mobile">
```bash
curl --location --request POST '<YOUR_API_DOMAIN>/auth/public/signinup/code' \
--header 'Content-Type: application/json; charset=utf-8' \
--data-raw '{
  "email": "johndoe@gmail.com"
}'
```
</Tab>
</CodeGroup>

<DependentContent passive group="frontend-custom-ui">
<ContentOption title="Mobile" value="mobile">
For phone number based login
</ContentOption>
</DependentContent>

<CodeGroup passive group="frontend-custom-ui">
<Tab title="Mobile" value="mobile">
```bash
curl --location --request POST '<YOUR_API_DOMAIN>/auth/public/signinup/code' \
--header 'Content-Type: application/json; charset=utf-8' \
--data-raw '{
  "phoneNumber": "+1234567890"
}'
```
</Tab>
</CodeGroup>

<DependentContent passive group="frontend-custom-ui">
<ContentOption title="Mobile" value="mobile">
The response body from the API call has a `status` property in it:

- `status: "OK"`: This means that the OTP was successfully sent.
- `status: "GENERAL_ERROR"`: This is possible if you have overridden the backend API to send back a custom error message which should display on the frontend, or if the input email or password failed the backend validation logic.
- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during `MFA`. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed.

The response from the API call is the following object (in case of `status: "OK"`):
</ContentOption>
</DependentContent>

<CodeGroup passive group="frontend-custom-ui">
<Tab title="Mobile" value="mobile">
```typescript check=false reason="This block documents the response shape rather than executable code."
{
    status: "OK";
    deviceId: string;
    preAuthSessionId: string;
    flowType: "USER_INPUT_CODE" | "MAGIC_LINK" | "USER_INPUT_CODE_AND_MAGIC_LINK";
}
```
</Tab>
</CodeGroup>

<DependentContent passive group="frontend-custom-ui">
<ContentOption title="Mobile" value="mobile">
You want to save the `deviceId` and `preAuthSessionId` on the frontend storage. These are useful to:

- Resend a new OTP.
- Detect if the user has already sent an OTP before or if this is an entirely new login attempt. This distinction can be important if you have different UI for these two states. For example, if this info already exists, you do not want to show the user an input box to enter their email / phone, and instead want to show them the enter OTP form with a resend button.
- Verify the user's input OTP.
</ContentOption>
</DependentContent>

##### 2.2 Resending a OTP

After you send the OTP to the user, you may want to display a resend button to them.
When the user clicks on this button, you should call the following API

<CodeGroup group="frontend-custom-ui">
<Tab title="Web" value="web">
<DependentContent group="install-method" label="Installation method">
<ContentOption title="npm" value="npm">
```tsx
import { resendCode, clearLoginAttemptInfo } from "supertokens-web-js/recipe/passwordless";

async function resendOTP() {
  try {
    let response = await resendCode();

    if (response.status === "RESTART_FLOW_ERROR") {
      // this can happen if the user has already successfully logged in into
      // another device whilst also trying to login to this one.

      // we clear the login attempt info that was added when the createCode function
      // was called - so that if the user does a page reload, they will now see the
      // enter email / phone UI again.
      await clearLoginAttemptInfo();
      window.alert("Login failed. Please try again");
      window.location.assign("/auth");
    } else {
      // OTP resent successfully.
      window.alert("Please check your email for the OTP");
    }
  } catch (err: any) {
    if (err.isSuperTokensGeneralError === true) {
      // this may be a custom error message sent from the API by you.
      window.alert(err.message);
    } else {
      window.alert("Oops! Something went wrong.");
    }
  }
}
```
</ContentOption>
<ContentOption title="Script tag" value="script-tag">
```tsx check=false reason="This example omits surrounding application and SuperTokens configuration."
async function resendOTP() {
  try {
    let response = await supertokensPasswordless.resendCode();

    if (response.status === "RESTART_FLOW_ERROR") {
      // this can happen if the user has already successfully logged in into
      // another device whilst also trying to login to this one.

      // we clear the login attempt info that was added when the createCode function
      // was called - so that if the user does a page reload, they will now see the
      // enter email / phone UI again.
      await supertokensPasswordless.clearLoginAttemptInfo();
      window.alert("Login failed. Please try again");
      window.location.assign("/auth");
    } else {
      // OTP resent successfully.
      window.alert("Please check your email for the OTP");
    }
  } catch (err: any) {
    if (err.isSuperTokensGeneralError === true) {
      // this may be a custom error message sent from the API by you.
      window.alert(err.message);
    } else {
      window.alert("Oops! Something went wrong.");
    }
  }
}
```
</ContentOption>
</DependentContent>
</Tab>
<Tab title="Mobile" value="mobile">
```bash
curl --location --request POST '<YOUR_API_DOMAIN>/auth/public/signinup/code/resend' \
--header 'Content-Type: application/json; charset=utf-8' \
--data-raw '{
  "deviceId": "...",
  "preAuthSessionId": "...."
}'
```
</Tab>
</CodeGroup>

<DependentContent passive group="frontend-custom-ui">
<ContentOption title="Mobile" value="mobile">
The response body from the API call has a `status` property in it:

- `status: "OK"`: This means that the OTP was successfully sent.
- `status: "RESTART_FLOW_ERROR"`: This can happen if the user has already successfully logged in into another device whilst also trying to login to this one. You want to take the user back to the login screen where they can enter their email / phone number again. Be sure to remove the stored `deviceId` and `preAuthSessionId` from the frontend storage.
- `status: "GENERAL_ERROR"`: This is possible if you have overridden the backend API to send back a custom error message which should display on the frontend.
</ContentOption>
</DependentContent>

##### How to detect if the initial OTP has been sent

If you are building the send and enter OTP interfaces on the same page, you might run into an issue when the user refreshes the page.
To prevent this you need a way to know which UI to show.

<DependentContent passive group="frontend-custom-ui">
<ContentOption title="Mobile" value="mobile">
Since you save the `preAuthSessionId` and `deviceId` after sending the initial OTP, you can determine which interface to show.
Check if you stored these tokens on the device.

If they aren't present, show the form from step 2.1. Otherwise, show the OTP form from step 2.3 with the resend action from step 2.2.

:::note[You need to clear these tokens if:]

- the user navigates away from the OTP entry page
- you get a `RESTART_FLOW_ERROR` at any point in time from an API call
- the user has successfully logged in.
:::
</ContentOption>
</DependentContent>

<CodeGroup group="frontend-custom-ui">
<Tab title="Web" value="web">
<DependentContent group="install-method" label="Installation method">
<ContentOption title="npm" value="npm">
```tsx
import { getLoginAttemptInfo } from "supertokens-web-js/recipe/passwordless";

async function hasInitialOTPBeenSent() {
  return (await getLoginAttemptInfo()) !== undefined;
}
```
</ContentOption>
<ContentOption title="Script tag" value="script-tag">
```tsx check=false reason="This example omits surrounding application and SuperTokens configuration."
async function hasInitialOTPBeenSent() {
  return (await supertokensPasswordless.getLoginAttemptInfo()) !== undefined;
}
```
</ContentOption>
</DependentContent>
</Tab>
<Tab title="Mobile" value="mobile">

</Tab>
</CodeGroup>

<DependentContent passive group="frontend-custom-ui">
<ContentOption title="Web" value="web">
If `hasInitialOTPBeenSent` returns `true`, show the OTP form from step 2.3 with the resend action from step 2.2. Otherwise, show the form from step 2.1 asking users to enter their email or phone number.
</ContentOption>
</DependentContent>

##### 2.3 Verifying the OTP

When the user enters an OTP you have to call the following API to verify it

<CodeGroup group="frontend-custom-ui">
<Tab title="Web" value="web">
<DependentContent group="install-method" label="Installation method">
<ContentOption title="npm" value="npm">
```tsx
import { consumeCode, clearLoginAttemptInfo } from "supertokens-web-js/recipe/passwordless";

async function handleOTPInput(otp: string) {
  try {
    let response = await consumeCode({
      userInputCode: otp,
    });

    if (response.status === "OK") {
      // we clear the login attempt info that was added when the createCode function
      // was called since the login was successful.
      await clearLoginAttemptInfo();
      if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) {
        // user sign up success
      } else {
        // user sign in success
      }
      window.location.assign("/home");
    } else if (response.status === "INCORRECT_USER_INPUT_CODE_ERROR") {
      // the user entered an invalid OTP
      window.alert(
        "Wrong OTP! Please try again. Number of attempts left: " +
          (response.maximumCodeInputAttempts - response.failedCodeInputAttemptCount),
      );
    } else if (response.status === "EXPIRED_USER_INPUT_CODE_ERROR") {
      // it can come here if the entered OTP was correct, but has expired because
      // it was generated too long ago.
      window.alert("Old OTP entered. Please regenerate a new one and try again");
    } else {
      // this can happen if the user tried an incorrect OTP too many times.
      // or if it was denied due to security reasons in case of automatic account linking

      // we clear the login attempt info that was added when the createCode function
      // was called - so that if the user does a page reload, they will now see the
      // enter email / phone UI again.
      await clearLoginAttemptInfo();
      window.alert("Login failed. Please try again");
      window.location.assign("/auth");
    }
  } catch (err: any) {
    if (err.isSuperTokensGeneralError === true) {
      // this may be a custom error message sent from the API by you.
      window.alert(err.message);
    } else {
      window.alert("Oops! Something went wrong.");
    }
  }
}
```
</ContentOption>
<ContentOption title="Script tag" value="script-tag">
```tsx check=false reason="This example omits surrounding application and SuperTokens configuration."
async function handleOTPInput(otp: string) {
  try {
    let response = await supertokensPasswordless.consumeCode({
      userInputCode: otp,
    });

    if (response.status === "OK") {
      // we clear the login attempt info that was added when the createCode function
      // was called since the login was successful.
      await supertokensPasswordless.clearLoginAttemptInfo();
      if (response.createdNewRecipeUser && response.user.loginMethods.length === 1) {
        // user sign up success
      } else {
        // user sign in success
      }
      window.location.assign("/home");
    } else if (response.status === "INCORRECT_USER_INPUT_CODE_ERROR") {
      // the user entered an invalid OTP
      window.alert(
        "Wrong OTP! Please try again. Number of attempts left: " +
          (response.maximumCodeInputAttempts - response.failedCodeInputAttemptCount),
      );
    } else if (response.status === "EXPIRED_USER_INPUT_CODE_ERROR") {
      // it can come here if the entered OTP was correct, but has expired because
      // it was generated too long ago.
      window.alert("Old OTP entered. Please regenerate a new one and try again");
    } else {
      // this can happen if the user tried an incorrect OTP too many times.
      // or if it was denied due to security reasons in case of automatic account linking

      // we clear the login attempt info that was added when the createCode function
      // was called - so that if the user does a page reload, they will now see the
      // enter email / phone UI again.
      await supertokensPasswordless.clearLoginAttemptInfo();
      window.alert("Login failed. Please try again");
      window.location.assign("/auth");
    }
  } catch (err: any) {
    if (err.isSuperTokensGeneralError === true) {
      // this may be a custom error message sent from the API by you.
      window.alert(err.message);
    } else {
      window.alert("Oops! Something went wrong.");
    }
  }
}
```
</ContentOption>
</DependentContent>
</Tab>
<Tab title="Mobile" value="mobile">
```bash
curl --location --request POST '<YOUR_API_DOMAIN>/auth/public/signinup/code/consume' \
--header 'Content-Type: application/json; charset=utf-8' \
--data-raw '{
  "deviceId": "...",
  "preAuthSessionId": "...",
  "userInputCode": "<Entered OTP>"
}'
```
</Tab>
</CodeGroup>

<DependentContent passive group="frontend-custom-ui">
<ContentOption title="Mobile" value="mobile">
The response body from the API call has a `status` property in it:

- `status: "OK"`: User sign in / up was successful. The response also contains more information about the user, for example their user ID, and if it was a new user or existing user.
- `status: "INCORRECT_USER_INPUT_CODE_ERROR"`: The entered OTP is invalid. The response contains information about the maximum number of retries and the number of failed attempts.
- `status: "EXPIRED_USER_INPUT_CODE_ERROR"`: The entered OTP is too old. You should ask the user to resend a new OTP and try again.
- `status: "RESTART_FLOW_ERROR"`: The user entered invalid OTPs too many times and must restart the flow.
- `status: "GENERAL_ERROR"`: This is possible if you have overridden the backend API to send back a custom error message which should display on the frontend.
- `status: "SIGN_IN_UP_NOT_ALLOWED"`: This can happen during automatic account linking or during `MFA`. The `reason` prop that's in the response body contains a support code using which you can see why the sign in / up was not allowed.
</ContentOption>
</DependentContent>

On success, the backend sends session tokens in the response. Web SDK requests handle them automatically. Native SDKs only do so when the request uses their integrated HTTP client or interceptor; raw requests such as the `curl` examples must be implemented through that integration in the app.

### 3. Initialize the backend SDK

You need to initialize the **Backend SDK** alongside the code that starts your server.
The init call includes [configuration details](/references/backend-sdks/reference#sdk-configuration) for your app. It specifies how the backend connects to the **SuperTokens Core**, as well as the **Recipes** used in your setup.

For the **Passwordless** recipe, you also need to specify the `flowType` and `contactMethod`.
Click one of the options from the next form and the code snippet updates.

<CodeGroup group="backend-language">
<Tab title="Node.js" value="nodejs">
```tsx title="Backend SDK Init"
import supertokens from "supertokens-node";
import Session from "supertokens-node/recipe/session";
import Passwordless from "supertokens-node/recipe/passwordless";

supertokens.init({
  // Replace this with the framework you are using
  framework: "express",
  supertokens: {
    // We use try.supertokens for demo purposes.
    // At the end of the tutorial we will show you how to create
    // your own SuperTokens core instance and then update your config.
    connectionURI: "https://try.supertokens.io",
    // apiKey: <YOUR_API_KEY>
  },
  appInfo: {
    // learn more about this on https://supertokens.com/docs/references/backend-sdks/reference#sdk-configuration
    appName: "<YOUR_APP_NAME>",
    apiDomain: "<YOUR_API_DOMAIN>",
    websiteDomain: "<YOUR_WEBSITE_DOMAIN>",
    apiBasePath: "/auth",
    websiteBasePath: "/auth",
  },
  recipeList: [
    Passwordless.init({
      flowType: "MAGIC_LINK",
      contactMethod: "EMAIL",
    }),
    Session.init(),
  ],
});
```
</Tab>
<Tab title="Python" value="python">
```python title="Backend SDK Init"
from supertokens_python import init, InputAppInfo, SupertokensConfig
from supertokens_python.recipe import passwordless, session
from supertokens_python.recipe.passwordless import ContactEmailOnlyConfig

init(
    app_info=InputAppInfo(
        app_name="<YOUR_APP_NAME>",
        api_domain="<YOUR_API_DOMAIN>",
        website_domain="<YOUR_WEBSITE_DOMAIN>",
        api_base_path="/auth",
        website_base_path="/auth"
    ),
    supertokens_config=SupertokensConfig(
        # We use try.supertokens for demo purposes.
        # At the end of the tutorial we will show you how to create
        # your own SuperTokens core instance and then update your config.
        connection_uri="https://try.supertokens.io",
        # api_key: <YOUR_API_KEY>
    ),
    framework='fastapi',
    recipe_list=[
        session.init(), # initializes session features
        passwordless.init(
            flow_type="MAGIC_LINK",
            contact_config=ContactEmailOnlyConfig()
        )
    ],
    mode='asgi' # use wsgi if you are running using gunicorn
)
```
</Tab>
<Tab title="Go" value="go">
```go title="Backend SDK Init"
import (
	"github.com/supertokens/supertokens-golang/recipe/session"
	"github.com/supertokens/supertokens-golang/recipe/passwordless"
	"github.com/supertokens/supertokens-golang/recipe/passwordless/plessmodels"
	"github.com/supertokens/supertokens-golang/supertokens"
)

func main() {
  apiBasePath := "/auth"
  websiteBasePath := "/auth"
  err := supertokens.Init(supertokens.TypeInput{
    Supertokens: &supertokens.ConnectionInfo{
          // We use try.supertokens for demo purposes.
          // At the end of the tutorial we will show you how to create
          // your own SuperTokens core instance and then update your config.
          ConnectionURI: "https://try.supertokens.io",
          // APIKey: <YOUR_API_KEY>
    },
    AppInfo: supertokens.AppInfo{
            AppName: "<YOUR_APP_NAME>",
            APIDomain: "<YOUR_API_DOMAIN>",
            WebsiteDomain: "<YOUR_WEBSITE_DOMAIN>",
            APIBasePath: &apiBasePath,
            WebsiteBasePath: &websiteBasePath,
    },
    RecipeList: []supertokens.Recipe{
      passwordless.Init(plessmodels.TypeInput{
                FlowType: "MAGIC_LINK",
                ContactMethodEmail: plessmodels.ContactMethodEmailConfig{Enabled: true},
            }),
      session.Init(nil), // initializes session features
    },
  })

	if err != nil {
		panic(err.Error())
	}
}
```
</Tab>
</CodeGroup>

</VariantContent>

## Next steps

Having completed the main setup, you can explore more advanced topics related to the **Passwordless** recipe.

<CardGroup cols={3}>
  <Card title="Customize the Magic Link" href="/authentication/passwordless/customize-the-magic-link">
Change how Magic Links get created.
</Card>
  <Card title="OTP Customization" href="/authentication/passwordless/customize-the-otp">
Change the format of the generated One-Time Password.
</Card>
  <Card title="Hooks and overrides" href="/authentication/passwordless/hooks-and-overrides">
Add custom logic after the logs in or signs up.
</Card>
  <Card title="Email Delivery" href="/platform-configuration/email-delivery">
Customize how emails get delivered to your users.
</Card>
  <Card title="SMS Delivery" href="/platform-configuration/sms-delivery">
Customize how SMS messages get delivered to your users.
</Card>
</CardGroup>
