User Deprovisioning

Learn how to manage deprovisioning users within your application.

Wristband Managed Users Only

This page covers deprovisioning users who are managed by Wristband. If your application owns and stores its own users instead (meaning Wristband only brokers SSO authentication), refer to SSO-Only User Deprovisioning instead.

User deprovisioning is the process of removing or deactivating a user in Wristband.

Your customers may need to deprovision users from their tenant for several reasons, including:

  • Employee offboarding: Removing access when an employee leaves the organization.
  • Access revocation: Revoking a user's access to your application.
  • Seat management: Freeing up user seats to stay within subscription limits or reduce licensing costs.

This guide covers several approaches to deprovisioning users in Wristband.


Ways to Deprovision a User

There are several ways to handle user deprovisioning in Wristband, depending on your customers' requirements and the level of automation you want to provide.

Option 1: Rely on Enterprise IdP Access Controls

For customers who use an enterprise IdP to authenticate with your application, you may not need to implement explicit user deprovisioning in Wristband. With this approach, the user's record remains in Wristband, but they can no longer authenticate through the enterprise IdP once their account is deactivated in the upstream identity provider (e.g., Okta or Microsoft Entra ID).

This approach may be sufficient if your enterprise customers are comfortable relying on their IdP to control access. However, it does not remove or deactivate the user's record in Wristband.

Option 2: Handle Deprovisioning Manually Through Support

You can manage deprovisioning through a manual administrative workflow. When an enterprise customer offboards an employee, they submit a support ticket requesting access removal. Your operations team then deactivates or deletes the user in the Wristband Dashboard.

This approach avoids the need for additional development but introduces operational overhead.

Option 3: Build a Customer Portal for User Management

You can use Wristband's APIs to build a customer portal that allows your customers to manage users within their tenants, including deactivating or deleting users as needed.

This approach reduces the operational burden on your support team by allowing customers to manage deprovisioning themselves. However, it still requires customers to manually initiate deprovisioning through the portal, which can be inefficient when managing large numbers of users.

Option 4: Automate Deprovisioning With a Wrapper API

You can build a dedicated API layer, referred to throughout this guide as a wrapper API, that allows your customers to trigger user deprovisioning programmatically. This eliminates the need for manual support tickets and enables a more scalable, automated workflow.

The following sections outline the architectural steps for implementing the wrapper API approach.


How the Wrapper API Works

When a customer initiates an offboarding event, their system sends a request to your custom deprovisioning endpoint. Because the customer's directory tools integrate with your application rather than directly with Wristband, the offboarding request won't include a Wristband userId. Instead, your endpoint should accept a user identifier from the customer's directory payload, typically an email or externalId, and use it to identify the corresponding Wristband user.

Your endpoint should:

  1. Authenticate the incoming customer request.
  2. Validate the target user's externalId or email from the request.
  3. Authenticate itself to Wristband to obtain an access token for making API calls.
  4. Look up the corresponding Wristband user by tenant and externalId or email to get their Wristband userId.
  5. Use that userId to deactivate or delete the user.

An end-to-end user deprovisioning request follows this operational flow:

%%{init: {'themeVariables': {'fontSize': '16px'}}}%%
sequenceDiagram
    participant Customer as Customer's backend
    participant Wrapper as Your Application APIs
    participant Token as Wristband Token Endpoint
    participant API as Wristband User APIs

    Customer->>Wrapper: Token request (client creds)
    Wrapper->>Token: Proxy request
    Token-->>Wrapper: Access token
    Wrapper-->>Customer: Access token

    Customer->>Wrapper: Deprovision request (Bearer token)
    Wrapper->>Wrapper: Validate token
    Wrapper->>Token: Request own token
    Token-->>Wrapper: Access token
    Wrapper->>API: Look up + deprovision user
    API-->>Wrapper: 200 or 204
    Wrapper-->>Customer: 200 or 204

1) Create a Tenant-Level M2M Client for Your Customer

Already Have Your Own API Key System?

Steps 1 and 2 are only required if you're using Wristband to generate API keys for your application. If you already have your own API key system, you can continue using it to secure your endpoints and skip directly to Step 3.

