Anonymous Users
Anonymous users allow you to treat visitors as first-class identities before they provide a verified email, phone number, or username.
You can reduce registration friction while still using Descope as your customer identity layer: issue session tokens, attach custom data, and later convert the same anonymous user to a standard user while retaining information you collected during the anonymous phase.
How Anonymous Users Work
Descope represents an anonymous user by issuing a dedicated anonymous session JWT (not a standard user record with login IDs). That token:
- Is signed by Descope and behaves like other session tokens for your app (e.g. API authorization).
- Has a lifetime tied to the JWT (and refresh behavior you configure)—when the session expires, that anonymous identity ends unless you refresh or convert the user.
- Carries a
danuclaim (true) in the payload so your backend can tell this is an anonymous session. - Can include custom claims (for example user preferences, unverified email addresses, or app-specific flags) for use in your product.
When you are ready, you can convert the anonymous user to a regular user, without losing any data you've already gathered on the user.
See our blog on Boosting conversions with anonymous users and guest checkout.
Creating Anonymous Users
With Flows
The Create Anonymous user - Add Information To JWT flow template provides a starting point. Upon completion, Descope issues an anonymous identity token.

The following example illustrates a typical JWT payload and header after the flow runs. The danu claim marks the session as anonymous; displayName (or any claims you configure) represents optional custom data for your application:
// Payload:
{
"danu": true,
"displayName": "xxxxx",
"drn": "DS",
"exp": 1731843388,
"iat": 1731842788,
"iss": "xxxxxxxxx",
"rexp": "2024-12-15T11:26:28Z",
"sub": "xxxxxxxxx"
}
// Header:
{
"alg": "RS256",
"kid": "xxxxxxxxxxxxxxxx",
"typ": "JWT"
}With SDKs
You can use the Descope Management SDK to mint an anonymous session JWT programmatically. A management key is required. For broader SDK setup, see User management SDKs.
Install the SDK
npm i --save @descope/node-sdkpip3 install descopego get github.com/descope/go-sdkImport and initialize Management SDK
import DescopeClient from '@descope/node-sdk';
const managementKey = "xxxx"
try{
// baseUrl="<URL>" // When initializing the Descope clientyou 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__', managementKey: managementKey });
} 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
)
management_key = "xxxx"
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 a custom domain within your Descope project."
descope_client = DescopeClient(project_id='__ProjectID__', management_key=management_key)
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"
import "fmt"
// 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"
)
managementKey = "xxxx"
// 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__", managementKey:managementKey})
if err != nil {
// handle the error
log.Println("failed to initialize: " + err.Error())
}Create an anonymous user
This operation creates an anonymous user within the project with the details provided.
// Args:
// customClaims (Record<string, any>, optional): A dictionary of custom claims to include in the JWT.
// These claims can be used to store additional user information.
// selectedTenant (string, optional): The ID of the tenant to associate with the JWT.
// This is useful for multi-tenant applications.
const customClaims = {
role: "guest",
permissions: ["read"]
};
const selectedTenant = "tenant_123";
const resp = await descopeClient.management.jwt.anonymous(customClaims, selectedTenant);
if (!resp.ok) {
console.log("Failed to generate JWT for anonymous user.");
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 generated JWT for anonymous user.");
console.log(resp.data);
}# Args:
# custom_claims (dict, optional): A dictionary of custom claims to include in the JWT.
# These claims can be used to store additional user information.
# tenant_id (str, optional): The ID of the tenant to associate with the JWT.
# This is useful for multi-tenant applications.
custom_claims = {
"role": "guest",
"permissions": ["read"]
}
tenant_id = "tenant_123"
try:
jwt_response = descope_client.mgmt.jwt.anonymous(custom_claims=custom_claims, tenant_id=tenant_id)
print("Successfully generated JWT for anonymous user")
print(json.dumps(jwt_response, indent=4))
except AuthException as error:
print("Failed to generate JWT")
print("Status Code: " + str(error.status_code))
print("Error: " + str(error.error_message))// Args:
// ctx (context.Context): Application context for the transmission of context capabilities like
// cancellation signals during the function call. If no context is available,
// use context.Background() as an alternative.
// customClaims (map[string]any, optional): A map of custom claims to include in the JWT. These claims
// can be used to store additional user information.
// selectedTenant (string, optional): The ID of the tenant to associate with the JWT. This is useful
// for multi-tenant applications.
ctx := context.Background()
customClaims := map[string]any{
"role": "guest",
"permissions": []string{"read"},
}
selectedTenant := "tenant_123"
res, err := descopeClient.JWT().Anonymous(ctx, customClaims, selectedTenant)
if err != nil {
fmt.Println("Failed to generate JWT for anonymous user:", err)
} else {
fmt.Println("Successfully generated JWT for anonymous user:")
fmt.Printf("Session Token: %s\n", res.SessionToken)
fmt.Printf("Refresh Token: %s\n", res.RefreshToken)
}Converting Anonymous Users to Regular Users
With Flows
To move from an anonymous session to a regular user, use a flow such as Anonymous User Conversion. This flow authenticates the user and links a verified login ID while preserving context from the anonymous phase.

- The template demonstrates one authentication pattern; you can adapt the same approach to other factors your product supports.
- Verify ownership of the login ID (for example via magic link or OTP) before completing conversion. Doing so prevents users from attaching an email or phone number that already belongs to another account.
With SDKs
If you handle conversion in your backend (instead of using a flow), follow the same rules as above: verify a real login ID, then create a full user and copy over data you stored in the anonymous JWT.
Typical Steps:
- Validate the anonymous session JWT.
- Extract the claims you intend to keep for the user.
- Create the new user using any of the authentication methods in our Client SDKs. Populate custom attributes and other fields from the anonymous user claims.