SenseCrypt Docs
Guides

Managing tenants

Onboard in the admin console and manage tenants — the auto-created default tenant, creating, renaming, and deleting tenants, and getting from signup to your first registered application.

A tenant is SenseCrypt's unit of isolation and its unit of identity: each tenant is its own OIDC issuer and SAML IdP, on its own hostname, with its own signing keys and its own directory of users, apps, and groups. This guide walks through onboarding in the admin console and managing tenants day to day. For the conceptual model — issuers, hostnames, keys, and the isolation boundary — read Multi-tenancy & issuers alongside this guide.

Accounts and tenants

Two levels, and the distinction matters:

  • Your account is what you create when you sign up for the console. It's the ownership and billing umbrella, and it holds your administrators.
  • A tenant is the isolation boundary underneath the account. One account can hold many tenants, and two tenants under the same account are as isolated from each other as tenants under different accounts — separate issuers, separate keys, separate directories.

So you don't integrate against "your account" — you integrate against a specific tenant's issuer.

The default tenant

Every account is created with one default tenant already in place, so you have a working issuer the moment you sign up — there's nothing to provision before you can register your first app. The default tenant behaves like any other, with one exception: it cannot be deleted. It's the anchor tenant for your account.

If a single environment is all you need, you can do everything in the default tenant. Create more tenants when you want separate, isolated environments — for example, one per customer, or separate staging and production issuers.

Creating a tenant

Add a tenant from the admin console (or via the Management API, POST /v1/admin/tenants). A tenant needs two things, and takes an optional third:

  • A display name — human-readable, shown in the console. It must be unique within your account, and you can rename it later.
  • A slug — the DNS label that becomes the tenant's subdomain, so its issuer is https://{slug}.sensecrypt.com.
  • A security profile (fapi_profile, optional) — omit it or send null for a Standard tenant, or "plain_fapi" for a FAPI 2.0/CIBA tenant whose every application is held to that profile. Like the slug it is set once, here, and can never be changed: PATCH /v1/admin/tenants/{id} refuses it as immutable_field. A tenant that needs the profile later is a new tenant with its own issuer.

The slug has strict rules, because it's baked into the public issuer URL:

  • It must be a valid lowercase DNS label — lowercase letters, digits, and internal hyphens.
  • It must be globally unique across all accounts — no two live tenants anywhere share a slug.
  • Certain infrastructure names are reserved (for example www, admin, api) and are rejected.
  • It is immutable after creation — see the warning below.

When the tenant is created, SenseCrypt automatically provisions everything it needs to be usable immediately: its signing keys (an ES256 key for OIDC, an RSA key for SAML; a second, RS256 OIDC key is created lazily, the first time one is needed to sign — registering an application for id_token_signed_response_alg: "RS256" creates no key on its own), the standard attribute schema, and the built-in console and machine-to-machine roles. Discovery, JWKS, and SAML metadata work the instant an app points at the new subdomain.

The slug and the security profile are permanent. You can rename a tenant's display name freely, but neither the slug nor fapi_profile can ever change — the slug is the public subdomain relying parties trust as the issuer, and the profile is what the discovery document promises them. Choose both deliberately.

Renaming a tenant

You can rename a tenant's display name at any time from the console (or PATCH /v1/admin/tenants/{tenant_id}). The new name must be unique within your account. Renaming only changes the human-readable label — it does not change the slug, the issuer URL, or anything a relying party depends on.

Deleting a tenant

Any tenant except the default can be deleted (DELETE /v1/admin/tenants/{tenant_id}). Deletion is a cascade: SenseCrypt removes the tenant's groups and users, revokes its OIDC apps and SAML service providers, revokes the device keys of its users, and retires its signing keys. In practical terms, everything that tenant issued stops working — tokens no longer verify against a retired key, and its clients are rejected.

Because deletion is destructive and irreversible from the console, treat it with care, especially for a tenant with live integrations. The default tenant is intentionally undeletable so your account always retains at least one working issuer.

