Tenant Management

The Descope service supports multi-tenancy architecture natively. Each project can have multiple tenants, and the end-users can be assigned to either the project or can belong to one or many tenants. The most common use case of tenants is when you are building a B2B application, and each of your customers can have multiple users. You must manage these users and their roles at a tenant level. Descope admins can create and update tenants either manually in the Descope console or using the tenant management API and sdk from within their application as show below.

Tenant Domain

Under the configuration of tenants in Descope, you can configure tenant domains. These domains are utilized during user registration and will automatically maps users to the tenant based on the domain in \ their email address. It only takes effect if the user signs in using methods other than SAML. In case of SAML authentication, the tenant-id is a required parameter.

Tenant Session Management

Descope allows you to configure some of the session management configurations at a per tenant level. You can configure these items within the Descope Console by going to the tenants page, selecting the tenant you want to configure, and then select Custom under the Session Management section. Descriptions of the configurations can be found within this section of the project configuration guide for the supported tenant level configurations.

Once you have enabled these configurations at the tenant level, the tenant level configuration will take precedence over the project level configuration.

Note: If a user exists in multiple tenants, a merged policy favoring stricter security will be chosen.

Custom Tenant Attributes

Descope allows you to create custom attributes that can store further details about your tenants. You can create custom attributes within the tenant's page under the custom attributes tab.

Custom attributes can be of various types and store any data you want to store for the tenant. For example, this data could be a tenant's paid tier, geographical location, etc. You can later utilize these attributes within custom claims or loaded for a tenant and displayed them within your application.

Tenant management using the management SDK

Install SDK

NodeJSPythonGoJava
npm i --save @descope/node-sdk
pip3 install descope
go get github.com/descope/go-sdk
// Include the following in your `pom.xml` (for 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>

Import and initialize Management SDK

NodeJSPythonGoJava
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 CNAME within your Descope project.
    const descopeClient = DescopeClient({ projectId: '__ProjectID__', managementKey: managementKey });
} catch (error) {
    // handle the error
    console.log("failed to initialize: " + error)
}

// Note that you can handle async operation failures and capture specific errors to customize errors.
//     An example can be found here: https://github.com/descope/node-sdk?tab=readme-ov-file#error-handling
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 CNAME 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 CNAME 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())
}
import com.descope.client;

// Initialized after setting the DESCOPE_PROJECT_ID env var (and optionally DESCOPE_MANAGEMENT_KEY)
var descopeClient = new DescopeClient();

// ** Or directly **
var descopeClient = new DescopeClient(Config.builder()
        .projectId("__ProjectID__")
        .managementKey("management-key")
        .build());

Load All tenants

Use the code below to load all existing tenants within the project

NodeJSPythonGoJava
let resp = await descopeClient.management.tenant.loadAll();
if (!resp.ok) {
  console.log("Unable to load tenants.")
  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 loaded tenants:")
  console.log(resp.data)
}
try:
    resp = descope_client.mgmt.tenant.load_all()
    print ("Successfully loaded tenants:")
    print(json.dumps(resp, indent=2))
except AuthException as error:
    print ("Unable to load tenants.")
    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. 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.
ctx := context.Background()

res, err := descopeClient.Management.Tenant().LoadAll(ctx)
if  (err != nil){
  fmt.Println("Unable to load tenants: ", err)
} else {
  fmt.Println("Successfully loaded tenants: ")
  for _, t := range res {
    fmt.Println(t)
  }
}
TenantService ts = descopeClient.getManagementServices().getTenantService();
// Load all tenants
try {
    List<Tenant> tenants = ts.loadAll();
    for (Tenant t : tenants) {
        // Do something
    }
} catch (DescopeException de) {
    // Handle the error
}

Load Tenant by ID

This function allows for you to load a specific tenant based on the tenant's ID.

NodeJSPythonGoJava
// Args:
//  id (String): The ID of the tenant which you want to load
const id = "xxxx"

let resp = await descopeClient.management.tenant.load(id);
if (!resp.ok) {
  console.log("Failed to load tenant.")
  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 loaded tenant.")
  console.log(resp.data)
}
# Args:
#  id (String): The ID of the tenant which you want to load
id = "xxxx"

try:
    resp = descope_client.mgmt.tenant.load(id=id)
    print("Successfully loaded tenant")
    print(json.dumps(resp, indent=4))
except AuthException as error:
    print ("Failed to load tenant")
    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. 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.
ctx := context.Background()
// 	id (String): The ID of the tenant which you want to load
id := "xxxx"

