Refapp SCIM API — supported surface
Companion to Setting up SCIM provisioning with Microsoft Entra ID and SCIM authentication.
The Refapp SCIM API is not a complete implementation of SCIM 2.0. It implements the subset that Microsoft Entra ID's provisioning service actually uses, and it is shaped by that decision throughout. If you are configuring Entra, you will not notice the gaps. If you are writing your own SCIM client, read this page first — the parts we do not implement are listed here rather than left to be discovered.
Everything on this page is advertised by the API itself. Rather than trusting this document, ask your own tenant:
GET <root>/scim/v2/<companyId>/ServiceProviderConfig GET <root>/scim/v2/<companyId>/ResourceTypes GET <root>/scim/v2/<companyId>/Schemas
All three require the same bearer token as the rest of the API. If those documents ever disagree with this page, they are correct.
Supported capabilities
From ServiceProviderConfig:
| Capability | Supported | Notes |
|---|---|---|
patch |
Yes | PATCH is how Entra sends nearly every update |
filter |
Yes | Heavily restricted — see Filtering. maxResults is 100 |
bulk |
No | maxOperations and maxPayloadSize are both 0 |
sort |
No | sortBy and sortOrder are accepted and silently ignored |
etag |
No | No If-Match / If-None-Match concurrency control |
changePassword |
No | Refapp accounts have no IdP-settable password |
The only authentication scheme is oauthbearertoken (RFC 6750) — a static bearer token you generate in Refapp. There is no OAuth token endpoint and no client-credentials flow. See SCIM authentication.
Resources
Exactly two, from ResourceTypes:
| Resource | Endpoint | Schema |
|---|---|---|
User |
/Users |
urn:ietf:params:scim:schemas:core:2.0:User |
Group |
/Groups |
urn:ietf:params:scim:schemas:core:2.0:Group |
A Refapp Group is a subaccount — the resource description reads "Identity provider group mapped onto Refapp subaccounts". Groups are not permissions and not teams; they select which subaccounts a user can work in. Roles are carried on the User resource instead, under roles.
The enterprise user extension (urn:ietf:params:scim:schemas:extension:enterprise:2.0:User) is declared on Userwith required: false. We read exactly one attribute from it: costCenter. Nothing else in that extension is stored — manager, employeeNumber, department, division and organization are accepted and discarded.
There are no Refapp-proprietary schema extensions. Everything we consume is standard SCIM.
User attributes
| Attribute | Type | Multi | Mutability | Required |
|---|---|---|---|---|
userName |
string | readOnly, unique per server | Yes | |
name.givenName |
string | readWrite | ||
name.familyName |
string | readWrite | ||
emails[].value / .type / .primary |
complex | yes | readWrite | |
phoneNumbers[].value / .type |
complex | yes | readWrite | |
preferredLanguage |
string | readWrite | Yes | |
active |
boolean | readWrite | Yes | |
roles[].value |
complex | yes | readWrite | |
externalId |
string | readWrite | (see below) |
Three of these deserve emphasis, because each is a way to get a create rejected or a user into a state you did not intend:
userNameisreadOnlybecause it is derived, not stored. A Refapp user's identifier is their email address, anduserNameis simply that address read back. You never set it — not at creation and not later — and a write that targetsuserNameis ignored rather than rejected.The address itself is mutable: set it throughemailsanduserNamefollows it automatically on the next read. So "read-only" here means "this field is a view of the address, edit the address instead", not "the address is fixed".It is unique across the server, so moving a user onto an address another user already holds answers409.userNameechoes the address you filtered on. Where a company uses plus-addressing, the stored address carries a label (anna+acme@example.com) while a client filters on the plain one. A lookup that matched through that fallback answersuserNamewith the address you asked for, so the client recognises the match, whileemailscarries the address actually stored. The two disagreeing is normal and is the only case where they differ — see the FAQ.preferredLanguageis declared required. Entra's defaultcustomappssomapping includes it, so do not delete that mapping when trimming the attribute list.externalIdis the identity link. It is not in the schema document because SCIM defines it as a common attribute present on every resource, but it is the single most important field you send. It must carry the same value your SAML assertion puts in the NameID. Getting this wrong is the failure described in step 1 of the setup guide.
roles carries only a value sub-attribute. The display, type and primary sub-attributes that appear in Entra's default target schema are not read.
Group attributes
| Attribute | Type | Multi | Mutability | Required |
|---|---|---|---|---|
displayName |
string | readWrite | Yes | |
members[].value |
complex | yes | readWrite |
displayName is the join key: it is matched against the subaccount's configured SSO group names. See group mapping in the setup guide.
Filtering
This is the narrowest part of the API, and the most common source of surprise.
Two attributes are filterable, with one operator:
| Supported | |
|---|---|
| Attributes | userName, externalId |
| Operators | eq |
That is the entire filter surface. It is exactly what Entra's provisioning service issues when matching an existing user, which is why it is enough.
Anything else is rejected explicitly rather than ignored — an unsupported filter is a 400, not an empty result:
GET /Users?filter=displayName eq "x" {"schemas":["urn:ietf:params:scim:api:messages:2.0:Error"], "status":"400","scimType":"invalidFilter", "detail":"Filtering on displayName is not supported, only userName and externalId"}
GET /Users?filter=userName sw "a" {"schemas":["urn:ietf:params:scim:api:messages:2.0:Error"], "status":"400","scimType":"invalidFilter", "detail":"Only the eq operator is supported, not sw"}
There are no compound filters: no and, no or, no not, no parentheses.
Sorting is the exception to this strictness. sortBy and sortOrder do not error — they return 200 with an unsorted list. ServiceProviderConfig declares sort.supported: false, so this is advertised, but it is the one place where an unsupported feature fails quietly instead of loudly. Do not rely on result ordering.
Paging
List responses are standard SCIM ListResponse documents:
{"schemas":["urn:ietf:params:scim:api:messages:2.0:ListResponse"], "totalResults":0,"startIndex":1,"itemsPerPage":0,"Resources":[]}
startIndex is 1-based. maxResults is 100, so a client that needs the full set must page with startIndex and count.
Errors
Errors are SCIM Error documents, not Refapp's normal HTML or JSON error pages:
| Status | When |
|---|---|
400 |
Malformed request, unsupported filter, or a rejected value — including an email outside the company's allowed domains |
401 |
Any authentication problem at all — see below |
404 |
Resource id not found |
409 |
Uniqueness conflict, typically a userName already in use |
Every authentication failure returns the same 401, whatever the cause — a wrong token, a token belonging to a different company, an unknown company id, or a company that does not have SCIM enabled:
{"schemas":["urn:ietf:params:scim:api:messages:2.0:Error"], "status":"401","detail":"Bearer token is missing or not valid"}
This is deliberate. The endpoint cannot be used to work out which company ids exist or which of them have SCIM half-configured. The cost is that a 401 tells you nothing about which of those four things went wrong, so work through them in order: troubleshooting a 401.
Note that a URL that is not a SCIM route at all — a misspelled endpoint, say — falls through to the normal Refapp web application and returns an HTML page. If you get HTML back, check the path before anything else.