From signup to your first app

Putting it together, the path from a new account to a working sign-in is short:

  1. Sign up for the console. Your account is created with its default tenant ready to use — its issuer is live at https://{slug}.sensecrypt.com.
  2. (Optional) Create a tenant if you want a separate isolated environment rather than using the default. Pick its slug carefully.
  3. Register an application in the tenant — an OIDC app for "Sign in with SenseCrypt", or a SAML service provider to federate an enterprise app.
  4. Wire up your integration against that tenant's issuer, resolving endpoints from {issuer}/.well-known/openid-configuration (OIDC) or {issuer}/v1/idp/saml/metadata (SAML). The Add Login (OIDC) quickstart walks this end to end.
  5. Open the access gate so users can actually sign in — application access is default-closed. See Groups and access.

Working across multiple tenants

If you run more than one tenant, treat each one as a fully separate IdP: a separate issuer, a separate JWKS, and separate application registrations. A client or token minted in one tenant is not usable in another. When you verify tokens, trust the iss claim and verify against the JWKS of that issuer — it tells you exactly which tenant issued the token. See Multi-tenancy & issuers for the full isolation guarantees.

Using the Management API

Everything above is done in the admin console. The tenant lifecycle operations also live on the tenant Management API — the /v1/admin surface served on your tenant's admin host, {tenant-host}/v1/admin. Most Management-API resources are driven programmatically with a machine-to-machine (M2M) access token: mint one with the client-credentials grant, then pass it as Authorization: Bearer {mgmtToken} — the token acts only within the one tenant it was minted for, and only up to the capabilities its bound console/M2M roles carry. See Machine-to-machine for how to obtain {mgmtToken}.

Tenant lifecycle management is owner-only — there is no M2M-token path. Creating, listing, renaming, and deleting tenants are account-owner operations: the routes require the account owner's authenticated console session and reject M2M service tokens. There is deliberately no tenants:* capability in either the console or the M2M capability catalog, so no M2M role — however broad — can grant tenant administration. Unlike users, groups, or apps (which you can drive with an M2M token), tenant CRUD has no headless-automation equivalent — perform these in the admin console as the owner. The curl blocks below are a faithful reference of exactly what the console issues under the hood (endpoint, method, request body, and response shape), authenticated by the owner's console session — an Authorization: Bearer {mgmtToken} is not accepted on these routes.

All requests and responses are JSON. The natural order mirrors the sections above.

Create a tenant

POST {tenant-host}/v1/admin/tenants — the body carries the display name, the immutable slug, and optionally the equally immutable fapi_profile:

# authenticated as the account owner (console session)
curl -X POST {tenant-host}/v1/admin/tenants \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Production",
    "slug": "prod",
    "fapi_profile": null
  }'

On success it returns 201 Created with the new tenant, including its computed public issuer URL:

{
  "id": "6f9c1e2a-3b4d-4c5e-8a9b-0c1d2e3f4a5b",
  "name": "Production",
  "slug": "prod",
  "is_default": false,
  "issuer_url": "https://prod.example.com",
  "created_at": "2026-07-16T12:00:00Z"
}

The slug is validated server-side: an invalid DNS label or a reserved name is rejected with 422 slug_invalid, a slug already live anywhere is 409 slug_taken, and a display name already used in your account is 409 tenant_name_exists.

List tenants

GET {tenant-host}/v1/admin/tenants — optional query params q (case-insensitive name substring), limit (1–200, default 50), and offset (default 0):

# authenticated as the account owner (console session)
curl "{tenant-host}/v1/admin/tenants?q=prod&limit=50&offset=0"

Returns a page (the default tenant sorts first, then newest):