res, err := descopeClient.Management.Tenant().Load(ctx, id)
if  (err != nil){
  fmt.Println("Unable to load tenant: ", err)
} else {
  fmt.Println("Successfully loaded tenant: ")
  fmt.Println(res)
}
TenantService ts = descopeClient.getManagementServices().getTenantService();
try {
    ts.load("my-custom-id");
} catch (DescopeException de) {
    // Handle the error
}

Search Tenants

This function allows for you to search Descope tenants by ID, name, self service provisioning domain, and custom attributes.

NodeJSPythonGoJava
// Args:
//  ids (String[]): Array of tenant IDs to search for.
const ids = ["TestTenant"]
//  names (String[]): Array of tenant names to search for.
const names = ["TestTenant"]
//  selfProvisioningDomains (String[]): Array of self service provisioning domains to search for.
const selfProvisioningDomains = ["example.com", "company.com"]
//  customAttributes (String[]): Array of self service provisioning domains to search for.
const customAttributes = {"mycustomattribute": "Test"}

// When searching based on one of these items or a few of these items, leave the applicable items you are not searching for to null.
let resp = await descopeClient.management.tenant.searchAll(ids, names, selfProvisioningDomains, customAttributes);

if (!resp.ok) {
  console.log("Failed to search tenants.")
  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 searched tenants.")
  resp.data.forEach((tenant) => {
    console.log(tenant)
  });
}
# Args:
#  ids (String[]): Array of tenant IDs to search for.
ids = ["TestTenant"]
#  names (String[]): Array of tenant names to search for.
names = ["TestTenant"]
#  selfProvisioningDomains (String[]): Array of self service provisioning domains to search for.
self_provisioning_domains = ["example.com", "company.com"]
#  customAttributes (String[]): Array of self service provisioning domains to search for.
custom_attributes = {"mycustomattribute": "Test"}

# When searching based on one of these items or a few of these items, leave the applicable items you are not searching for to null.
try:
    resp = descope_client.mgmt.tenant.search_all(ids=ids, names=names, self_provisioning_domains=self_provisioning_domains, custom_attributes=custom_attributes)
    print("Successfully searched tenants")
    print(json.dumps(resp, indent=4))
except AuthException as error:
    print ("Failed to search tenants")
    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. 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.
ctx := context.Background()
//  searchOptions (&descope.TenantSearchOptions{}): Search options for your tenant search
searchOptions := &descope.TenantSearchOptions{}
searchOptions.IDs = []string{"TestTenant"}
searchOptions.Names = []string{"TestTenant"}
searchOptions.SelfProvisioningDomains = []string{"example.com", "company.com"}
searchOptions.CustomAttributes = map[string]any{"mycustomattribute": "Test"}

res, err := descopeClient.Management.Tenant().SearchAll(ctx, searchOptions)
if  (err != nil){
  fmt.Println("Unable to search tenants: ", err)
} else {
  fmt.Println("Successfully searched tenants: ")
  for _, t := range res {
    fmt.Println(t)
  }
}
TenantService ts = descopeClient.getManagementServices().getTenantService();

try {
    List<Tenant> tenants = ts.searchAll(TenantSearchRequest.builder()
            .ids(Arrays.asList("TestTenant"))
            .names(Arrays.asList("TestTenant"))
            .customAttributes(Map.of("mycustomattribute", "Test"))
            .selfProvisioningDomains(Arrays.asList("example.com", "company.com")));
    for (Tenant t : tenants) {
        // Do something
    }
} catch (DescopeException de) {
    // Handle the error
}

Create Tenant

At the time of creation, the tenant must be given a name and a tenant-id. If you don't provide a tenant-id, a tenant-id is automatically generated. The tenant-id is used for sign-up/sign-in and other management operations later. In addition, you can also set domains for the tenant. The domain is used to automatically assign the end-user to a tenant at the time of sign-up and sign-in. The tenant name must be unique per project. The tenant ID is generated automatically for the tenant when not provided.

NodeJSPythonGoJava
// There are two ways to create a tenant via SDK. createWithId (which will assign the given id) and create (which will automatically generate the id). Examples below:
// createWithId: Create a new tenant with a given name and tenant id.
// ==================================================================
// Args:
//    name (str): The tenant's name
var name = "TestTenantCreateWithId"
//    id (str): The tenant's id.
var id = "TestConfiguredId"
//    selfProvisioningDomains (List[str]): An optional list of domains that are associated with this tenant. Users authenticating from these domains will be associated with this tenant.
var selfProvisioningDomains = ["TestDomain1.com", "TestDomain2.com"]
//   customAttributes (dict): Optional, set the different custom attributes values of the keys that were previously configured in Descope console app
const customAttributes = {"attribute1": "Value 1", "attribute2": "Value 2"}

let resp = await descopeClient.management.tenant.createWithId(id, name, selfProvisioningDomains, customAttributes)
if (!resp.ok) {
  console.log(resp)
  console.log("Unable to create tenant.")
  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 created tenant.")
  console.log(resp.data)
}

// Create: Create a new tenant with the given name. The id will be automatically generated and returned.
// ==================================================================
// Args:
//    name (str): The tenant's name
name = "TestTenantCreateGeneratedId"
//    selfProvisioningDomains (List[str]): An optional list of domains that are associated with this tenant. Users authenticating from these domains will be associated with this tenant.
selfProvisioningDomains = ["TestDomain3.com", "TestDomain4.com"]
//   customAttributes (dict): Optional, set the different custom attributes values of the keys that were previously configured in Descope console app
const customAttributes = {"attribute1": "Value 1", "attribute2": "Value 2"}

resp = await descopeClient.management.tenant.create(name, selfProvisioningDomains, customAttributes)
if (!resp.ok) {
  console.log("Unable to create tenant.")
  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 created tenant.")
  console.log(resp.data)
}
# Create a new tenant with the given name. Tenant IDs are provisioned automatically, but can be provided explicitly if needed. Both the name and ID must be unique per project.
# Args:
#   name (str): The tenant's name
name = "TestTenantCreateWithId"
#   id (str): Optional tenant ID. If not provided, it will be auto assigned.
id = "TestTenantCreateWithId"
#   self_provisioning_domains (List[str]): An optional list of domains that are associated with this tenant. Users authenticating from these domains will be associated with this tenant.
self_provisioning_domains = ["TestDomain1.com", "TestDomain2.com"]
#   custom_attributes (dict): Optional, set the different custom attributes values of the keys that were previously configured in Descope console app
custom_attributes = {"attribute1": "Value 1", "attribute2": "Value 2"}

try:
  resp = descope_client.mgmt.tenant.create(name=name, id=id, self_provisioning_domains=self_provisioning_domains, custom_attributes=custom_attributes)
  print ("Successfully created tenant.")
  print(json.dumps(resp, indent=2))
except AuthException as error:
  print ("Unable to create tenant.")
  print ("Status Code: " + str(error.status_code))
  print ("Error: " + str(error.error_message))
// There are two ways to create a tenant via SDK. CreateWithID (which will assign the given id) and Create (which will automatically generate the id). Examples below:
// CreateWithID: Create a new tenant with a given name and tenant id.
// ==================================================================
// Args:
//    ctx: context.Context - Application context 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.
ctx := context.Background()
//    tenantRequest (&descope.TenantRequest{}): Tenant options for creation
tenantRequest := &descope.TenantRequest{}
tenantRequest.Name = []string{"TestTenant"}
tenantRequest.SelfProvisioningDomains = []string{"example.com", "company.com"}
tenantRequest.CustomAttributes = map[string]any{"mycustomattribute": "Test"}
//    id (str): The tenant's id.
id := "TestConfiguredId"


err := descopeClient.Management.Tenant().CreateWithID(ctx, id, tenantRequest)
if  (err != nil){
  fmt.Println("Unable to create tenant with specified ID: ", err)
} else {
  fmt.Println("Successfully created tenant with specified ID")
}

// Create: Create a new tenant with the given name. The id will be automatically generated and returned.
// ==================================================================
// Args:
//    ctx: context.Context - Application context 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.
ctx := context.Background()
//    tenantRequest (&descope.TenantRequest{}): Tenant options for creation
tenantRequest := &descope.TenantRequest{}
tenantRequest.Name = []string{"TestTenant"}
tenantRequest.SelfProvisioningDomains = []string{"example.com", "company.com"}
tenantRequest.CustomAttributes = map[string]any{"mycustomattribute": "Test"}

tenantID, err := descopeClient.Management.Tenant().Create(ctx, tenantRequest)
if  (err != nil){
  fmt.Println("Unable to create tenant: ", err)
} else {
  fmt.Println("Successfully created tenant. The automatically generated ID is: ", tenantID)
}
TenantService ts = descopeClient.getManagementServices().getTenantService();
// The self provisioning domains or optional. If given they'll be used to associate
// Users logging in to this tenant
try {
    ts.create("My Tenant", Arrays.asList("domain.com"), new HashMap<String, Object>() {{
        put("custom-attribute-1", "custom-value1");
        put("custom-attribute-2", "custom-value2");
    }});
} catch (DescopeException de) {
    // Handle the error
}

