nOTP (WhatsApp) Authentication with Backend SDKs

This guide is meant for developers that are NOT using Descope on the frontend to design login screens and authentication methods.

If you'd like to use Descope Flows, Quick Start should be your starting point. If you'd like to use our Client SDKs, refer to our Client SDK docs.

nOTP allows users to log in via WhatsApp with just a single click, eliminating the need for codes, usernames, and typing. Unlike traditional OTP methods, nOTP doesn't require the company to connect to email servers or SMS providers, which can significantly reduce costs as it scales with the number of users.

To get started with authentication using nOTP (WhatsApp), refer to our nOTP Documentation. Continue reading to learn how to integrate nOTP authentication into your application using our Backend SDKs.

Backend SDK

Install SDK

Terminal
npm i --save @descope/node-sdk

Import and initialize SDK

import DescopeClient from '@descope/node-sdk';
try{
    //  baseUrl="<URL>" // When initializing the Descope client, you can also configure the baseUrl ex: https://auth.company.com  - this is useful when you utilize a custom domain within your Descope project.
    const descopeClient = DescopeClient({ projectId: '__ProjectID__' });
} catch (error) {
    // handle the error
    console.log("failed to initialize: " + error)
}

User Sign-Up

To implement nOTP (WhatsApp) authentication, the first step is user sign-up using the NOTP (WhatsApp) authentication method. Use the SignUp function to create a new user via WhatsApp. The Login ID should ideally be a phone number or can be left empty, in which case the phone number from WhatsApp will be used as the Login ID during verification. After calling the sign-up function, you will receive a response that includes a redirect URL or a QR code image. Present this to the user, who will then use WhatsApp to scan the QR code or follow the link to begin the authentication process.

// Args:
//    user: Optional user object to populate new user information.
const user = { "name": "Joe Person", "phone": "+15555555555", "email": "email@company.com"}
//    loginId: email or phone - becomes the unique ID for the user from here on and also used for delivery
const loginId = "email@company.com"
const resp = await descopeClient.notp.signUp(loginId, user);
if (!resp.ok) {
  console.log("Failed to initialize NOTP signup")
  console.log("Status Code: " + resp.code)
  console.log("Error Code: " + resp.error.errorCode)
  console.log("Error Description: " + resp.error.errorDescription)
  console.log("Error Message: " + resp.error.errorMessage)
}
else {
  console.log("Successfully initialized NOTP signup.")
  // resp.data contains { pendingRef, redirectUrl, image }
  // Present the QR `image` or send the user to `redirectUrl` to complete auth
  console.log(resp.data)
}

User Sign-In

To sign in a user with nOTP (WhatsApp) authentication, use the SignIn function with the NOTP (WhatsApp) authentication method. The Login ID should be a phone number or can be left empty. If left empty, the phone number from WhatsApp will be used as the login ID during the verification process. Upon calling the sign-in function, the response will include a redirect URL and/or a QR code image. Present this information to the user; they should scan the QR code or follow the link in WhatsApp to start the authentication flow.

// Args:
//    loginId: email or phone - must be same as provided at the time of signup.
const loginId = "email@company.com"
//    loginOptions (LoginOptions): this allows you to configure behavior during the authentication process.
const loginOptions = {
      "stepup": false,
      "mfa": false,
      "customClaims": {"claim": "Value1"},
      "templateOptions": {"option": "Value1"}
    }
//    refreshToken (optional): the user's current refresh token in the event of stepup/mfa
const resp = await descopeClient.notp.signIn(loginId, loginOptions);
if (!resp.ok) {
  console.log("Failed to initialize NOTP Sign-In")
  console.log("Status Code: " + resp.code)
  console.log("Error Code: " + resp.error.errorCode)
  console.log("Error Description: " + resp.error.errorDescription)
  console.log("Error Message: " + resp.error.errorMessage)
}
else {
  console.log("Successfully initialized NOTP Sign-In.")
  // resp.data contains { pendingRef, redirectUrl, image }
  // Present the QR `image` or send the user to `redirectUrl` to complete sign-in
  console.log(resp.data)
}

User Sign-Up-or-In

Use the SignUpOrIn function to authenticate a user with nOTP (WhatsApp). If the user does not exist, SignUpOrIn will create a new user automatically. The Login ID should be a phone number or can be left empty. If left empty, the WhatsApp phone number will be used as the login ID during verification. After calling SignUpOrIn, the response will contain a redirect URL and/or a QR code image. Present the QR code or URL to the user, who should scan the QR code or follow the link in WhatsApp to start the authentication flow.

// Args:
//    loginId: email or phone - becomes the unique ID for the user from here on and also used for delivery.
const loginId = "email@company.com"
//    signUpOptions (SignUpOptions): this allows you to configure behavior during the authentication process.
const signUpOptions = {
      "customClaims": {"claim": "Value1"},
      "templateOptions": {"option": "Value1"}
    }
const resp = await descopeClient.notp.signUpOrIn(loginId, signUpOptions);
if (!resp.ok) {
  console.log("Failed to initialize NOTP Sign-Up or Sign-In")
  console.log("Status Code: " + resp.code)
  console.log("Error Code: " + resp.error.errorCode)
  console.log("Error Description: " + resp.error.errorDescription)
  console.log("Error Message: " + resp.error.errorMessage)
}
else {
  console.log("Successfully initialized NOTP Sign-Up or Sign-In.")
  // resp.data contains { pendingRef, redirectUrl, image }
  // Present the QR `image` or send the user to `redirectUrl` to complete authentication
  console.log(resp.data)
}

Update User

Use the UpdateUser function to update user details via nOTP (WhatsApp) authentication. The Login ID should be a phone number or can be left empty—if left empty, the WhatsApp phone number will be used as the login ID during verification. After calling UpdateUser, you’ll receive a response containing a redirect URL and/or QR code image. Present this QR code or URL to the user, who must scan the QR code or follow the link with WhatsApp to begin the authentication and update process.

//    ctx: context.Context - Application context for the request (supports cancellation, etc.).
//    loginID: The login ID of the user to update, should be a phone number.
//    phone: The new phone number to set for the user (or leave empty to use the WhatsApp phone number during verification).
//    updateOptions: (optional) *descope.NOTPUpdateOptions for advanced update flows (custom claims, template options, etc.).
//    r: *http.Request - the incoming HTTP request, used to extract the user's refresh token (required for the update).

ctx := context.Background()
loginID := "+15555555555" // The login ID of the user being updated (a phone number).
phone := "+15556667777"   // The new phone number to set. Can be "" (empty) to allow the WhatsApp phone number to be used.

// The user's refresh token is extracted from the incoming request, so pass the *http.Request from your handler.
var r *http.Request = nil // Replace with your actual http.Request.

// Example updateOptions. See SDK docs for all available options.
// For defaults, set to nil.
updateOptions := &descope.NOTPUpdateOptions{
    // CustomClaims:    map[string]any{"role": "user"},
    // TemplateID:      "your-custom-template",
    // TemplateOptions: map[string]string{"greeting": "Hi"},
}

// Call the UpdateUser function to initiate the nOTP (WhatsApp) update flow.
resp, err := descopeClient.Auth.NOTP.UpdateUser(ctx, loginID, phone, updateOptions, r)
if err != nil {
    fmt.Println("Failed to start nOTP (WhatsApp) update user flow:", err)
    return
}
// resp will include a redirect URL and/or QR code image, plus a pending reference used to poll for the session.
fmt.Println("Successfully started nOTP sign-in (WhatsApp).")
fmt.Println("Redirect URL:", resp.RedirectURL)
if resp.Image != "" {
    fmt.Println("QR Code image (base64):", resp.Image)
}
// Save resp.PendingRef and pass it to GetSession to complete the flow.
fmt.Println("Pending reference:", resp.PendingRef)

Get Session

To complete the WhatsApp nOTP flow, after the user completes verification in WhatsApp, retrieve their JWT by invoking the waitForSession or GetSession function and passing in the pendingRef from your SignIn/SignUp/SignUpOrIn call. The function polls Descope until the user finishes verifying in WhatsApp, then returns the session and refresh tokens (setting them as cookies on your response). If the user doesn't complete verification in time, it returns an error instead of a session.

On success, waitForSession resolves with sessionResp.data as a JWTResponse. Use sessionJwt for session validation and refreshJwt to renew the session:

{
  "sessionJwt": "eyJhbGciOiJSUzI...",
  "refreshJwt": "eyJhbGciOiJ...",
  "cookieDomain": "",
  "cookiePath": "/",
  "cookieMaxAge": 2419199,
  "cookieExpiration": 1685116422,
  "user": {
    "loginIds": ["+15555555555"],
    "userId": "U2abc123",
    "name": "Joe Person",
    "email": "email@company.com",
    "phone": "+15555555555",
    "verifiedEmail": true,
    "verifiedPhone": true,
    "roleNames": [],
    "userTenants": [],
    "status": "enabled",
    "externalIds": ["+15555555555"],
    "customAttributes": {},
    "createdTime": 1682612331
  },
  "firstSeen": true
}
// Args:
//    pendingRef: the reference string returned from notp.signIn / signUp / signUpOrIn.
const pendingRef = resp.data.pendingRef
//    config (optional WaitForSessionConfig): tune how long and how often to poll.
const config = {
      "timeoutMs": 120000,        // give up after 2 minutes (default applies if omitted)
      "pollingIntervalMs": 1000   // poll once per second (default applies if omitted)
    }
const sessionResp = await descopeClient.notp.waitForSession(pendingRef, config);
if (!sessionResp.ok) {
  // Note: a timeout does NOT throw - it resolves with ok: false, so check it here.
  console.log("Failed to complete NOTP authentication")
  console.log("Status Code: " + sessionResp.code)
  console.log("Error Code: " + sessionResp.error.errorCode)
  console.log("Error Description: " + sessionResp.error.errorDescription)
  console.log("Error Message: " + sessionResp.error.errorMessage)
}
else {
  const { sessionJwt, refreshJwt, user, firstSeen } = sessionResp.data
  console.log("Successfully authenticated via NOTP.")
  console.log("Session JWT:", sessionJwt)
  console.log("Refresh JWT:", refreshJwt)
  console.log("User ID:", user.userId)
  console.log("First seen (new user):", firstSeen)
}
Was this helpful?

On this page