SSO-Only User Deprovisioning
Give your customers a way to immediately revoke a user's access to your application, without Wristband ever storing that user's data.
SSO-Only Integrations OnlyThis page covers deprovisioning users in SSO-only integrations, where your application owns and stores its own users. If your users are provisioned and stored in Wristband, refer to User Deprovisioning instead.
In an SSO-only integration, Wristband only brokers the login handshake. Your own application owns the session and access tokens afterward. Revoking access in the organization's IdP stops future logins, but doesn't affect access your application already granted. That employee can keep using the application until your session expires or you explicitly deprovision them.
As with the User Deprovisioning documentation for Wristband-stored users, you can 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.
Confirm Requirements Before BuildingOnly consider building a wrapper API if your customer requires automated, self-service deprovisioning and that requirement isn't negotiable.
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. - Deactivate or delete that user directly in your own system.
That first step, authenticating the customer, is where Wristband comes in.
Already Have Your Own API Keys?If your application already has an API key system, you can reuse it instead, and skip 1) Create a Tenant-Level M2M Client for Your Customer and 2) Create a Token Endpoint Wrapper API entirely. Be aware that Wristband would not be involved, and authenticating incoming requests would be entirely your responsibility.
The rest of this page assumes you're using a Wristband tenant-level M2M client to authenticate your customer instead.
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
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->>Wrapper: Deactivate or delete user
Wrapper-->>Customer: 200 or 204
1) Create a Tenant-Level M2M Client for Your Customer
Important: Skip this section if you are using your own API key system.
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, it doesn't need any Wristband resource permissions.
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
Important: Skip this section if you are using your own API key system.
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
Since your users live entirely in your own system, this endpoint doesn't need to resolve a Wristband userId at all. It just needs enough information to identify the user in your own database: either an externalId matching what your customer's identity provider uses or the user's email.
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 | Whatever identifier you use to look up the user in your own system. 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 identifier. |
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;
}Mapping by Tenant Name
The token's tnt_id claim identifies the tenant by its Wristband ID. If your own system maps customers by the Wristband tenant name instead of tenant ID, you'll need to include the tenant's name in the access token.
The tenant's name isn't included in the access token by default. Add it as a custom claim from Security → Token Settings in Application View using the tenant.name path expression and a claim name of your choosing (e.g., tnt_name).

Token Settings: Add a custom claim using the tenant.name path expression.
Once configured, it appears nested under custom_claims in the token payload. For example:
{
"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",
"custom_claims": {
"tnt_name": "acme"
}
}Deprovision the User
Once the request is validated, look up the user in your own system using the externalId or email from the request, then deactivate or delete their record directly. This is entirely your own application logic, Wristband isn't involved at this step.
Your wrapper API doesn't necessarily need to support both deactivating and deleting. You can decide upfront to only ever do one, 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 token endpoint and the deprovision 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 JWT validation SDKs.
import { createWristbandJwtValidator } from '@wristband/typescript-jwt';
const VANITY_DOMAIN = process.env.APPLICATION_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;
}
// Deprovisions a user identified by either externalId or email.
export async function deprovisionUser(
authorizationHeader: string,
externalId: string | undefined,
email: string | undefined
) {
if ((!externalId && !email) || (externalId && email)) {
throw new Error('Provide exactly one of externalId or email');
}
const tenantId = await validateCustomerRequest(authorizationHeader);
// NOTE: This is where you look up and deactivate or delete the user in
// your own system. Scope the lookup to `tenantId` if your data model
// supports it.
await yourOwnUserStore.deactivateOrDelete({ tenantId, externalId, email });
}(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.
This works the same way as it does for Wristband-stored users. Refer to Client Secret Rotation on the main User Deprovisioning page for the full spec and code example, since the mechanics are identical, only the deprovisioning logic itself differs between the two pages.
Updated about 1 hour ago