Skip to content

Superadmin API

The /superadmin/* HTTP API on the api host is the operator surface for a Self-Managed Kindo install: creating and reading customer organizations, managing their domains and SSO connections, and managing other superadmins. It is the surface kindo install and kindo upgrade themselves call to bootstrap the first superadmin, and it is the surface an operator scripts against for everything covered on Configure & Validate.

The superadmin dashboard (https://superadmin.<baseDomain>) ships with every install, but it has no operator sign-in yet — it is not a usable surface until that ships. Every action — creating an organization, setting its domains, configuring its SSO connection, adding and removing superadmins, and reading status — goes through this API.

Every route except POST /superadmin/bootstrap requires a superadmin API key in the x-api-key header:

x-api-key: <your superadmin key>

There is exactly one live key per superadmin at a time. A key expires SUPERADMIN_API_KEY_TTL_DAYS days after it is minted (default 90); rotate it before it expires with POST /superadmin/key, authenticated with the current key. Rotation mints the new key first and then revokes the old one, so a failed rotation never leaves you with zero working keys.

Do not send a superadmin key in Authorization — the middleware reads x-api-key only.

kindo install --apply and kindo upgrade --apply both call POST /superadmin/bootstrap with the operatorEmail from install-contract.yaml, and print the returned key to the terminal exactly once. The route is first-call-wins: once any superadmin exists, a later call returns 409 already_bootstrapped and changes nothing. Re-running the step standalone — kindo install --step superadmin-bootstrap — is always safe to retry.

Store the key immediately. There is no CLI command or API route to retrieve a lost key.

Recovery when the only superadmin loses their key

Section titled “Recovery when the only superadmin loses their key”

Bootstrap reopens for exactly one situation: a single live superadmin whose account matches the requested operatorEmail and who holds zero live API keys. If the only superadmin on an install loses their key, an operator with database access can restore that state and let kindo install --step superadmin-bootstrap mint a fresh one:

  1. Confirm there is exactly one live superadmin account, and that its email matches the operatorEmail in install-contract.yaml. If more than one superadmin exists, use POST /superadmin/superadmins with a live superadmin’s key instead — this reopens-bootstrap path is only for the case where no key can authenticate at all.
  2. Delete that superadmin’s rows in the API key store scoped to the superadmin key configuration, leaving them with zero keys.
  3. Re-run kindo install --step superadmin-bootstrap. It mints and prints a fresh key for that same email.

This never creates a new organization or a new system account — it reuses the existing ones and only replaces the missing key.

Every gated route can return these; individual endpoints below call out the domain-specific ones that also apply.

StatuserrorMeaningSeen from
401unauthorizedThe x-api-key header is missing, or the key doesn’t verify.Every gated route
403forbiddenThe key verifies, but its owner’s account isn’t a live superadmin (soft-deleted or demoted).Every gated route
400invalid_requestThe request body failed validation.Any route with a body
404not_foundThe referenced row — an org id, a superadmin id — doesn’t exist.PUT /superadmin/orgs/{id}/domains, DELETE /superadmin/superadmins/{id}, POST /superadmin/superadmins, POST/PATCH /superadmin/orgs/{id}/sso-connection
409SUPERADMIN_DOMAIN_OWNED_BY_ANOTHER_ORGA domain in the request already belongs to a different organization.POST /superadmin/orgs, PUT /superadmin/orgs/{id}/domains
409SUPERADMIN_DOMAIN_REMOVAL_WHILE_SSO_ENFORCEDRemoving a domain would break sign-in while SSO is enforced for that organization; retry with force: true to override.PUT /superadmin/orgs/{id}/domains
400SSO_ORG_DOMAIN_NOT_CONFIGUREDThe organization has no verified SSO domain yet.POST /superadmin/orgs/{id}/sso-connection

All paths are relative to https://api.<baseDomain>.

The one unauthenticated route. First-call-wins.

Body:

{ "operatorEmail": "[email protected]" }

Responses: 201 with the newly-minted key, once —

{
"customerOrgId": "...",
"key": "sk-...",
"superadminUserId": "...",
"systemAccountUserId": "...",
"systemOrgId": "..."
}

— or 409 { "error": "already_bootstrapped" } once a superadmin already exists.

Terminal window
curl -X POST "https://api.<baseDomain>/superadmin/bootstrap" \
-H "Content-Type: application/json" \
-d '{"operatorEmail": "[email protected]"}'

Rotates the caller’s own key. No body.

Response: 201 { "key": "sk-..." } — the new key, shown once.

Terminal window
curl -X POST "https://api.<baseDomain>/superadmin/key" \
-H "x-api-key: $SUPERADMIN_KEY"

Creates a customer organization and its admin user.

Body:

{
"name": "Example Corp",
"adminEmail": "[email protected]",
"domains": ["example.com"]
}

domains is optional. If adminEmail already has a live account anywhere, no organization is created — the response names the account’s existing org instead.

Response: 201 when a new organization and admin user were created, 200 when the admin email already had a live account —

{ "adminUserId": "...", "created": true, "orgId": "..." }
Terminal window
curl -X POST "https://api.<baseDomain>/superadmin/orgs" \
-H "x-api-key: $SUPERADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Example Corp", "adminEmail": "[email protected]", "domains": ["example.com"]}'

Lists every organization.

Response: 200 { "orgs": [{ "id", "name", "type", "adminEmails", "domains", "ssoEnforced" }, ...] }

Terminal window
curl "https://api.<baseDomain>/superadmin/orgs" \
-H "x-api-key: $SUPERADMIN_KEY"

Cross-org counts and bootstrap state — the same data the dashboard’s status view shows.

Response: 200 { "bootstrapped": true, "counts": { "orgs": 3, "ssoConnections": 1, "superadmins": 1 } }

Terminal window
curl "https://api.<baseDomain>/superadmin/status" \
-H "x-api-key: $SUPERADMIN_KEY"

Sets an organization’s SSO domains to exactly the given list.

Body:

{ "domains": ["example.com", "example.org"], "force": false }

Removing a domain while SSO is enforced for that organization is rejected unless force: true is given.

Response: 200 { "domains": [...] }

Terminal window
curl -X PUT "https://api.<baseDomain>/superadmin/orgs/$ORG_ID/domains" \
-H "x-api-key: $SUPERADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"domains": ["example.com"]}'

Promotes an existing user to superadmin.

Body:

{ "email": "[email protected]" }

The email must already belong to a live account — this route never creates one. An email with no live account returns 404 { "error": "not_found" }; use POST /superadmin/orgs to create one first.

Response: 201 { "key": "sk-...", "systemAccountUserId": "...", "userId": "..." } — the new superadmin’s key, shown once.

Terminal window
curl -X POST "https://api.<baseDomain>/superadmin/superadmins" \
-H "x-api-key: $SUPERADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]"}'

Demotes a superadmin back to an ordinary user and revokes their superadmin keys. Demoting yourself, or the last superadmin, is allowed. With no superadmin left, POST /superadmin/bootstrap simply reopens as it would on a fresh install — that is not the keyless-recovery path above, which applies only when exactly one live superadmin remains and has lost their key; see Recovery when the only superadmin loses their key.

Response: 200 { "success": true }

Terminal window
curl -X DELETE "https://api.<baseDomain>/superadmin/superadmins/$USER_ID" \
-H "x-api-key: $SUPERADMIN_KEY"

Creates a SAML connection for the organization — the same operation the org admin’s own Settings → SSO screen performs.

Response: 201 with the new connection, including the values (ACS URL, SP entity ID) the customer’s IT admin enters into their Identity Provider. certificate is metadata about the stored certificate — { fingerprintSha256, notAfter } — never the PEM itself; null until one is set:

{
"providerId": "saml-...",
"spConfig": { "acsUrl": "...", "spEntityId": "..." },
"idpEntityId": "",
"redirectUrl": "",
"certificate": null,
"isConfigured": false,
"domains": ["example.com"]
}
Terminal window
curl -X POST "https://api.<baseDomain>/superadmin/orgs/$ORG_ID/sso-connection" \
-H "x-api-key: $SUPERADMIN_KEY"

PATCH /superadmin/orgs/{id}/sso-connection

Section titled “PATCH /superadmin/orgs/{id}/sso-connection”

Applies the Identity Provider’s side of the configuration once the customer’s IT admin returns it: the IdP entity ID, redirect URL, and signing certificate. Omitted fields are left untouched.

Body:

{
"idpEntityId": "https://idp.example.com/entity",
"redirectUrl": "https://idp.example.com/sso",
"certificate": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
}

Response: 200 with the updated connection, in the same shape as create.

Terminal window
curl -X PATCH "https://api.<baseDomain>/superadmin/orgs/$ORG_ID/sso-connection" \
-H "x-api-key: $SUPERADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"idpEntityId": "https://idp.example.com/entity", "redirectUrl": "https://idp.example.com/sso"}'