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
Terminal
pip3 install descope
Terminal
go get github.com/descope/go-sdk
// Include the following in your `pom.xml` (Maven)
<dependency>
    <artifactId>java-sdk</artifactId>
    <groupId>com.descope</groupId>
    <version>sdk-version</version> // Check https://github.com/descope/descope-java/releases for the latest versions
</dependency>
Terminal
gem install descope
Terminal
composer require descope/descope-php
Terminal
dotnet add package descope

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)
}
from descope import (
    REFRESH_SESSION_TOKEN_NAME,
    SESSION_TOKEN_NAME,
    AuthException,
    DeliveryMethod,
    DescopeClient,
    AssociatedTenant,
    RoleMapping,
    AttributeMapping,
    LoginOptions
)
try:
    # You can configure the baseURL by setting the env variable Ex: export DESCOPE_BASE_URI="https://auth.company.com  - this is useful when you utilize custom domain within your Descope project."
    descope_client = DescopeClient(project_id='__ProjectID__')
except Exception as error:
    # handle the error
    print ("failed to initialize. Error:")
    print (error)
import "github.com/descope/go-sdk/descope"
import "github.com/descope/go-sdk/descope/client"

// Utilizing the context package allows for the transmission of context capabilities like cancellation
//      signals during the function call. In cases where context is absent, the context.Background()
//      function serves as a viable alternative.
//      Utilizing context within the Descope GO SDK is supported within versions 1.6.0 and higher.
import (
	"context"
)

// DescopeBaseURL // within the client.Config, you can also configure the baseUrl ex: https://auth.company.com  - this is useful when you utilize a custom domain within your Descope project.

descopeClient, err := client.NewWithConfig(&client.Config{ProjectID:"__ProjectID__"})
if err != nil {
    // handle the error
    log.Println("failed to initialize: " + err.Error())
}
import com.descope.client.Config;
import com.descope.client.DescopeClient;

var descopeClient = new DescopeClient(Config.builder().projectId("__ProjectID__").build());
require 'descope'

@project_id = ENV['__ProjectID__']
@client = Descope::Client.new({ project_id: @project_id})
require 'vendor/autoload.php';
use Descope\SDK\DescopeSDK;
 
$descopeSDK = new DescopeSDK([
    'projectId' => $_ENV['__ProjectID__'],
]);
// appsettings.json

{
  "Descope": {
    "ProjectId": "__ProjectID__",
    "ManagementKey": "DESCOPE_MANAGEMENT_KEY"
  }
}

// Program.cs

using Descope;
using Microsoft.Extensions.Configuration;

// ... In your setup code
var config = new ConfigurationBuilder()
  .AddJsonFile("appsettings.json")
  .Build();

var descopeProjectId = config["Descope:ProjectId"];
var descopeManagementKey = config["Descope:ManagementKey"];

var descopeConfig = new DescopeConfig(projectId: descopeProjectId);
var descopeClient = new DescopeClient(descopeConfig)
{
    ManagementKey = descopeManagementKey,
};

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)
}
//    ctx: context.Context - Application context for function calls, like cancellation signals during requests.
//    loginID: The login ID for the user, should be a phone number (or leave empty to be provided later).
//    user: (optional) *descope.User object to pre-fill with user info (name, email, phone, etc.).
//    signUpOptions: (optional) Additional sign-up options for nOTP (WhatsApp).

ctx := context.Background()
loginID := "+15555555555" // The user's phone number; can be "" (empty string) if you want to use WhatsApp phone as login ID

user := &descope.User{
    Name:  "Joe Person",
    Email: "email@company.com",
    // Phone: If left empty, defaults to loginID.
}

signUpOptions := &descope.SignUpOptions{
    // Optional sign-up options. You can use any of the following (or leave as nil for defaults):
    // CustomClaims:     Set any custom claims to associate with the user.
    // TemplateID:       Override the default WhatsApp message template.
    // TemplateOptions:  Map for template variables (for customizing WhatsApp message contents).
    // TenantID:         If your app supports multi-tenancy, specify a Tenant ID.
    //
    // Example:
    // signUpOptions := &descope.SignUpOptions{
    //     CustomClaims: map[string]any{"role": "admin"},
    //     TemplateID: "whatsapp-custom-template",
    //     TemplateOptions: map[string]string{
    //         "greeting": "Hi",
    //         "product": "YourProduct",
    //     },
    //     TenantID: "tenant-123",
    // }
    // Or, to use default values:
    // signUpOptions := nil
}

resp, err := descopeClient.Auth.NOTP.SignUp(ctx, loginID, user, signUpOptions)
if err != nil {
    fmt.Println("Failed to start nOTP (WhatsApp) sign-up 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)

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)
}
//    ctx: context.Context - Application context for the request (supports cancellation, etc.).
//    loginID: The login ID for the user, should be a phone number (or leave empty to be provided later).
//    r: *http.Request - the incoming HTTP request, used if refresh tokens are required (for step-up/MFA).
//    loginOptions: (optional) *descope.LoginOptions for advanced login flows (MFA, step-up, custom claims, template options, etc.).

ctx := context.Background()
loginID := "+15555555555" // The user's phone number. Can be "" (empty) to allow WhatsApp phone number as login ID.

// Use *http.Request from your handler if performing step-up/MFA, otherwise can be nil if not used in your flow.
var r *http.Request = nil // Replace with your actual http.Request if available.

// Example loginOptions. See SDK docs for all available options.
// For defaults, set to nil.
loginOptions := &descope.LoginOptions{
    // Stepup:         false,
    // MFA:            false,
    // CustomClaims:   map[string]any{"role": "user"},
    // TemplateID:     "your-custom-template",
    // TemplateOptions: map[string]string{"greeting": "Hi"},
    // TenantID:       "tenant-123",
}

// Call the SignIn function to initiate nOTP (WhatsApp) authentication.
resp, err := descopeClient.Auth.NOTP.SignIn(ctx, loginID, r, loginOptions)
if err != nil {
    fmt.Println("Failed to start nOTP (WhatsApp) sign-in 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)

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)
}
// Args:
//    ctx: context.Context - Application context for the request.
//    loginID: The login ID for the user, should be a phone number (or can be empty, in which case the WhatsApp phone number will be used as login ID during verification).
//    signUpOptions: (optional) *descope.SignUpOptions for advanced options (TenantID, CustomClaims, TemplateOptions, etc.).

ctx := context.Background()
loginID := "+15555555555" // The user's phone number. Can be "" (empty) to use WhatsApp phone number.

// Example signUpOptions. For defaults, set to nil.
signUpOptions := &descope.SignUpOptions{
    TenantID:       "tenant-123",
    CustomClaims:   map[string]any{"role": "user"},
    TemplateOptions: map[string]string{"greeting": "Hi"},
}

// Call the SignUpOrIn function to initiate nOTP (WhatsApp) authentication.
resp, err := descopeClient.Auth.NOTP.SignUpOrIn(ctx, loginID, signUpOptions)
if err != nil {
    fmt.Println("Failed to start nOTP (WhatsApp) sign-up-or-in 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)

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)
}
//    ctx: context.Context - Application context for the request (supports cancellation, etc.).
//    pendingRef: The pending reference returned by SignIn/SignUp/SignUpOrIn, used to poll for the session.
//    w: http.ResponseWriter - the outgoing HTTP response, used to set the session/refresh token cookies.

ctx := context.Background()

// The pendingRef value comes from the response of the initial SignIn/SignUp/SignUpOrIn call (resp.PendingRef).
pendingRef := "<pending-ref-from-sign-in>" // Replace with the actual resp.PendingRef value.

// Use the http.ResponseWriter from your handler so the SDK can set session cookies on the response.
var w http.ResponseWriter = nil // Replace with your actual http.ResponseWriter if available.

// Call GetSession to complete the nOTP (WhatsApp) flow once the user has sent the verification message.
// This returns a valid session only after the user completes verification in WhatsApp; otherwise it errors.
authInfo, err := descopeClient.Auth.NOTP.GetSession(ctx, pendingRef, w)
if err != nil {
    fmt.Println("Failed to get nOTP (WhatsApp) session:", err)
    return
}

// authInfo contains the session and refresh tokens, plus user details.
fmt.Println("Successfully retrieved nOTP session (WhatsApp).")
fmt.Println("Session JWT:", authInfo.SessionToken.JWT)
if authInfo.RefreshToken != nil {
    fmt.Println("Refresh JWT:", authInfo.RefreshToken.JWT)
}
fmt.Println("First seen (new user):", authInfo.FirstSeen)
Was this helpful?

On this page