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.
Authentication
Section titled “Authentication”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.
Bootstrap and the key lifecycle
Section titled “Bootstrap and the key lifecycle”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:
- Confirm there is exactly one live superadmin account, and that its email matches the
operatorEmailininstall-contract.yaml. If more than one superadmin exists, usePOST /superadmin/superadminswith a live superadmin’s key instead — this reopens-bootstrap path is only for the case where no key can authenticate at all. - Delete that superadmin’s rows in the API key store scoped to the superadmin key configuration, leaving them with zero keys.
- 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.
Error responses
Section titled “Error responses”Every gated route can return these; individual endpoints below call out the domain-specific ones that also apply.
| Status | error | Meaning | Seen from |
|---|---|---|---|
401 | unauthorized | The x-api-key header is missing, or the key doesn’t verify. | Every gated route |
403 | forbidden | The key verifies, but its owner’s account isn’t a live superadmin (soft-deleted or demoted). | Every gated route |
400 | invalid_request | The request body failed validation. | Any route with a body |
404 | not_found | The 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 |
409 | SUPERADMIN_DOMAIN_OWNED_BY_ANOTHER_ORG | A domain in the request already belongs to a different organization. | POST /superadmin/orgs, PUT /superadmin/orgs/{id}/domains |
409 | SUPERADMIN_DOMAIN_REMOVAL_WHILE_SSO_ENFORCED | Removing a domain would break sign-in while SSO is enforced for that organization; retry with force: true to override. | PUT /superadmin/orgs/{id}/domains |
400 | SSO_ORG_DOMAIN_NOT_CONFIGURED | The organization has no verified SSO domain yet. | POST /superadmin/orgs/{id}/sso-connection |
Endpoints
Section titled “Endpoints”All paths are relative to https://api.<baseDomain>.
POST /superadmin/bootstrap
Section titled “POST /superadmin/bootstrap”The one unauthenticated route. First-call-wins.
Body:
Responses: 201 with the newly-minted key, once —
{ "customerOrgId": "...", "key": "sk-...", "superadminUserId": "...", "systemAccountUserId": "...", "systemOrgId": "..."}— or 409 { "error": "already_bootstrapped" } once a superadmin already exists.
curl -X POST "https://api.<baseDomain>/superadmin/bootstrap" \ -H "Content-Type: application/json" \POST /superadmin/key
Section titled “POST /superadmin/key”Rotates the caller’s own key. No body.
Response: 201 { "key": "sk-..." } — the new key, shown once.
curl -X POST "https://api.<baseDomain>/superadmin/key" \ -H "x-api-key: $SUPERADMIN_KEY"POST /superadmin/orgs
Section titled “POST /superadmin/orgs”Creates a customer organization and its admin user.
Body:
{ "name": "Example Corp", "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": "..." }curl -X POST "https://api.<baseDomain>/superadmin/orgs" \ -H "x-api-key: $SUPERADMIN_KEY" \ -H "Content-Type: application/json" \GET /superadmin/orgs
Section titled “GET /superadmin/orgs”Lists every organization.
Response: 200 { "orgs": [{ "id", "name", "type", "adminEmails", "domains", "ssoEnforced" }, ...] }
curl "https://api.<baseDomain>/superadmin/orgs" \ -H "x-api-key: $SUPERADMIN_KEY"GET /superadmin/status
Section titled “GET /superadmin/status”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 } }
curl "https://api.<baseDomain>/superadmin/status" \ -H "x-api-key: $SUPERADMIN_KEY"PUT /superadmin/orgs/{id}/domains
Section titled “PUT /superadmin/orgs/{id}/domains”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": [...] }
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"]}'POST /superadmin/superadmins
Section titled “POST /superadmin/superadmins”Promotes an existing user to superadmin.
Body:
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.
curl -X POST "https://api.<baseDomain>/superadmin/superadmins" \ -H "x-api-key: $SUPERADMIN_KEY" \ -H "Content-Type: application/json" \DELETE /superadmin/superadmins/{id}
Section titled “DELETE /superadmin/superadmins/{id}”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 }
curl -X DELETE "https://api.<baseDomain>/superadmin/superadmins/$USER_ID" \ -H "x-api-key: $SUPERADMIN_KEY"POST /superadmin/orgs/{id}/sso-connection
Section titled “POST /superadmin/orgs/{id}/sso-connection”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"]}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.
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"}'