A customer asked for SCIM on a Friday, and we thought it would take six weeks. It took two. That gap between estimate and reality is mostly a knowledge problem, not an engineering one. SCIM has a reputation for complexity it does not quite deserve.
The harder question is not how to build it. It is whether to build it at all. SCIM shows up on enterprise security questionnaires and procurement checklists, which creates pressure to treat it as a box to tick. That pressure is worth resisting until your deals actually need it.
01
What SCIM actually is
SCIM, the System for Cross-domain Identity Management, is a REST API standard defined in RFC 7643 and RFC 7644. An identity provider like Okta or Azure AD calls your SCIM endpoint to push user and group changes. When an employee joins, Okta sends a POST /Users to your app and the account appears. When they are offboarded from the HR system the same night, Okta sends a DELETE /Users/{id} and the account is gone, within minutes of the termination record being created. No manual work, no forgotten active accounts, no ex-employee browsing your dashboard weeks after their last day.
The wire format is JSON with a defined schema for user attributes. Groups are first-class objects too. SCIM 2.0 is the version everything targets today. You can read the full spec, but in practice you only implement a slice of it.
02
When you do not need SCIM
If your customers pay monthly on a credit card, manage fewer than 50 seats, and onboard themselves without involving IT, SCIM will sit unused. Seat-based SaaS at the SMB tier does not have an HR system talking to an identity provider. Users sign up with an email address and a Google login. The admin who bought the subscription also manages the team manually. That is fine. Building SCIM for that customer profile means building infrastructure that nobody calls.
You also do not need SCIM if you already have SSO working and your churn risk from the enterprise segment is low. Just-in-time provisioning, where an account is created the first time a user signs in via SAML, covers most of what smaller enterprise accounts actually need. The gap SCIM fills is deprovision speed and bulk group sync. If those are not on a security questionnaire you have received, they are probably not blocking deals.
03
When you do need SCIM
Enterprise compliance teams have a simple problem. People leave. Access should end the same day. Manual deprovision relies on someone remembering to do it, which is not a control an auditor accepts. SCIM solves this by putting the termination flow into the HR system: the manager submits the offboarding ticket, the identity provider is notified, your app gets the delete call. The audit log shows the timestamp. Nobody had to remember anything.
Three signals that SCIM is worth building now. First: a deal above $50k ARR is blocked by a security review that lists SCIM. Second: you are selling to a customer with 500 or more employees who uses Okta or Azure AD as their identity platform. Third: you have already shipped SAML SSO and the next question on every enterprise questionnaire is provisioning. SAML gets users in. SCIM gets users in and out.
04
What a minimal SCIM endpoint looks like
A functional SCIM implementation does not require a framework. You need a handful of routes and a way to map SCIM user attributes to your internal user model. Here is a minimal Users endpoint in TypeScript using Hono:
import { Hono } from "hono";
import type { Context } from "hono";
// SCIM user schema (subset of RFC 7643)
interface ScimUser {
schemas: string[];
id: string;
externalId?: string;
userName: string;
name?: { givenName?: string; familyName?: string };
emails?: Array<{ value: string; primary?: boolean }>;
active: boolean;
}
function toScimUser(user: { id: string; email: string; name: string; active: boolean }): ScimUser {
return {
schemas: ["urn:ietf:params:scim:schemas:core:2.0:User"],
id: user.id,
userName: user.email,
name: { givenName: user.name.split(" ")[0], familyName: user.name.split(" ")[1] },
emails: [{ value: user.email, primary: true }],
active: user.active,
};
}
export const scimUsers = new Hono();
// List users (Okta calls this to reconcile state)
scimUsers.get("/", async (c: Context) => {
const users = await db.users.list({ tenantId: c.get("tenantId") });
return c.json({
schemas: ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
totalResults: users.length,
Resources: users.map(toScimUser),
});
});
// Create user
scimUsers.post("/", async (c: Context) => {
const body = await c.req.json<ScimUser>();
const user = await db.users.create({
tenantId: c.get("tenantId"),
email: body.userName,
name: [body.name?.givenName, body.name?.familyName].filter(Boolean).join(" "),
active: body.active ?? true,
});
return c.json(toScimUser(user), 201);
});
// Update user (PUT replaces, PATCH updates fields)
scimUsers.put("/:id", async (c: Context) => {
const body = await c.req.json<ScimUser>();
const user = await db.users.update(c.req.param("id"), {
active: body.active,
name: [body.name?.givenName, body.name?.familyName].filter(Boolean).join(" "),
});
return c.json(toScimUser(user));
});
// Delete user (triggered on HR termination via Okta)
scimUsers.delete("/:id", async (c: Context) => {
await db.users.deactivate(c.req.param("id"));
return new Response(null, { status: 204 });
});Notice the delete route calls deactivate rather than hard-deleting the row. That is the right pattern. You want the audit trail intact. Okta also frequently sends PATCH to set active: false instead of deleting outright. Handle both. The full API reference covers the PATCH operation format and attribute mapping in detail.
05
The four operations that matter
Every SCIM identity provider integration ultimately needs four things from your endpoint. List lets the IdP pull your current user state and reconcile it against its own records. This is called a sync sweep and Okta runs it periodically. You do not control the schedule.
Create handles new employee onboarding. When an IT admin assigns your app to a user in the Okta admin console, Okta calls your POST /scim/v2/Users within seconds. The account should be usable immediately after you respond with a 201.
Update covers profile changes: name, department, email alias. Some customers push these through SCIM; others do not bother. You should accept them but they are rarely load-bearing for a deal.
Delete is the one that closes contracts. When an HR system marks someone as terminated, the chain runs: HR system notifies Okta, Okta calls your delete endpoint, the account is deactivated. An auditor can look at the timestamp on the delete call and confirm compliance with the offboarding SLA. That is worth real money to the right buyer. See permissions docs for how to model role revocation at the same time as deactivation.
06
Testing with Okta
Okta has a free developer tenant that you can use to test SCIM integration before any customer is involved. Create a free account at developer.okta.com, create an app integration, and set the provisioning tab to use a custom SCIM endpoint. You will need a public URL, so ngrok http 4100 or a deployed preview is the simplest path.
Okta will run a connectivity test when you save the configuration. It does a GET /scim/v2/Users?count=1 and validates the response schema. If that passes, assign a test user in Okta and watch your logs. You will see the POST, and you can verify the account appears. Then unassign the user. The delete call should follow within about 30 seconds.
Topics
- #SCIM
- #SCIM 2.0
- #user provisioning
- #enterprise SSO
- #Okta provisioning
- #Azure AD SCIM
Keep going in the docs
Agents
Permissions
Wildcard matching, scope contracts, and inline policy checks. How to say what an agent can do.
Reference
Framework adapters
Drop-in adapters for Hono, Express, Next.js, Fastify, Nuxt, SvelteKit, Astro, and NestJS.
Reference
API reference
Every function, option, and return type in the kavachOS SDK.
Read next
Share this post
Get started
Try kavachOS Cloud free
SCIM provisioning, SAML SSO, and agent identity on every plan.