To secure requests to your wrapper API, create a dedicated tenant-level Machine-to-Machine (M2M) client for each customer. Each customer can use their clientId and clientSecret to obtain a Wristband access token and include it as a bearer token in requests to your API. Your wrapper API must validate the access token before processing any deprovisioning requests.

For instructions on creating tenant-level M2M clients, see the Tenant-Level Clients guide. Because these clients are used only to authenticate requests to your wrapper API, not to call Wristband APIs directly, they don't need any Wristband resource permissions.

Once you've created the client, share the following information with your customer through a secure channel:

FieldDescription
Client IDThe clientId of the tenant-level M2M client you created for this customer.
Client SecretThe clientSecret of the tenant-level M2M client you created for this customer.

2) Create a Token Endpoint Wrapper API

Expose a token-exchange endpoint in your wrapper API to allow customers to obtain Wristband access tokens securely. Your endpoint must accept the customer's tenant-level M2M credentials, forward them to the Wristband Token Endpoint using the client_credentials grant type, and return the resulting access token to the caller.

Suggested Specification

The following section provides a suggested specification for your custom token-exchange endpoint. You can adapt the endpoint design and implementation to fit your application's requirements.

Request

POST /api/v1/auth/token HTTP/1.1
Host: yourapp.com
Content-Type: application/json
 
{
  "clientId": "abc123",
  "clientSecret": "def456"
}

Headers

NameRequiredDescription
Content-TypeYesMust be application/json.

Body

FieldTypeRequiredDescription
clientIdstringYesThe clientId of the tenant-level M2M client you issued to this customer.
clientSecretstringYesThe clientSecret of the tenant-level M2M client you issued to this customer.

Response

HTTP/1.1 200 OK
Content-Type: application/json
 
{
  "accessToken": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
  "expiresIn": 1800
}

HTTP Response Codes

StatusDescription
200 OKThe credentials were valid, and an access token is included in the response body.
400 Bad RequestThe request body was missing clientId or clientSecret.
401 UnauthorizedThe clientId and clientSecret combination was invalid.

Code Example

For example, a TypeScript function that proxies the request to Wristband's Token Endpoint:

async function handleTokenRequest(clientId: string, clientSecret: string) {
  const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64');
 
  const response = await fetch(`https://${process.env.APPLICATION_VANITY_DOMAIN}/api/v1/oauth2/token`, {
    method: 'POST',
    headers: {
      Authorization: `Basic ${credentials}`,
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: 'grant_type=client_credentials',
  });
 
  if (!response.ok) {
    throw new Error(`Failed to get token: ${response.status}`);
  }
 
  const { access_token, expires_in } = await response.json();
  return { accessToken: access_token, expiresIn: expires_in };
}

3) Create a Deprovision Endpoint Wrapper API

Your custom deprovisioning endpoint acts as a bridge between your customer's identity system and Wristband. Wristband's user deactivation and deletion APIs require a Wristband userId, while your customers typically identify users by an upstream identifier, such as externalId or email. Your endpoint must accept these identifiers, resolve the corresponding Wristband userId using your internal database or the Wristband Query Tenant Users API, and then perform the requested deprovisioning operation.

Suggested Specification

The following is a recommended API specification for your custom deprovisioning endpoint. You can adapt it to fit your application's architecture and requirements.

Request

POST /api/v1/users/deprovision HTTP/1.1
Host: yourapp.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
 
{
  "email": "[email protected]"
}

Headers

NameRequiredDescription
AuthorizationYesA Bearer token authenticating your customer's call. See Validate Your Customer's Requests.
Content-TypeYesMust be application/json.

Body

FieldTypeRequiredDescription
externalIdstringConditionallyThe user's identifier in your customer's identity provider. Required if email isn't provided.
emailstringConditionallyThe user's email address. Required if externalId isn't provided.

User Identifier Requirements

Provide exactly one of externalId or email to identify the user. If your wrapper API supports only one identifier, you can omit the other from the specification entirely.

Response

HTTP/1.1 204 No Content

HTTP Response Codes

StatusDescription
204 No ContentThe user was successfully deprovisioned
400 Bad RequestThe request body was missing required fields, or included both externalId and email.
401 UnauthorizedThe Bearer token was missing, invalid, or failed tenant validation.
404 Not FoundNo user was found matching the provided externalId or email.

Validate Customer Requests

Use the Wristband JWT SDK to validate incoming access tokens at your deprovisioning endpoint. Extract the tnt_id claim from the validated token to identify the tenant associated with the request, since the request payload doesn't include a tenant identifier.

Using Your Own API Keys

If you're using your own API key system, validate incoming requests using your existing authentication mechanism. You'll also need an alternative way to determine the tenant associated with each request, since your API keys won't necessarily contain a tnt_id claim.

Code Example

The following example demonstrates how to use the Wristband TypeScript JWT SDK to validate the incoming access token and extract the tnt_id claim:

import { createWristbandJwtValidator } from '@wristband/typescript-jwt';
 
// Create once and reuse across requests.
const wristbandJwtValidator = createWristbandJwtValidator({
  wristbandApplicationVanityDomain: process.env.APPLICATION_VANITY_DOMAIN!,
});
 
async function validateCustomerRequest(authorizationHeader: string): Promise<string> {
  // Pull the raw Bearer token out of the Authorization header.
  const token = wristbandJwtValidator.extractBearerToken(authorizationHeader);
 
  // Verify the token's signature, issuer, and expiration.
  const result = await wristbandJwtValidator.validate(token);
 
  if (!result.isValid) {
    throw new Error(`Invalid token: ${result.errorMessage}`);
  }
 
  // A valid signature means this tnt_id can be trusted as the calling customer's tenant.
  const tenantId = result.payload?.tnt_id;
  if (!tenantId) {
    throw new Error('Token is missing a tenant ID');
  }
 
  return tenantId;
}

Authenticate Requests to Wristband

Use a dedicated application-level M2M client that you control to authenticate requests from your wrapper API to Wristband.

To set up your client, follow the Application-Level Clients guide.

Assign the client a role with the Application permission boundary to allow it to query and manage users across tenants. Then, grant the role the permissions listed in the table below based on your deprovisioning requirements.

PermissionUsed For
user:readLooking up a user by email or externalId
user:update, user:manage-statusDeactivating a user (updating status to INACTIVE)
user:deleteDeleting a user
client:rotate-secretRotating your customer's tenant-level client's secret on their behalf. Only needed if you're exposing a secret rotation endpoint (covered later in this page).

Code Example

To authenticate requests to Wristband APIs, your wrapper API needs an access token. You can retrieve tokens directly from the Token Endpoint or use the Wristband M2M SDK, which automatically retrieves and caches tokens in memory.

The following example shows how to initialize the TypeScript M2M SDK and use it to retrieve an access token.

import { createWristbandM2MClient } from '@wristband/typescript-m2m-auth';
 
const wristbandM2MClient = createWristbandM2MClient({
  clientId: process.env.APPLICATION_CLIENT_ID!,
  clientSecret: process.env.APPLICATION_CLIENT_SECRET!,
  wristbandApplicationVanityDomain: process.env.APPLICATION_VANITY_DOMAIN!,
});
 
const accessToken = await wristbandM2MClient.getToken();

Look Up the User in Wristband

Your deprovisioning endpoint needs to identify the correct Wristband user before deprovisioning them. Enterprise identity providers typically identify users by an externalId or email, while Wristband uses its own userId.

To find the corresponding Wristband user, call the Query Tenant Users API with a filter matching the user's externalId or email.

If the tenant has only one identity provider enabled, the query should return at most one matching user because externalId and email are unique within a tenant and identity provider. Extract the user's userId from the response and use it in subsequent deprovisioning operations.

Already Mapping Users to Wristband IDs?

If your application already stores the Wristband userId with each local user record, you can retrieve it directly from your database instead of calling the Query Tenant Users API.

Code Example

The following example demonstrates how to call the Wristband Query Tenant Users API using TypeScript to resolve the Wristband userId:

async function findUserIdByEmail(tenantId: string, email: string, accessToken: string): Promise<string> {
  const query = encodeURIComponent(`email eq '${email}'`);
  const response = await fetch(
    `https://${process.env.APPLICATION_VANITY_DOMAIN}/api/v1/tenants/${tenantId}/users?query=${query}`,
    {
      headers: {
        Authorization: `Bearer ${accessToken}`,
      },
    }
  );
 
  if (!response.ok) {
    throw new Error(`Failed to query user: ${response.status}`);
  }
 
  const { items } = await response.json();
  if (items.length === 0) {
    throw new Error('No matching user found');
  }
 
  return items[0].id;
}
async function findUserIdByExternalId(tenantId: string, externalId: string, accessToken: string): Promise<string> {
  const query = encodeURIComponent(`externalId eq '${externalId}'`);
  const response = await fetch(
    `https://${process.env.APPLICATION_VANITY_DOMAIN}/api/v1/tenants/${tenantId}/users?query=${query}`,
    {
      headers: {
        Authorization: `Bearer ${accessToken}`,
      },
    }
  );
 
  if (!response.ok) {
    throw new Error(`Failed to query user: ${response.status}`);
  }
 
  const { items } = await response.json();
  if (items.length === 0) {
    throw new Error('No matching user found');
  }
 
  return items[0].id;
}

Choose a Deprovisioning Method

Wristband supports two ways to deprovision a user, depending on whether you want to retain or permanently remove their user record.

  • Deactivate a user: Use the Patch User API to set the user's status to INACTIVE. This preserves the user record but prevents the user from authenticating. You can restore access later by setting the status back to ACTIVE.
  • Delete a user: Use the Delete User API to permanently delete the user's record. This action cannot be undone.

Code Example

The following TypeScript example demonstrates how to deactivate a user using the Patch User API or permanently remove them using the Delete User API.

async function deactivateUser(userId: string, accessToken: string) {
  const response = await fetch(`https://${process.env.APPLICATION_VANITY_DOMAIN}/api/v1/users/$USERID`, {
    method: 'PATCH',
    headers: {
      Authorization: `Bearer ${accessToken}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ status: 'INACTIVE' }),
  });
 
  if (!response.ok) {
    throw new Error(`Failed to deactivate user: ${response.status}`);
  }
}
async function deleteUser(userId: string, accessToken: string) {
  const response = await fetch(`https://${process.env.APPLICATION_VANITY_DOMAIN}/api/v1/users/$USERID`, {
    method: 'DELETE',
    headers: {
      Authorization: `Bearer ${accessToken}`,
    },
  });
 
  if (!response.ok) {
    throw new Error(`Failed to delete user: ${response.status}`);
  }
}

Putting It All Together

The following TypeScript example brings together the key components covered in this guide, including token proxying, access token validation, user lookup, and deprovisioning. The example uses framework-agnostic functions that you can adapt to your existing backend infrastructure.

Other Languages and Frameworks

If you're using a different language or framework, see Authentication SDKs for available M2M and JWT validation SDKs.

import { createWristbandM2MClient } from '@wristband/typescript-m2m-auth';
import { createWristbandJwtValidator } from '@wristband/typescript-jwt';
 
const VANITY_DOMAIN = process.env.APPLICATION_VANITY_DOMAIN!;
 
// M2M client used to authenticate the wrapper API's own calls to Wristband.
const wristbandM2MClient = createWristbandM2MClient({
  clientId: process.env.APPLICATION_CLIENT_ID!,
  clientSecret: process.env.APPLICATION_CLIENT_SECRET!,
  wristbandApplicationVanityDomain: VANITY_DOMAIN,
});
 
// JWT validator used to validate the customer's incoming access tokens.
const wristbandJwtValidator = createWristbandJwtValidator({
  wristbandApplicationVanityDomain: VANITY_DOMAIN,
});
 
// Validates a customer's incoming request and returns the tenant it belongs to.
async function validateCustomerRequest(authorizationHeader: string): Promise<string> {
  const token = wristbandJwtValidator.extractBearerToken(authorizationHeader);
  const result = await wristbandJwtValidator.validate(token);
 
  if (!result.isValid) {
    throw new Error(`Invalid token: ${result.errorMessage}`);
  }
 
  const tenantId = result.payload?.tnt_id;
  if (!tenantId) {
    throw new Error('Token is missing a tenant ID');
  }
 
  return tenantId;
}
 
// Resolves a Wristband userId from either an email or an externalId.
async function findUserId(
  tenantId: string,
  email: string | undefined,
  externalId: string | undefined,
  accessToken: string
): Promise<string> {
  const filter = email ? `email eq '${email}'` : `externalId eq '${externalId}'`;
  const query = encodeURIComponent(filter);
  const response = await fetch(
    `https://${VANITY_DOMAIN}/api/v1/tenants/${tenantId}/users?query=${query}`,
    { headers: { Authorization: `Bearer ${accessToken}` } }
  );
 
  if (!response.ok) {
    throw new Error(`Failed to query user: ${response.status}`);
  }
 
  const { items } = await response.json();
  if (items.length === 0) {
    throw new Error('No matching user found');
  }
 
  return items[0].id;
}
 
// Deactivates a user identified by email or externalId.
export async function deactivateUserByEmailOrExternalId(
  authorizationHeader: string,
  email: string | undefined,
  externalId: string | undefined
) {
  if ((!email && !externalId) || (email && externalId)) {
    throw new Error('Provide exactly one of email or externalId');
  }
 
  const tenantId = await validateCustomerRequest(authorizationHeader);
  const accessToken = await wristbandM2MClient.getToken();
  const userId = await findUserId(tenantId, email, externalId, accessToken);
 
  const response = await fetch(`https://${VANITY_DOMAIN}/api/v1/users/$USERID`, {
    method: 'PATCH',
    headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ status: 'INACTIVE' }),
  });
 
  if (!response.ok) {
    throw new Error(`Failed to deactivate user: ${response.status}`);
  }
}
import { createWristbandM2MClient } from '@wristband/typescript-m2m-auth';
import { createWristbandJwtValidator } from '@wristband/typescript-jwt';
 
const VANITY_DOMAIN = process.env.APPLICATION_VANITY_DOMAIN!;
 
// M2M client used to authenticate the wrapper API's own calls to Wristband.
const wristbandM2MClient = createWristbandM2MClient({
  clientId: process.env.APPLICATION_CLIENT_ID!,
  clientSecret: process.env.APPLICATION_CLIENT_SECRET!,
  wristbandApplicationVanityDomain: VANITY_DOMAIN,
});
 
// JWT validator used to validate the customer's incoming access tokens.
const wristbandJwtValidator = createWristbandJwtValidator({
  wristbandApplicationVanityDomain: VANITY_DOMAIN,
});
 
// Validates a customer's incoming request and returns the tenant it belongs to.
async function validateCustomerRequest(authorizationHeader: string): Promise<string> {
  const token = wristbandJwtValidator.extractBearerToken(authorizationHeader);
  const result = await wristbandJwtValidator.validate(token);
 
  if (!result.isValid) {
    throw new Error(`Invalid token: ${result.errorMessage}`);
  }
 
  const tenantId = result.payload?.tnt_id;
  if (!tenantId) {
    throw new Error('Token is missing a tenant ID');
  }
 
  return tenantId;
}
 
// Resolves a Wristband userId from either an email or an externalId.
async function findUserId(
  tenantId: string,
  email: string | undefined,
  externalId: string | undefined,
  accessToken: string
): Promise<string> {
  const filter = email ? `email eq '${email}'` : `externalId eq '${externalId}'`;
  const query = encodeURIComponent(filter);
  const response = await fetch(
    `https://${VANITY_DOMAIN}/api/v1/tenants/${tenantId}/users?query=${query}`,
    { headers: { Authorization: `Bearer ${accessToken}` } }
  );
 
  if (!response.ok) {
    throw new Error(`Failed to query user: ${response.status}`);
  }
 
  const { items } = await response.json();
  if (items.length === 0) {
    throw new Error('No matching user found');
  }
 
  return items[0].id;
}
 
// Deletes a user identified by email or externalId.
export async function deleteUserByEmailOrExternalId(
  authorizationHeader: string,
  email: string | undefined,
  externalId: string | undefined
) {
  if ((!email && !externalId) || (email && externalId)) {
    throw new Error('Provide exactly one of email or externalId');
  }
 
  const tenantId = await validateCustomerRequest(authorizationHeader);
  const accessToken = await wristbandM2MClient.getToken();
  const userId = await findUserId(tenantId, email, externalId, accessToken);
 
  const response = await fetch(`https://${VANITY_DOMAIN}/api/v1/users/$USERID`, {
    method: 'DELETE',
    headers: { Authorization: `Bearer ${accessToken}` },
  });
 
  if (!response.ok) {
    throw new Error(`Failed to delete user: ${response.status}`);
  }
}

Client Secret Rotation (Optional)

