Admin SSO and provisioning
Configure single sign-on for an organization's admins (the people who edit and publish handbooks), with just-in-time provisioning and role mapping, so you never have to invite admins manually.
PUT /organizations/{orgId}/sso/admin | Access token
{
"protocol": "saml",
"idp": {
"entityId": "https://idp.acme.com/saml/metadata",
"ssoUrl": "https://idp.acme.com/saml/sso",
"certificate": "-----BEGIN CERTIFICATE-----\nMIID…\n-----END CERTIFICATE-----"
},
"jitProvisioning": true,
"roleAttribute": "airmason_role",
"roleMapping": {
"hr-admin": "owner",
"hr-editor": "handbook_editor",
"people-ops": "admin"
},
"defaultRole": null,
"loginRedirectUrl": "https://idp.acme.com/login",
"logoutRedirectUrl": "https://idp.acme.com/logout?from=airmason",
"enabled": true,
"enforced": true
}Field | Notes |
|
|
| Your identity provider's entity ID, SSO URL and signing certificate (PEM). |
| When |
| Name of the SAML attribute carrying the user's role |
| Your role values → AirMason roles. Exactly these values are accepted: |
| Role for users whose attribute value has no mapping. |
| Optional https URL of your sign-in page. Admins who signed in through your SSO are sent here instead of the AirMason sign-in page (see Login and logout redirects). |
| Optional https URL. Admins who signed in through your SSO are sent here when they sign out (see Login and logout redirects). |
| Defaults to |
| When |
The response describes both sides of the handshake. Certificates are write-only: only their expiry dates come back, in certificateExpiries (one per certificate, in the order you sent them). certificateExpiresAt is the first certificate's expiry.
{
"protocol": "saml",
"enabled": true,
"enforced": true,
"idp": {
"entityId": "https://idp.acme.com/saml/metadata",
"ssoUrl": "https://idp.acme.com/saml/sso",
"certificateExpiresAt": "2028-03-01T00:00:00.000Z",
"certificateExpiries": ["2028-03-01T00:00:00.000Z"]
},
"sp": {
"entityId": "airmason-admin:1f2a7b3c-9d4e-4f5a-8b6c-0d1e2f3a4b5c",
"acsUrl": "https://api.airmason.com/v1/sso/saml/partner/1f2a7b3c-9d4e-4f5a-8b6c-0d1e2f3a4b5c/login"
},
"jitProvisioning": true,
"roleAttribute": "airmason_role",
"roleMapping": { "hr-admin": "owner", "hr-editor": "handbook_editor", "people-ops": "admin" },
"defaultRole": null,
"loginRedirectUrl": "https://idp.acme.com/login",
"logoutRedirectUrl": "https://idp.acme.com/logout?from=airmason"
}Configure sp.entityId (audience) and sp.acsUrl in your IdP. Both are per organization: sp.entityId is airmason-admin:<organization id> and sp.acsUrl is /v1/sso/saml/partner/<organization id>/login. If you reuse one identity-provider application across several tenants, make sure each assertion carries the audience of the organization it is for. Role, name and email changes in your IdP are applied on the user's next sign-in.
GET returns the current configuration in the same shape (404 when nothing is configured). DELETE returns 204, disables SSO, re-enables password login and erases the stored certificates.
PATCH /organizations/{orgId}/sso/admin changes only the fields you send and leaves the rest as configured. It takes the same fields and validation as PUT, returns the full configuration, and answers 404 when nothing is configured (it never creates one). Sending idp.certificate or idp.certificates replaces all stored certificates:
{ "idp": { "certificates": ["-----BEGIN CERTIFICATE-----\nMIID…(new)…", "-----BEGIN CERTIFICATE-----\nMIID…(current)…"] } }
Roles
Role | Can | Admin app preset |
| Everything (the organization's account owner) | Owner (not offered when inviting) |
| Everything: handbooks, employees, settings | Account Admin |
| Create, edit, publish and delete handbooks | Document Content Manager |
| View and edit handbooks (not create, publish or delete) | Document Content Editor |
| Manage employees | — |
| View handbooks, signatures and analytics (no changes) | Viewer |
Identifying the user
AirMason reads the assertion's NameID and these SAML attributes (in the AttributeStatement, by Name):
Attribute | Required | Notes |
NameID (the assertion's | Always | Your stable id for the person, at most 100 characters, compared case-insensitively. It identifies the admin, so it must never change and never be reused for someone else. Any format is accepted; |
| Always | The admin's sign-in email. Used to find an existing admin the first time a NameID is seen. The attribute name cannot be configured. |
| To create a new admin (JIT) | Re-applied on every sign-in. If one is missing on a later sign-in, the stored value is kept. |
your | If you use | See |
The NameID identifies the admin. Send your stable user id (your directory's immutable id, not the email). AirMason does not request a NameID format, so the one your IdP is configured with is used (
persistentrecommended). On each sign-in AirMason looks the admin up by NameID first:Known NameID: this is the admin. If the email changed, the new email is saved on their account; the sign-in is refused (
account_conflict) when another AirMason user already has that email.New NameID: AirMason looks for an admin with the email and links your NameID to them. If that account is already linked to a different NameID, the sign-in is refused (
account_conflict).Neither: with
jitProvisioningon, a new admin is created; otherwise the sign-in is refused (not_a_member).Use the same id as
sourceIdin admin sync (below): an admin you sync who then signs in is one account.An assertion without
emailor NameID, or a new admin withoutfirstName/lastName, is refused ("Looks like there is an issue with configured claims/attributes").
Signing and security requirements
| Requirement |
Signature | The Assertion must be signed. Signing the Response as well is optional. A signed Response around an unsigned Assertion is rejected. |
Certificate | The signature must verify against one of the configured certificates. To rotate keys without downtime: |
Algorithms | RSA-SHA256 with a SHA-256 digest (recommended) or RSA-SHA512 / SHA-512. SHA-1 is deprecated and HMAC is not accepted. Use exclusive C14N with the enveloped-signature transform. |
Encryption | Encrypted assertions are not supported. Send a signed plaintext assertion over HTTPS. |
Assertions | Exactly one per Response. |
Audience / Issuer |
|
Requests | AirMason does not sign AuthnRequests. Don't require signed requests. |
Clock | Up to 3 minutes of clock skew is tolerated on |
Login and logout redirects
Both fields apply only to sessions that started with your SSO for this organization. AirMason uses the URL exactly as configured (no parameters, no token).
Field | The admin is sent there when |
| they sign out. Use your IdP's logout endpoint to sign them out of both systems, or a landing page to sign them out of AirMason only. |
| their session expires (2 hours idle), they open an AirMason link with no session, or AirMason rejects your assertion. |
Admins who signed in with a password are never redirected. The URL comes from the organization they signed in through, even if they switch organizations later.
For expired sessions and links,
loginRedirectUrlonly works in a browser that has already signed in through your SSO. In a new browser the AirMason sign-in page is shown. If a second assertion is rejected within 2 minutes, the admin sees the AirMason error page with its reference code, which prevents a redirect loop.PUT replaces the whole configuration, so send both fields on every PUT (send
nullto remove one), or use PATCH to change only one. The employee endpoint rejects both with400 validation_error.
When sign-in fails
The admin sees a message ending in a reference code, for example "Single sign-on failed. Please contact your administrator. Reference: 3f9a1c". (If AirMason rejects your assertion and loginRedirectUrl is set, the admin is sent there instead.) Each failure is also recorded in GET /organizations/{orgId}/activity:
{
"type": "sso.login_failed",
"occurredAt": "2026-10-14T15:02:11.000Z",
"handbookId": null,
"actor": { "type": "system" },
"employeeId": null,
"description": null,
"details": { "referenceCode": "3f9a1c", "category": "role_not_mapped", "email": "jane@acme.com" }
}
| Cause |
| Signature, certificate, expiry, audience, encryption or more than one assertion (no |
| No |
| No NameID, a blank one, or one longer than 100 characters |
| A new admin without |
| No |
| Not an admin of the organization and |
| Invited but not activated, and |
| SSO is not among the organization's admin login methods |
| The email belongs to an AirMason user outside your organizations, to another user (after an email change), or to an admin linked to a different NameID |
Filter with
?type=sso.login_failed. The same feed also returnssso.user_provisioned(details:email,role) andsso.config_updated(details:action,platform,configurationId).At most 20 failures per minute are recorded per organization.
When you contact support, include the reference code.
Admin sync
Besides just-in-time creation at SAML sign-in, you can create, update and deactivate an organization's admins directly. Both endpoints use the organization's access token.
Import or update admins
PUT /organizations/{orgId}/admins — 1 to 100 admins per request.
{
"admins": [
{ "sourceId": "u-1842", "email": "jane@acme.com", "firstName": "Jane", "lastName": "Doe", "role": "admin" },
{ "sourceId": "u-2207", "email": "john@acme.com", "firstName": "John", "lastName": "Roe", "role": "viewer" }
]
}Field | Notes |
| Required. Your stable id for the person, 1 to 100 characters, compared case-insensitively. Use the same value as the SAML NameID (see Identifying the user). It must never change and never be reused for someone else. In responses, |
| Required. The admin's sign-in email. |
| Required, at most 50 characters each. |
| One of |
Each admin is matched by sourceId first, then by email:
Known
sourceId: this is the admin. A different email updates their account's email (sameid, same role and membership, reported asupdated), unless another AirMason user already has that email.New
sourceId: AirMason looks for someone with the email and links yoursourceIdto them. If they are already linked to a differentsourceId, the row fails.Neither: a new admin is created and added to the organization.
Someone you deactivated earlier is reactivated. Rows are processed in order and each one is reported separately, so one failure does not stop the others:
{
"summary": { "created": 1, "updated": 1, "failed": 0 },
"results": [
{
"index": 0,
"sourceId": "u-1842",
"email": "jane@acme.com",
"status": "created",
"admin": {
"id": 10112,
"sourceId": "u-1842",
"email": "jane@acme.com",
"firstName": "Jane",
"lastName": "Doe",
"role": "admin",
"status": "active",
"activatedAt": "2026-10-13T16:06:11.000Z"
}
},
{ "index": 1, "sourceId": "u-2207", "email": "john@acme.com", "status": "updated", "admin": { … } }
]
}A row that cannot be applied comes back with
"status": "failed"and anerrorobject:409"This email is already used by another AirMason user" (thesourceIdis known and its new email belongs to someone else),409"This email belongs to an admin with a different sourceId" (that account is already linked to anothersourceId),409"This email belongs to an AirMason user who cannot be added by this partner" (an AirMason account outside your organizations),409"Cannot change the role of the last owner of the organization",409"This user cannot be managed through the Partner API" (AirMason staff accounts), or400when the role is not available for the organization.The whole request is rejected with
400 validation_errorfor an empty list or more than 100 admins, a missing or too longsourceId, an invalid email, a missing first or last name, an unknown role, or the same email orsourceIdtwice (case-insensitive); and with409if the organization is deactivated.Re-sending the same admin with the same values is a no-op reported as
updated.If the email already belongs to an admin of another of your organizations, they are added to this organization with the requested role, but their name is not changed; the response shows the name on their existing account. An email already used by an AirMason account outside your organizations is refused (
409).Imported admins are not sent an invitation email and have no password. They sign in through the organization's admin SSO (above), or set a password with "Forgot password" on the admin sign-in page.
Deactivate an admin
POST /organizations/{orgId}/admins/{adminRef}/deactivate — {adminRef} is the admin's id from the import response, ext:<sourceId> (for example ext:u-1842), or their email. This removes access to this organization only; the person keeps any other organizations they belong to. It is idempotent: deactivating an already-deactivated admin returns 200 with "status": "inactive" again. Importing the admin again reactivates them.
Errors: 404 "Admin not found" (not a member of this organization), 409 for the last owner of the organization, an AirMason staff account, or a deactivated organization.
Employee SSO
Employee-facing SSO (for the handbook portal) is configured at PUT /organizations/{orgId}/sso/employee (and updated with PATCH, as above) with the same protocol, idp, enabled and enforced fields, but it is not configured the same way as admin SSO:
jitProvisioning,roleAttribute,roleMapping,defaultRole,logoutRedirectUrlandidp.certificatesare rejected with400 validation_error(send one certificate asidp.certificate), and the response carries no role fields. Employees are matched to the records you created through the API.sp.entityIdandsp.acsUrlare both<portal url>/custom_saml/saml2, for examplehttps://books.airmason.com/acme-corp/custom_saml/saml2.
Partner API documentation: