skip to content

Serving SCIM 2.0 /Users, what does a successful create return, and what must a colliding create return instead?

level: middleimportance: must knowfreq 38%

answer

  1. two namespaces, one resource
  2. who mints it decides who trusts it
  3. the constraint decides, not a pre-check
  4. conflict is a status, not an exception
  5. 409 plus scimType uniqueness

basics

~20 s

A successful create returns 201 with the resource as stored, including the id your service minted, plus a Location header. A create whose userName already exists returns 409 carrying scimType uniqueness — never a silent overwrite, a 200, or a 500 from a constraint.

solid answer

~50 s

A POST to `/Users` is another organisation's software creating a person in your product, so every part of your answer is read by a state machine. A success is **201** with the resource as you stored it — not an echo of the request — carrying the `id` you minted and the `meta` attribute with its version, plus a `Location` header. The `id` is yours and read-only; `externalId` is the client's own key for the same person and you store it without interpreting it. A create whose `userName` already exists is a conflict, and the contract is **409** with `scimType` set to `uniqueness` in the error body. That is what tells a conforming client to stop retrying the create, look the person up, and switch to an update. A 200 hides the collision, and a 500 from a database constraint tells the client to retry forever.

code

http · 23 lines
http
POST /scim/v2/Users HTTP/1.1
Authorization: Bearer <credential scoped to one practice group>
Content-Type: application/scim+json

{
  "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
  "externalId": "7f3a1c9e-practice-hr-key",
  "userName": "r.okonkwo",
  "active": true
}

HTTP/1.1 201 Created
Location: https://viewer.example/scim/v2/Users/2819c223-7f76-453a-919d-413861904646
Content-Type: application/scim+json

{
  "schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
  "id": "2819c223-7f76-453a-919d-413861904646",
  "externalId": "7f3a1c9e-practice-hr-key",
  "userName": "r.okonkwo",
  "active": true,
  "meta": { "resourceType": "User", "version": "W/\"3694e05e9dff590\"" }
}

go deeper

for a junior

Remember that a create here comes from another organisation's software, not a person filling in a form, and that the answer is a status code something automated will act on. 201 for success, 409 for a name already taken.

for a middle

Explain the two namespaces: the id the service provider mints and the externalId the provisioning client mints, and why an email address is neither. Then explain why a conflict is a 409 with scimType uniqueness and not an exception that escaped.

for a senior

Show that the uniqueness decision belongs to a database constraint scoped to the tenant, not to a read-then-write in application code, and that a resent create is indistinguishable from a real duplicate — so the same answer has to be correct for both.

for a principal

Argue what your product's account identity actually is and what changing it later costs. Choosing the client's key, your own, or the login name determines whether a customer can ever switch identity systems without losing the link between people and their records.

## What a create means on this endpoint A SCIM 2.0 service provider publishes a `/Users` collection, and a **provisioning client** — the customer's identity system, software you do not run and cannot patch — creates people in your product by POSTing a User resource to it. In a dental group's radiography viewer this is how every dentist, hygienist, radiographer and practice manager gets an account: nobody types them in, and nobody clicks a signup form. The create is therefore not a convenience wrapper around your registration flow. It is the contract by which another organisation's software populates your account table, and every part of your answer — status code, body, headers — is consumed by a state machine with no human in it. A successful create returns **201**. The body is the resource **as you stored it**, which is deliberately not an echo of the request: it carries what the service provider assigned. At minimum that is the `id` you minted and the `meta` attribute holding the resource's version and location. A `Location` header points at the new resource. A conforming client records your `id` against its own record of that person and uses it for every later read, replace and PATCH. ## Two identifiers, and whose namespace each lives in This leaf's single most common confusion is answering "the user's id" without saying whose id. Four things claim the role and only one of them is the resource's identity: | identifier | who mints it | who may change it | what it is for | |---|---|---|---| | `id` | the service provider — you | nobody; it is read-only | the resource's URL and the client's handle on it | | `externalId` | the provisioning client | the client | the client's own key for the same person, opaque to you | | `userName` | the client, in the request body | the client, by a later update | a unique login name, and the attribute collisions are usually about | | your local primary key | your database | nobody | your internal joins; it need not be the value you expose as `id` | An email address belongs in none of those rows. It is mutable, it is frequently shared inside a small practice, and a client that changes it expects the person to survive the change. ## The collision, and why 409 is not optional The client resends. It resends because its own job timed out, because a queue redelivered, because an administrator re-ran a sync. A create it already made will therefore arrive again, and a second person genuinely may share a `userName` if the customer reuses one. Both surface at the same place: a unique constraint. What your handler does, in order: 1. Authenticate the credential the write arrived on and resolve the one tenant it is scoped to. 2. Validate the body against the schema you advertise, rejecting what you will not store rather than dropping it silently. 3. Mint the `id` yourself. It is read-only, so a value the client put there is not authoritative and you disregard it. 4. Insert under a unique constraint on the tenant and `userName` together, letting the database arbitrate the race rather than a read-then-write in application code. 5. Translate a constraint violation into **409** with `scimType` `uniqueness` and a `detail` string a support engineer can act on. 6. Return **201** with the stored resource and a `Location` header. Step five is the one people skip, and it is the one the client depends on. A 409 with `uniqueness` says *this person is already here* — the client stops creating, issues a lookup, and converts its intent into an update. A bare 500 says *try again later*, so it does, forever, and the customer's sync sits red. A 200 with the existing resource is worse than either: the client believes it created a person it did not create, and any difference between what it sent and what you hold is now permanent and invisible. ## What this costs when it is wrong - **Duplicate humans.** Keying on email rather than a stable identifier creates a second account when a dentist marries and changes name, and the images filed under the old account stay there. - **Silent drift.** A create that quietly wins against an existing record lets the client's view and yours diverge with nothing logging the difference. - **An unreadable failure.** A 409 with no `scimType` leaves the customer's administrator staring at "conflict" with nothing to do about it. - **A race you wrote yourself.** Checking for existence and then inserting is two statements; two concurrent creates from one sync will both pass the check. The discipline is small and it holds the rest of the endpoint up: you own `id`, the client owns `externalId`, the database owns uniqueness, and the status code is how you say which of those spoke.

  • The provisioning endpoint authenticates with a bearer credential. What must that credential be scoped to, and why does it matter here specifically?
    One tenant — one practice group — and nothing else. Every write on this endpoint names people by attributes the client chose, so the only thing standing between two customers' user tables is the authority you derived from the credential. Resolve the tenant from it once, at the edge, and make the uniqueness constraint and every lookup carry that tenant. A credential that can write across tenants turns one leaked secret into an estate-wide account-creation primitive.
  • A create times out at the client after you committed it. What does the client's resend look like, and what does a well-built service provider do?
    It looks exactly like a genuine duplicate, because it is the same body again. You answer 409 with `scimType` `uniqueness`, which is the correct answer to both cases. The conforming client then issues a filtered read on `userName`, finds the resource with the `id` it never received, and continues. That is why the filter subset is load-bearing even for clients that only ever create and update.
  • The client puts an `id` in the create body. What do you do with it?
    Disregard it and mint your own. `id` is read-only and belongs to the service provider; accepting a client-supplied one hands another organisation's software the power to choose your primary keys and, worse, to collide them deliberately. If the client needs its own key carried, that is what `externalId` is for and you store it verbatim without interpreting it.

It is the difference between a hospital's own patient number and the referral number the clinic that sent them wrote on the form. Both identify the same person, only one of them is yours to issue, and filing by the wrong one is how a second chart gets opened.

saying these in an interview costs you the question

  • Returns 200 with the existing user so the client believes it created one
  • Treats the email address as the key shared between both systems
  • Lets the provisioning client choose the resource id
  • Answers a collision with 500 because a constraint threw
  • Returns 409 with no scimType, leaving the client nothing to act on
  • Checks for existence and then inserts, and loses the concurrent create