{
  "items": [
    {
      "id": "6f9c1e2a-3b4d-4c5e-8a9b-0c1d2e3f4a5b",
      "name": "Production",
      "slug": "prod",
      "is_default": false,
      "issuer_url": "https://prod.example.com",
      "created_at": "2026-07-16T12:00:00Z"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}

Get one tenant

GET {tenant-host}/v1/admin/tenants/{tenant_id} returns the same tenant shape as create. A {tenant_id} that isn't one of your account's tenants returns 404 (no existence leak):

# authenticated as the account owner (console session)
curl {tenant-host}/v1/admin/tenants/{tenant_id}

Rename a tenant

PATCH {tenant-host}/v1/admin/tenants/{tenant_id} — the body accepts only name. The new name must be unique within your account:

# authenticated as the account owner (console session)
curl -X PATCH {tenant-host}/v1/admin/tenants/{tenant_id} \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Production EU"
  }'

Returns 200 OK with the updated tenant. Renaming changes only the display label — never the slug or issuer URL. Two other fields are refused here by name, each with a 422 in the validation envelope:

FieldcodeWhy
fapi_profileimmutable_fieldSet once at creation; a profile change would rewrite what discovery promises existing relying parties.
op_session_ttl_secondsfield_movedIt lives on PATCH /v1/admin/issuer now; the msg names that route.

Delete a tenant

DELETE {tenant-host}/v1/admin/tenants/{tenant_id} succeeds with 204 No Content and an empty body. Deletion cascades exactly as described above — it soft-deletes the tenant's groups and users, revokes its OIDC apps and SAML service providers and its users' device keys, and retires its signing keys:

# authenticated as the account owner (console session)
curl -X DELETE {tenant-host}/v1/admin/tenants/{tenant_id}

The default tenant cannot be deleted — attempting it returns 400 default_tenant_undeletable.

Issuer settings

Unlike tenant CRUD, the Issuer surface is capability-gated rather than owner-only, so it is reachable with an M2M token: GET and PATCH {tenant-host}/v1/admin/issuer, gated on issuer:read and issuer:update. It is the tenant-host view of what relying parties see.

GET returns the issuer, discovery and JWKS URLs, the immutable fapi_profile, the shape discovery advertises right now (advertised_profile) with its cause (advertised_cause: tenant when the tenant was created on the profile, applications when a Standard tenant's every live OIDC application adopts it), the counts applications_total and applications_fapi, and non_compliant_applications — the applications keeping a Standard tenant on the standard shape.

It also returns the browser-session lifetime twice, and the pair is the point: op_session_ttl_seconds is the tenant's raw override (null when it has none) and op_session_ttl_effective_seconds is the value actually in force after the deployment default is applied. So you can read back the lifetime you just PATCHed, and see what a null resolves to.

non_compliant_applications has three states, and they are not interchangeable: a list of applications, [] when there are none, and null when the caller may not see the names — an M2M token whose roles do not carry oidc_apps:read (an owner session holds every capability, so it always sees the list). On null the names are withheld, though applications_total and applications_fapi still come back, so the number is derivable; a caller holding only issuer:read must not read null as "none". The list is also a first page of at most 200 names, ordered by name, so on a large tenant it is not the whole set: the count is applications_totalapplications_fapi, never the length of the list.

PATCH holds exactly one writable field, op_session_ttl_seconds — the OP browser-session lifetime, an integer from 0 to 2592000 (30 days). null falls back to the deployment default; 0 disables browser sessions entirely, so every authorization runs a fresh face ceremony and prompt=none always answers login_required. A fractional value is refused, not rounded.

# authenticated with the issuer:update capability
curl -X PATCH {tenant-host}/v1/admin/issuer \
  -H "Content-Type: application/json" \
  -d '{ "op_session_ttl_seconds": 3600 }'
  • Multi-tenancy & issuers — issuers, hostnames, per-tenant keys, and isolation.
  • Add Login (OIDC) — register an app and get sign-in working.
  • Groups and access — open the default-closed access gate so users can sign in.
  • The tenant routes (/v1/admin/tenants) are a console-plane surface and are not part of the published API reference; the console's Tenants page is the supported way to create, rename and inspect tenants. Their contract changes are recorded in the changelog.

On this page