// You can optionally set your own ID when creating a tenant
try {
    ts.createWithId("my-custom-id", "My Tenant", Arrays.asList("domain.com"), new HashMap<String, Object>() {{
        put("custom-attribute-1", "custom-value1");
        put("custom-attribute-2", "custom-value2");
    }});
} catch (DescopeException de) {
    // Handle the error
}

Update Tenant

Use the code below to update an existing tenant with the given name and domains. All parameters are used as overrides to the existing tenant. Empty fields will override populated fields.

NodeJSPythonGoJava
// Args:
//    id (str): The tenant's id.
var id = "xxxxxx"
//    name (str): The tenant's name
var name = "Test Updated Name"
//    selfProvisioningDomains (List[str]): An optional list of domains that are associated with this tenant. Users authenticating from these domains will be associated with this tenant. If changed, it will be the new list of self provisioned domains.
var selfProvisioningDomains = ["TestUpdatedDomain1.com", "TestUpdatedDomain2.com"]
//   customAttributes (dict): Optional, set the different custom attributes values of the keys that were previously configured in Descope console app
const customAttributes = {"attribute1": "Value 1", "attribute2": "Value 2"}

const resp = await descopeClient.management.tenant.update(id, name, selfProvisioningDomains, customAttributes);
if (!resp.ok) {
  console.log(resp)
  console.log("Failed to update tenant.")
  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 update tenant.")
  console.log(resp.data)
}
# Args:
#   id (str): The ID of the tenant to update.
id = "xxxxxx"
#   name (str): The tenant's name, if changed, it will be the new name.
name = "Test Updated Name"
#   self_provisioning_domains (List[str]): An optional list of domains that are associated with this tenant. Users authenticating from these domains will be associated with this tenant. If changed, it will be the new list of self provisioned domains.
self_provisioning_domains = ["TestUpdatedDomain1.com", "TestUpdatedDomain2.com"]
#   custom_attributes (dict): Optional, set the different custom attributes values of the keys that were previously configured in Descope console app
custom_attributes = {"attribute1": "Value 1", "attribute2": "Value 2"}

try:
  resp = descope_client.mgmt.tenant.update(id=id, name=name, self_provisioning_domains=self_provisioning_domains, custom_attributes=custom_attributes)
  print ("Successfully updated tenant.")
except AuthException as error:
  print ("Unable to update tenant.")
  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. 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.
ctx := context.Background()
//    id (str): The id of the tenant you want to update.
id := "xxxxxx"
//    tenantRequest (&descope.TenantRequest{}): Tenant options for update
tenantRequest := &descope.TenantRequest{}
tenantRequest.Name = []string{"TestTenant"}
tenantRequest.SelfProvisioningDomains = []string{"example.com", "company.com"}
tenantRequest.CustomAttributes = map[string]any{"mycustomattribute": "Test"}

err := descopeClient.Management.Tenant().Update(ctx, id, tenantRequest)
if  (err != nil){
  fmt.Println("Unable to update tenant: ", err)
} else {
  fmt.Println("Successfully updated tenant")
}
TenantService ts = descopeClient.getManagementServices().getTenantService();
// Update will override all fields as is. Use carefully.
try {
    ts.update("my-custom-id", "My Tenant", Arrays.asList("domain.com", "another-domain.com"), new HashMap<String, Object>() {{
                put("custom-attribute-1", "custom-value1");
                put("custom-attribute-2", "custom-value2");
            }});
} catch (DescopeException de) {
    // Handle the error
}

Delete Tenant

Use the code below to delete an existing tenant. Please note that this action is irreversible.

NodeJSPythonGoJava
// Args:
//    id (str): The tenant's id.
var id = "xxxxxx"

let resp = await descopeClient.management.tenant.delete(id);
if (!resp.ok) {
  console.log(resp)
  console.log("Unable to delete tenant.")
  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 deleted tenant.")
  console.log(resp.data)
}
# Args:
#   id (str): The id of the tenant to be deleted.
id = "xxxxxx"

try:
  resp = descope_client.mgmt.tenant.delete(id=id)
  print("Successfully deleted tenant.")
except AuthException as error:
  print ("Unable to delete tenant.")
  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. 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.
ctx := context.Background()
//    id (str): The id of the tenant you want to delete.
id := "xxxxxx"

err := descopeClient.Management.Tenant().Delete(ctx, id)
if  (err != nil){
  fmt.Println("Unable to delete tenant: ", err)
} else {
  fmt.Println("Successfully deleted tenant")
}
TenantService ts = descopeClient.getManagementServices().getTenantService();
// Tenant deletion cannot be undone. Use carefully.
try {
    ts.delete("my-custom-id");
} catch (DescopeException de) {
    // Handle the error
}