Skip to content

Endpoints and protocol support

Every SCIM endpoint is served under /scim/v2 on the standard HTTP listeners. Requests and responses use the application/scim+json media type, including error responses, as required by RFC 7644 section 8.1. A OPTIONS request to any path under the base returns 204 No Content, which satisfies clients that probe with a preflight request before their first call.

EndpointMethodsAuthentication
/scim/v2/UsersGET, POSTBearer token
/scim/v2/Users/{id}GET, PUT, PATCH, DELETEBearer token
/scim/v2/Users/.searchPOSTBearer token
/scim/v2/GroupsGET, POSTBearer token
/scim/v2/Groups/{id}GET, PUT, PATCH, DELETEBearer token
/scim/v2/Groups/.searchPOSTBearer token
/scim/v2/.searchPOSTBearer token
/scim/v2/BulkPOSTBearer token
/scim/v2/ServiceProviderConfigGETNone
/scim/v2/ResourceTypes, /scim/v2/ResourceTypes/{id}GETNone
/scim/v2/Schemas, /scim/v2/Schemas/{urn}GETNone
/scim/v2/MeanyReturns 501

The three discovery endpoints are readable without authentication, which is permitted by RFC 7644 and expected by clients that probe a target before credentials have been configured. They expose the shape of the service and no account data. Where even that is unwanted, restrict the path by address as described under Restricting network access. Filtering is not accepted on the discovery endpoints; a filter parameter there is refused with 403 Forbidden.

/Me is defined by RFC 7644 section 3.11 as an alias for the resource belonging to the authenticated subject. Stalwart returns 501 Not Implemented, which the specification explicitly sanctions. The endpoint exists for end-user self-service, whereas a SCIM client authenticates as a machine principal whose subject is the integration’s own service account, and not a provisionable user.

Requests to any unrecognised path under /scim/v2 return 404 Not Found, and a recognised path called with an unsupported method returns 405 Method Not Allowed with an Allow header naming the methods that are accepted.

GET /scim/v2/ServiceProviderConfig reports what the server supports. The values are fixed:

CapabilityValue
patchSupported
bulkSupported, at most 1000 operations and 1 MiB per request
filterSupported, at most 200 results
changePasswordNot supported
sortSupported
etagSupported
paginationIndex and cursor, default page 100, maximum page 200, index by default
authenticationSchemesOAuth bearer token only
interopProfileConformantfalse, see schema handling

changePassword reports whether a SCIM client may set the password attribute on a user it provisions. In this deployment model the identity provider holds the credential and Stalwart validates against it over OIDC, so there is no password to push and the attribute is absent from the published User schema.

GET /scim/v2/Schemas returns the attribute definitions the server actually honours, derived from its own account model. It is the authoritative statement of what Stalwart stores, and attribute mapping explains what each attribute does once it arrives.

Requests are not required to confine themselves to that set. RFC 7643 section 4.1 defines a User resource considerably larger than any mail server implements, and RFC 7644 section 3.1 states plainly that a client “does not need to remove” attributes the server will not act on, adding that a client “SHOULD NOT expect a service provider to return SCIM resources with exactly the same schema and values as submitted”. Identity providers are built on that assumption: none of them shapes its request body from the target’s /Schemas document.

Stalwart therefore accepts and discards the attributes of the core RFC 7643 User and Group schemas that it does not implement, and the enterprise user extension, in POST, PUT and PATCH request bodies alike. They are neither stored nor returned. The one that is not merely discarded is the structured name: where displayName and name.formatted are both absent, name.givenName and name.familyName are joined and stored as the display name, so an account provisioned by a client that sends only structured name parts is not left nameless.

An attribute that belongs to no schema Stalwart recognises is still refused, with 400 Bad Request and a scimType of invalidSyntax. A misspelled mapping such as dispalyName, an unrecognised schema URI, a duplicated attribute, and a missing schemas value are all refused the same way.

The SCIM interoperability profile section 5.3 requires every undefined attribute to be rejected. Stalwart follows RFC 7644 where the two disagree, and reports interopProfileConformant as false. Every other requirement of that profile is met. Why undefined attributes are accepted sets out the reasoning.

Filters are accepted on GET /Users, GET /Groups, and the /.search endpoints. Two operators are supported, eq and and, which is exactly what the interoperability profile mandates and exactly what identity providers send in practice. Every other operator, including ne, co, sw, ew, gt, lt, pr, or, and not, is refused with 400 Bad Request and a scimType of invalidFilter. The full grammar is parsed before the request is rejected, so the error names the unsupported construct.

The attributes that can be filtered are:

ResourceAttributes
Userid, externalId, userName, emails, emails.value, active, displayName, name.formatted, groups, groups.value
Groupid, externalId, displayName, members, members.value

Filtering on any other attribute is refused with invalidFilter. Value filters of the form attr[sub eq "x"] are not accepted inside a filter parameter, though they are supported in PATCH paths, where clients genuinely need them.

Most filters resolve against an index. active, displayName, and name.formatted cannot be fully answered from an index, so they are evaluated after the indexed part of the filter has narrowed the candidate set. If more than 200 candidates remain at that point, the request is refused with 400 Bad Request and a scimType of tooMany, and the message asks for a narrower filter. Combining such an attribute with an indexed one using and avoids the condition.

sortBy accepts id and, for users, userName; sortOrder accepts ascending and descending. Sorting by any other attribute is refused with invalidValue, because the server sorts from an index.

Both pagination styles defined for SCIM are available. Index-based pagination follows RFC 7644 section 3.4.2.4, using startIndex and count, and the response reports totalResults, startIndex, and itemsPerPage. Cursor-based pagination follows RFC 9865: the client sends cursor and the response returns nextCursor until the result set is exhausted. A page holds 100 resources unless count says otherwise, and 200 is the maximum. A cursor is bound to the query that produced it, so presenting one alongside a changed filter or sort order is rejected.

Every resource carries a meta.version entity tag, returned in the ETag header as well as the body. The value is derived from the content of the resource, so it changes when the resource changes and does not change when it does not; for groups it covers the membership as well as the group’s own attributes.

If-None-Match on a GET returns 304 Not Modified when the resource is unchanged, which lets a client poll cheaply. If-Match on a PUT, PATCH, or DELETE returns 412 Precondition Failed when the resource has changed since it was read, which prevents two overlapping synchronisation cycles from overwriting each other. Requests that omit the header proceed unconditionally.

meta.lastModified is not emitted, because Stalwart does not record a modification timestamp on accounts. meta.created is present. Clients that use timestamps for incremental synchronisation should use entity tags instead.

POST /scim/v2/Bulk accepts up to 1000 operations in a single request, subject to a 1 MiB payload limit, and supports failOnErrors and bulkId forward references so that a user can be created and added to a group created in the same request. Each operation reports its own status, so a partial failure does not discard the successful operations.

The endpoint exists because the IPSIE profile requires it and because provisioning scripts can use it directly.

POST /scim/v2/Users/.search and POST /scim/v2/Groups/.search accept the same parameters as a GET query, carried in a SearchRequest body instead of the query string. POST /scim/v2/.search runs the query across both resource types and returns a single combined ListResponse.

RFC 7644 section 7.5.2 warns against placing personal data in request URIs, and Stalwart records request URIs in its telemetry, so GET /Users?filter=emails eq "[email protected]" writes an address into the trace log where the equivalent .search request does not.

Errors are returned as SCIM error documents carrying an HTTP status, a detail string, and, where the specification defines one, a scimType.

StatusscimTypeCause
400invalidSyntaxMalformed body, an attribute or schema URI belonging to no known schema, a duplicated attribute, or a missing schemas value
400invalidFilterUnsupported operator or attribute in a filter
400invalidPathUnsupported PATCH path, or a value filter where one is not accepted
400invalidValueA value the server cannot accept, including a domain not open to SCIM
400tooManyA filter matching more than 200 candidates before a non-indexed attribute is evaluated
400mutabilityAn attempt to modify a read-only attribute
401Missing, malformed, or expired bearer token, or HTTP Basic authentication
403Valid credential lacking a required permission, or the Community Edition
404Unknown resource, unknown endpoint, or a resource outside the caller’s tenant
405Method not supported by the endpoint; the Allow header lists what is
409uniquenessAn address or group name that is already in use
412An If-Match precondition that no longer holds
413A payload above the endpoint’s size limit
429Rate limit exceeded; a Retry-After header states when to retry
501/Me

A resource belonging to another tenant returns 404, not 403, so that tenant boundaries do not leak the existence of accounts.