Docs
Open the console →
API reference

API reference: SAML & SCIM

Cushy SAML 2.0 SSO endpoints (metadata, SP-initiated login, ACS), the per-organization SAML/SCIM/group-mapping configuration APIs, and the SCIM 2.0 provisioning surface (Users, Groups, discovery).

SAML 2.0 sign-in (public)

GET/api/auth/saml/metadataPublic

SP metadata XML (entity ID + HTTP-POST ACS). Import this into your IdP. Add ?org=<slug> to include that organization's Single Log-Out block — its SingleLogoutService endpoints (Redirect and POST) and the SP signing certificate the IdP needs to verify our LogoutRequest. The SP signing key is per organization, so the un-scoped document carries no certificate.

Parameters
NameInRequiredDescription
orgquerynoOrganization slug — adds the SLO endpoints + SP signing certificate.
Example response
xml
<md:EntityDescriptor entityID="https://host/api/auth/saml/metadata">…</md:EntityDescriptor>
Errors
StatusWhen
200Always — SP metadata is public by design (it contains no secret).
POST/api/auth/saml/loginPublic

Begin SP-initiated SSO: match the work email domain / org slug to an enabled SAML config and return the IdP redirect URL (deflate+base64 AuthnRequest).

Parameters
NameInRequiredDescription
emailbodynoWork email (its domain is matched).
slugbodynoOrganization slug (alternative to email).
Example request
json
{ "email": "alice@corp.example" }
Example response
json
{ "ok": true, "redirectUrl": "https://idp/sso?SAMLRequest=…&RelayState=…", "requestId": "_abc…" }
Errors
StatusWhen
404SAML isn't configured for that domain/org (enumeration-safe, generic).
POST/api/auth/saml/acsPublic (form POST binding)

Assertion Consumer Service: verify the signed assertion (exc-c14n + RSA-SHA256, Conditions, single-use replay), JIT-match by email with the group-mapped role, and set a session cookie (303 to /).

Parameters
NameInRequiredDescription
SAMLResponsebodyyesbase64 SAML Response (application/x-www-form-urlencoded).
RelayStatebodynoRound-tripped from the AuthnRequest.
Example request
json
// application/x-www-form-urlencoded (HTTP-POST binding), not JSON:
SAMLResponse=<base64 signed assertion>&RelayState=/dashboard
Example response
json
// 302 redirect — never JSON.
// success → Location: <RelayState or /dashboard>  (Set-Cookie: cc_session=…)
// failure → Location: /login?sso=failed
Errors
StatusWhen
303Success — redirect to / with a session cookie.
400Bad/expired/replayed/unsigned/tampered assertion, wrong audience, or InResponseTo mismatch (generic).
403The organization is suspended.
GET/api/auth/saml/sloPublic (HTTP-Redirect binding; POST for the HTTP-POST binding)

Single Log-Out, both directions on one endpoint. A <LogoutResponse> answering a logout Cushy started is checked for signature, InResponseTo (single-use) and StatusCode — a PartialLogout is reported as partial, never as success — and the browser lands back on the login page. An IdP-initiated <LogoutRequest> must be signed and must verify against that organization's configured IdP certificate; the matching session (its SessionIndex, or every session of the principal when none is named) is revoked and Cushy replies with a signed <LogoutResponse>. On the Redirect binding the signature is the SAML Bindings §3.4.4.1 signature over the URL-encoded query string, not over the XML.

Parameters
NameInRequiredDescription
SAMLRequestquerynoDeflated+base64 LogoutRequest (IdP-initiated). Must be signed.
SAMLResponsequerynoDeflated+base64 LogoutResponse answering a Cushy-initiated logout.
RelayStatequerynoRound-tripped onto our LogoutResponse.
SigAlgquerynoMust be rsa-sha256; SHA-1 is refused.
SignaturequerynoBase64 signature over SAMLRequest|SAMLResponse=…&RelayState=…&SigAlg=….
Example response
json
// 302 redirect to the IdP's SLO endpoint (or /login when none is configured).
Errors
StatusWhen
303Success — to the IdP with a signed LogoutResponse, or to /login?signedout=1.
400Unsigned or badly-signed LogoutRequest, unknown/expired InResponseTo, wrong Destination or Issuer, expired or replayed message (all generic).

SAML config (Org Admin, session only)

POST/api/org/samlOrg Admin (session only)

Create/update the org's SAML config. Accepts pasted IdP metadata XML (auto-fills entity ID / SSO URL / cert) or explicit fields. The signing certificate is validated on save.

