User Deprovisioning
Give your customers a way to immediately revoke a user's access to your application when that user leaves their organization.
Wristband-Stored Users OnlyThis page covers deprovisioning users who are provisioned and stored in Wristband. If your application owns and stores its own users instead (meaning Wristband only brokers SSO authentication), refer to SSO-Only Integration instead.
User deprovisioning removes or restricts a user's access to your application within Wristband, typically because they've left the organization or changed roles. This is distinct from your customer deactivating the user on their side within their own identity provider.
Why This Matters
Your enterprise customers manage employee access centrally through their identity provider. When an employee leaves that organization or changes roles, they typically expect that change to cut off the employee's access everywhere, including your application.
Revoking an employee's access within the organization's IdP stops them from logging in to your application again, but it doesn't affect any access they may already have. That employee can keep using the application until their existing access expires, which could be hours or days later.
Explicitly deprovisioning the user in Wristband closes most of that gap. It immediately ends the user's Wristband auth session and blocks their refresh token, so the next time they try to log in or refresh their access token, they're denied access.
Existing Access Tokens Still WorkIf the user already holds an access token that hasn't expired yet, that token remains usable for the rest of its lifetime since access tokens aren't rechecked against user status on every request. Configure a shorter access token expiration to reduce the size of this window. Refer to JWT Settings to learn more.
Ways to Deprovision a User
There are a few different ways to approach deprovisioning, depending on how urgently your customers need it.
Don't Do It At All
You can choose not to deprovision at all. The user's record stays in your system, but once your customer deactivates them in their own IdP (Okta, Entra, etc.), they can no longer authenticate into your application through SSO. This works if your customers are comfortable relying solely on IdP-side deactivation.
Handle It Manually via Support
Your customer reaches out, for example through a support ticket, and asks you to deprovision a specific user. Someone on your team then deactivates or deletes the user through the Wristband dashboard. This requires no engineering work, but it doesn't scale and depends on your team being responsive.
Automate It With a Wrapper API
You build your own API layer (referred to throughout this page as a "wrapper API") that your customer calls directly to trigger deprovisioning themselves without needing to contact you.
The rest of this page covers how to implement the wrapper API option.
Confirm Requirements Before BuildingOnly consider building a wrapper API if your customer requires automated, self-service deprovisioning and that requirement isn't negotiable. It's the most work to build and maintain of the three options, so it's worth confirming the need is real before investing in it.
How the Wrapper API Works
Your customer calls a deprovision endpoint that you expose. Since your customer's system never interacts with Wristband directly, it won't have a Wristband userId on hand. It will only have whatever identifies the user on their own side, such as an externalId or email.
Your endpoint should:
- Authenticate the incoming customer request.
- Validate the target user's
externalIdoremailfrom the request. - Authenticate itself to Wristband to obtain an access token for making API calls.
- Look up the corresponding Wristband user by tenant and
externalIdoremailto get their WristbanduserId. - Use that
userIdto deactivate or delete the user.
Here's how that looks end to end:
%%{init: {'themeVariables': {'fontSize': '16px'}}}%%
sequenceDiagram
participant Customer as Customer's backend
participant Wrapper as Your wrapper API
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
To authenticate customer requests to your wrapper API, you can create a Wristband tenant-level M2M client just for them. They can use the clientId and clientSecret to obtain access tokens from Wristband and send those tokens with each request, which your wrapper API should validate before processing the request.
Refer to Tenant-Level Clients for instructions on creating a tenant-level M2M client. Since this client is only used to authenticate the customer's identity to your wrapper API (and not to call Wristband directly), it doesn't need any Wristband resource permissions.
Already Have Your Own API Keys?If your application already has an API key system, you could reuse that instead, if desired. Be aware that Wristband would not be involved, and authenticating incoming requests would be entirely your responsibility.
The rest of this page assumes you are using a Wristband tenant-level M2M client to authenticate your customer. This approach does require exposing an additional endpoint to fetch access tokens (covered later on this page).
Once created, share the following information with your customer through a secure channel:
| Field | Description |
|---|---|
| Client ID | The clientId of the tenant-level M2M client you created for this customer. |
| Client Secret | The clientSecret of the tenant-level M2M client you created for this customer. |
2) Create a Token Endpoint Wrapper API
Since your customer never calls Wristband directly, they need somewhere to send their tenant-level M2M client's credentials to get access tokens. Expose a wrapper API that accepts those credentials, proxies the request to Wristband's Token Endpoint, and returns the access token to your customer.
Suggested Spec
Below is one suggested spec for this endpoint. The exact design is entirely up to you.
Request
POST /api/v1/auth/token HTTP/1.1
Host: yourapp.com
Content-Type: application/json
{
"clientId": "abc123",
"clientSecret": "def456"
}Headers
| Name | Required | Description |
|---|---|---|
Content-Type | Yes | Must be application/json. |
Body
| Field | Type | Required | Description |
|---|---|---|---|
clientId | string | Yes | The clientId of the tenant-level M2M client you issued to this customer. |
clientSecret | string | Yes | The 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
| Status | Description |
|---|---|
200 OK | The credentials were valid, and an access token is included in the response body. |
400 Bad Request | The request body was missing clientId or clientSecret. |
401 Unauthorized | The 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
Wristband's own APIs for deactivating and deleting a user require a Wristband userId to identify which user to act on. Your customer's system doesn't interact with Wristband directly, so it will never have that userId on hand. Your deprovision endpoint should instead accept either an externalId or email and resolve the Wristband user from there.
Suggested Spec
Below is one suggested spec for this endpoint. The exact design is entirely up to you.
Request
POST /api/v1/users/deprovision HTTP/1.1
Host: yourapp.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
{
"email": "[email protected]"
}Headers
| Name | Required | Description |
|---|---|---|
Authorization | Yes | A Bearer token authenticating your customer's call. See Validate Your Customer's Requests. |
Content-Type | Yes | Must be application/json. |
Body
| Field | Type | Required | Description |
|---|---|---|---|
externalId | string | Conditionally | The user's identifier in your customer's identity provider. Required if email isn't provided. |
email | string | Conditionally | The user's email address. Required if externalId isn't provided. |
Note: Exactly one of
externalIdor
Response
HTTP/1.1 204 No ContentHTTP Response Codes
| Status | Description |
|---|---|
204 No Content | The user was successfully deprovisioned |
400 Bad Request | The request body was missing required fields, or included both externalId and email. |
401 Unauthorized | The Bearer token was missing, invalid, or failed tenant validation. |
404 Not Found | No user was found matching the provided externalId or email. |
Validate Your Customer's Requests
In your deprovision endpoint, validate incoming access tokens using a Wristband JWT SDK. Since the deprovision endpoint itself doesn't include any customer or tenant identifier in the request, the token's tnt_id claim is how you determine who's calling it.
For reference, a decoded access token for a tenant-level M2M client looks like this:
{
"sub": "sqi7mel4prbixj62rlruslyi64",
"tnt_id": "ffioj5rc3bd6rnwr7jxhvycfoa",
"van_dom": "invotastic-demo-wristband.us.wristband.dev",
"iss": "https://invotastic-demo-wristband.us.wristband.dev",
"sub_kind": "tenant_client",
"exp": 1785197092,
"app_id": "vl6jdobxy5gafkr3dvztbvzrue",
"iat": 1785110692,
"jti": "5hbgvgrlejglplvwokwttvazha",
"client_id": "sqi7mel4prbixj62rlruslyi64"
}Once the token's signature is verified, treat tnt_id as trusted, and use it to look up whatever you need to know about that customer.
Code Example
For example, using the Typescript JWT SDK to validate the token and return the tenantId:
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;
}This tenantId is what you'll pass into the lookup functions in the Look Up the User section (covered later on this page).
Authenticate Your Requests to Wristband
Your wrapper API must authenticate its own calls to Wristband's User APIs using an Application-level M2M client that you create and control, not the one issued to your customer. Refer to Application-Level Clients for details on how to create and configure this client.
Assign your application-level M2M client a role scoped with the Application permission boundary, since it needs to act on users across every tenant it serves. Include whichever of the following permissions match the actions it performs:
| Permission | Used For |
|---|---|
user:read | Looking up a user by email or externalId |
user:update, user:manage-status | Deactivating a user (updating status to INACTIVE) |
user:delete | Deleting a user |
client:rotate-secret | Rotating 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
Your wrapper API can obtain its access token directly from the Token Endpoint, or by using a Wristband M2M SDK to handle token retrieval and caching automatically.
For example, using the Typescript M2M SDK:
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
Since your endpoint only receives an externalId or email, it first needs to resolve that into a Wristband userId before it can deactivate or delete the user. Call the Query Tenant Users API, filtering by either email or externalId, to resolve the Wristband userId.
Already Have Your Own Mapping?If your application already maintains a mapping between your users and their Wristband
userId, for example because you store it alongside your own internal user record, you can look it up there instead of calling Wristband.If you instead map customers by tenant name rather than tenant ID,
tenantNameisn't included in the access token by default. You'll need to add it as a custom token claim first, using thetenant.namepath expression. Once configured, it appears nested undercustom_claimsin the token payload.
Code Example
For example, using the Query Tenant Users API in Typescript:
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 How to Deprovision
Wristband offers two ways to deprovision a user, depending on whether you want to preserve the user's record.
- Deactivating a user: Use the Patch User API to set the user's
statustoINACTIVE. The user's record is preserved, but they can no longer authenticate. This is reversible by setting the status back toACTIVE. - Deleting a user: Use the Delete User API to permanently remove the user's record. This can't be undone.
Code Example
For example, using the Patch User API and Delete User API in Typescript:
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}`);
}
}Your wrapper API doesn't necessarily need to support both. You can decide upfront to only ever deactivate, or only ever delete, and build your deprovision endpoint around that single choice. If you do want to support both, your endpoint needs some way to distinguish which one a given request wants (e.g., a separate endpoint per action, an HTTP method per action, or a field in the request body).
Putting It All Together
Below is a single, complete TypeScript example of everything covered on this page: the wrapper API's own M2M client, JWT validation, user lookup, deactivate/delete, and the token endpoint. It's written as plain functions, framework-agnostic, so you can wire them into whatever HTTP framework you're using.
Additional LanguagesFor other languages and frameworks, refer to Authentication SDKs for the full list of 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}`);
}
}(Optional) Client Secret Rotation
Since your customer holds a long-lived client secret for their tenant-level M2M client, you may want to let them rotate the secret on their own schedule without involving you directly.
Your wrapper API's own Application-level M2M client rotates the secret on your customer's behalf by calling the Rotate Client Secret API, using the client:rotate-secret permission listed in Authenticate Your Requests to Wristband, scoped with the Application permission boundary just like the rest of that client's permissions.
Rotating generates a new primary secret and demotes the old primary to secondary. Both remain valid until the secondary is explicitly deleted. For the underlying mechanics of rotating, validating, and deleting a secondary secret, refer to the Rotating a Client's Secret section on the Tenant-Level Clients page.
Suggested Spec
Below is one suggested spec for this endpoint. The exact design is entirely up to you.
Request
POST /api/v1/auth/rotate-secret HTTP/1.1
Host: yourapp.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/jsonHeaders
| Name | Required | Description |
|---|---|---|
Authorization | Yes | A Bearer token authenticating your customer's call, the same as the deprovision endpoint. |
Content-Type | Yes | Must be application/json. |
Response
HTTP/1.1 200 OK
Content-Type: application/json
{
"clientSecret": "new-secret-value..."
}HTTP Response Codes
| Status | Description |
|---|---|
200 OK | The secret was rotated, and the new secret is included in the response body. |
401 Unauthorized | The Bearer token was missing, invalid, or failed tenant validation. |
Code Example
The example below treats rotation like a typical API key "regenerate" action. Your customer only ever calls one endpoint in this scenario. The old secondary secret is deleted immediately after rotation via the Delete Secondary Client Secret API (which reuses the same client:rotate-secret permission).
An Alternative: Two Separate EndpointsIf your customer needs time to roll the new secret out across their own systems before the old one is cut off, you can split this into two separate endpoints instead. One endpoint only rotates the secret, while a second endpoint only deletes the secondary secret.
The customer would then do the following:
- Call your Rotate Secret wrapper API to fetch a new primary client secret
- Deploy the primary secret on their side and confirm it works as expected
- Call your Delete Secondary Secret wrapper API
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;
}Updated about 1 hour ago