You can allow customers to rotate their M2M client secrets programmatically by exposing a secret rotation endpoint.

When a customer requests a rotation, your API uses its application-level M2M client to call the Rotate Client Secret API. The application-level client must have the client:rotate-secret permission assigned to it.

Rotating a secret generates a new primary secret and moves the existing primary secret to the secondary slot, allowing customers to transition to the new credentials without downtime.

Suggested Specification

The following section provides a suggested specification for your custom secret rotation endpoint. You can adapt the endpoint design and request/response payloads to fit your application's requirements.

Request

POST /api/v1/auth/rotate-secret HTTP/1.1
Host: yourapp.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json

Headers

NameRequiredDescription
AuthorizationYesA Bearer token authenticating your customer's call, the same as the deprovision endpoint.
Content-TypeYesMust be application/json.

Response

HTTP/1.1 200 OK
Content-Type: application/json
 
{
  "clientSecret": "new-secret-value..."
}

HTTP Response Codes

StatusDescription
200 OKThe secret was rotated, and the new secret is included in the response body.
401 UnauthorizedThe Bearer token was missing, invalid, or failed tenant validation.

Code Example

You can simplify secret rotation by exposing a single endpoint that handles the entire process. When a customer requests rotation, your backend rotates the client secret and immediately deletes the old secret from the secondary slot using the Delete Secondary Client Secret API. This approach simplifies the customer workflow, but requires them to deploy the new secret immediately because the old secret will no longer be valid.

Alternative: Use Two Separate Endpoints

If customers need time to deploy and verify the new secret before the old one is invalidated, expose two separate endpoints: one to rotate the secret and another to delete the secondary secret.

The customer workflow would be:

  1. Call your Rotate Secret endpoint to generate a new primary secret.
  2. Deploy the new secret across their systems and verify that it works.
  3. Call your Delete Secondary Secret endpoint to invalidate the old secret.
import { createWristbandM2MClient } from '@wristband/typescript-m2m-auth';
import { createWristbandJwtValidator } from '@wristband/typescript-jwt';
 
const VANITY_DOMAIN = process.env.APPLICATION_VANITY_DOMAIN!;
 
const wristbandM2MClient = createWristbandM2MClient({
  clientId: process.env.APPLICATION_CLIENT_ID!,
  clientSecret: process.env.APPLICATION_CLIENT_SECRET!,
  wristbandApplicationVanityDomain: VANITY_DOMAIN,
});
 
const wristbandJwtValidator = createWristbandJwtValidator({
  wristbandApplicationVanityDomain: VANITY_DOMAIN,
});
 
// NOTE: This is the part that could be split out into its own endpoint,
// called separately once your customer has deployed and confirmed the
// new secret. See the callout above.
async function deleteSecondarySecret(customerClientId: string, accessToken: string) {
  const response = await fetch(`https://${VANITY_DOMAIN}/api/v1/clients/${customerClientId}/secondary-secret`, {
    method: 'DELETE',
    headers: { Authorization: `Bearer ${accessToken}` },
  });
 
  if (!response.ok) {
    throw new Error(`Failed to delete secondary secret: ${response.status}`);
  }
}
 
export async function rotateCustomerClientSecret(authorizationHeader: string): Promise<string> {
  const token = wristbandJwtValidator.extractBearerToken(authorizationHeader);
  const result = await wristbandJwtValidator.validate(token);
 
  if (!result.isValid) {
    throw new Error(`Invalid token: ${result.errorMessage}`);
  }
 
  // The client_id claim identifies exactly which client this token was issued to.
  const customerClientId = result.payload?.client_id;
  if (!customerClientId) {
    throw new Error('Token is missing a client ID');
  }
 
  const accessToken = await wristbandM2MClient.getToken();
 
  const rotateResponse = await fetch(`https://${VANITY_DOMAIN}/api/v1/clients/${customerClientId}/rotate-secret`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${accessToken}` },
  });
 
  if (!rotateResponse.ok) {
    throw new Error(`Failed to rotate secret: ${rotateResponse.status}`);
  }
 
  const { clientSecret } = await rotateResponse.json();
 
  // Delete the old secondary immediately, so the customer only calls one endpoint.
  await deleteSecondarySecret(customerClientId, accessToken);
 
  return clientSecret;
}






















Did this page help you?