Parameters
NameInRequiredDescription
metadataXmlbodynoIdP metadata XML — entity ID / SSO URL / cert are extracted from it.
idpEntityIdbodynoIdP entity ID (required if no metadata).
ssoUrlbodynoIdP SSO URL (https).
sloUrlbodynoIdP single-logout URL (https). Optional — omit it and sign-out ends the Cushy session only.
certificatebodynoIdP X.509 signing cert (PEM or base64). Public value.
defaultRolebodynoRole when no group maps; default Viewer.
emailDomainsbodynoDomains routed to this IdP.
attrEmailbodynoAssertion attribute for email (blank ⇒ NameID).
attrGroupsbodynoAssertion attribute for group membership.
Example request
json
{
  "idpEntityId": "https://idp.example.com/realms/acme",
  "ssoUrl": "https://idp.example.com/realms/acme/protocol/saml",
  "sloUrl": "https://idp.example.com/realms/acme/protocol/saml",
  "certificate": "-----BEGIN CERTIFICATE-----\nMIID…\n-----END CERTIFICATE-----",
  "emailDomains": ["acme.io"],
  "defaultRole": "Viewer",
  "attrEmail": "email",
  "attrGroups": "groups"
}
// or paste the IdP descriptor instead: { "metadataXml": "<EntityDescriptor …>" }
Example response
json
{ "ok": true, "saml": { "idpEntityId": "…", "ssoUrl": "…", "sloUrl": "…", "certificateSet": true, "defaultRole": "Viewer", "emailDomains": ["corp.example"], "attrGroups": "groups", "enabled": true, "spSloUrl": "https://host/api/auth/saml/slo", "spMetadataUrl": "https://host/api/auth/saml/metadata?org=acme", "spCertificate": "-----BEGIN CERTIFICATE-----…", "sloReady": true } }
Errors
StatusWhen
400Missing entity ID, non-https SSO URL, unusable certificate, bad role/domains, or unparseable metadata.
403Caller lacks manage_users_sso.

GET /api/org/saml returns the config (the IdP cert is never echoed, only certificateSet); PATCH /api/org/saml toggles { enabled }.

Saving generates the organization's SP signing keypair if it has none — required to sign a <LogoutRequest>. spCertificate is the PUBLIC half, returned so you can install it at the IdP; the private half is encrypted at rest and is never present in any response. An existing key is never rotated by a config save.

SCIM tokens & group mappings (Org Admin, session only)

POST/api/org/scimOrg Admin (session only)

Issue a per-org SCIM bearer token (cc_scim_…, shown once) and the SCIM base URL.

Example request
json
{ "name": "keycloak-acme" }  // a label for the token you are about to mint
Example response
json
{ "ok": true, "id": 3, "token": "cc_scim_…", "scimBaseUrl": "https://host/api/scim/v2" }
Errors
StatusWhen
401Not signed in.
403Only org admins can manage SCIM tokens.

GET /api/org/scim lists token metadata; DELETE /api/org/scim/{id} revokes. GET/POST /api/org/group-mappings lists / upserts a { groupName, role } mapping; DELETE /api/org/group-mappings/{id} clears a group's role.

SCIM 2.0 provisioning (per-org Bearer token)

All /api/scim/v2/* endpoints require Authorization: Bearer <cc_scim_…> and return application/scim+json. Discovery: GET /ServiceProviderConfig, GET /ResourceTypes, GET /Schemas.

POST/api/scim/v2/UsersSCIM Bearer

Provision a user (JIT). Role starts at the SCIM default (Viewer) and is elevated by group membership. Duplicate userName → 409 uniqueness.

Example request
json
{ "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], "userName": "alice@corp.example", "name": { "formatted": "Alice A" }, "active": true, "externalId": "0oaExt…" }
Example response
json
{ "schemas": ["…:User"], "id": "42", "userName": "alice@corp.example", "active": true, "roles": [{ "value": "Viewer", "primary": true }], "meta": { "resourceType": "User" } }
Errors
StatusWhen
401Missing/invalid/revoked SCIM token (SCIM error envelope).
409A user with that userName already exists (scimType uniqueness).
PATCH/api/scim/v2/Users/{id}SCIM Bearer

RFC 7644 PatchOp. replace/add of active (active:false deprovisions = deactivate + kill sessions; active:true reactivates), displayName, externalId.

Example request
json
{ "Operations": [{ "op": "replace", "path": "active", "value": false }] }
Example response
json
{ "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"], "id": "42",
  "userName": "ada.lovelace@acme.io", "active": false,
  "meta": { "resourceType": "User", "location": "https://cushy.example.com/api/scim/v2/Users/42" } }
Errors
StatusWhen
400Malformed PatchOp body.
401Missing or invalid SCIM bearer token.
404No such user in the token's organization.

Also: GET /Users?filter=userName eq "x" (and externalId eq), GET /Users/{id}, PUT /Users/{id}, DELETE /Users/{id} (deprovision, 204). Groups: GET/POST /Groups, GET/PATCH/PUT/DELETE /Groups/{id} — PATCH members add/remove applies the group→role mapping to each member.