Skip to main content

The line's SCIM — the directory's resources

Current for: directory 1.4.0 · Checked: 30.10.2026 · Written by hand (not generated).

The directory is a SCIM 2.0 implementation (RFC 7643/7644) with a fixed set of entities, and the one door to the line's data for the services, the coordinator, the console and the clients. The doors «under a type» (/control/policy/*, /control/legs*, /control/gal*, /control/authz*, /v1/service/{directory/feed, people/changes, …}, /v1/directory/*) are gone. A door of its own stays only where the directory itself acts: sign-in (OIDC/SAML/LDAP, credentials/verify, sessions, keys), the schema (/control/schema*), replication, the audit, /healthz, /control/about|health|maintenance, and /control/accounts|groups|objects|units for the console.

The root is https://dir.<domain>/scim/v2. The services' client library is shared/scim/client (Go).

Who may enter​

WhoWithMay
the coordinator, an identity administrator, a SCIM token from the console, a replicaits own tokeneverything: reads, writes, the feed
a service of the platform (svc:*)its service token, aud identityread every collection and the feed; …/{id}/secrets of its objects; a write is 403
a server or a service as an objectits object tokenGET …/{id}/secrets of itself alone
a person (platform token)their own tokenthe reader's mode: GET /Users, /Users/{id}, /Users/{id}/photo, /Schemas, /ResourceTypes, /ServiceProviderConfig
an administrator by their grantsplatform tokenas the coordinator (admintoken.go)

Writes everywhere: POST, PUT under If-Match (the version is meta.version, W/"<rev>"; a mismatch is 412), PATCH (Users and Groups only), DELETE. An error is the urn:ietf:params:scim:api:messages:2.0:Error document (status, scimType, detail).

Collections​

CollectionObject classSchemaTypes
/Usersaccountcore User + enterprise + urn:ietf:params:scim:schemas:extension:simpletwo:2.0:User—
/Groupsgroupcore Group + urn:ietf:params:scim:schemas:extension:simpletwo:2.0:Group—
/Unitsthe organisation's tree (feed ?since= only)urn:ietf:params:scim:schemas:simple:2.0:Unit—
/Serversserver…simple:2.0:Serveropen
/Servicesservice…simple:2.0:Serviceopen (secure_access.gateway, …)
/Sitessite…simple:2.0:Sitedc, office, geo, gateway_group
/Policiespolicy…simple:2.0:Policythe installation's policies, one per kind: credentials, sessions, guests, leavers, reviews, terms, app_links, app_passwords, username_retention, telephony, dialplan, legs, photos, branding; quota templates; secure_access.* profiles
/AddressListsaddress_list…simple:2.0:AddressList—
/Authzauthz…simple:2.0:Authzgrants (a server role's catalogue, catalogue:<role>), role, assignment (user:<uid>, group:<gid>)
/Legsleg…simple:2.0:Legsip-endpoint, panel

An object's attributes are at the top level of the document (name, type, unit, disabled, links, then the class's attributes from the extension's schema). Links are links: [{rel, value, $ref}]: applies_to on a policy (target root, unit:<id>, group:<id>), visible_to on an address list.

An object's subresources: GET|PUT|DELETE …/{id}/secrets/{key} (the values to the coordinator and the object itself, the facts to an administrator), …/{id}/files/{name}, GET …/{id}/revisions.

The User extension (…:simpletwo:2.0:User)​

Computed at the answer, read-only: kind (person | guest | bot | room | system), entity, guest, system, assistant, source, sponsor/owner ({value, display}), expiresAt, extension, site, phone, telPrimary, dial, formerUserNames, hideFromAddressLists, features (what the account is provisioned for: messaging, calendar, mail, drive, telephony, secure_access), projected, addressLists (the lists it is in), addressBook (the lists it reads), groupNames, roles, leaver (state, legalHold, responsible, deletedAt), photoHash, addresses (the address index's keys: tel:…, email:…, sip:…). The people catalogue's attributes (hire_date, …) ride there too, by their SCIM paths.

«A sees B» = addressBook(A) ∩ addressLists(B) ≠ ∅. «Whose is this address» = GET /Users?filter=…:addresses eq "<key>" (the keys are dirschema.AddressLookupKeys).

The Group extension (…:simpletwo:2.0:Group)​

kind, source, handle, address, owner, owners, writers, defaultConvs; members[].display is the login.

The feed by cursor​

Every collection answers ?since=<seq>[&count=<n>][&filter=…]: the rows written after that position of the one counter, tombstones included (…:Replication: {seq, deleted}), in the order written. The answer carries …simple:2.0:Replication:head (the last position) and :floor (the oldest tombstone still served; a cursor below it reads from 0). /Users?since= also carries …:simpletwo:2.0:User:provisioning (on|off): a change of mode is a read from 0. A service reads a deleted account by id too (GET /Users/{id}: the tombstone with leaver).

The answer also carries …:Replication:next: the store position the page was read up to, past the rows the feed did not show (another class's on the objects' feed, an expired tombstone's) — a reader continues from it, not from the last row shown. A feed is read in the words of the IETF SCIM drafts: ?deltaQuery=true the first time, ?deltaToken=<token> after, the pages by ?cursor=<nextCursor> (RFC 9865); the last page carries nextDeltaToken, which the reader keeps and sends back as it is — the token is opaque. A pass with nothing new is 304; a position the feed no longer remembers (below its floor, or past its head after a restore) is 400 with scimType: expiredDeltaToken and the reader starts again with deltaQuery=true; an unreadable token is 400 invalidValue; a deleted row carries meta.isDeleted: true. /ServiceProviderConfig advertises pagination (cursor, index, the page sizes, cursorTimeout) and deltaQuery (supported, deltaTokenExpiry). The old ?since=<seq> with …:Replication:head, :floor and :next stays for the replicas and for one version of transition. A feed page carries an ETag (the head, the floor and the page's extras). A reader at the head sends it back as If-None-Match and is answered 304 without a body: «nothing new» costs the directory the head, not a page. Feed pages run two at a time on one instance: a third waits a second and is answered 503 with Retry-After: 2; any request that has not answered in 8 s is answered 503 with Retry-After: 5 — before the reader gives up on its own timeout. A reader is expected to ask once a minute, with jitter, and to double its pause after a refusal.

The computed resources (read-only)​

ResourceFilterAnswer
/ResultantPoliciesaccount eq "…" [and device eq "…"] [and class eq "…"], or class eq "…" alonethe resultant set of policy per class: content, from, layers; for one class, the root's set (root/<class>) and every account's that differs from it
/FreeNumbers`kind eq "personroom

The reader's mode (/Users under a person's token)​

RequestAnswer
GET /Usersthe roster: everybody the reader may hold, with …simple:2.0:Reader on every row (the client's row: username = uid, login, display_name, groups, attrs, name_only, inactive, …) and on the list (cursor, coversOrg, phones)
GET /Users?since=N&count=the delta: the rows after the cursor; gone on those who left the reader's sight; tooLong — read the roster again
GET /Users?filter=displayName co "text" / userName eq "…" / emails.value eq "…"the people search over the reader's book; truncated
GET /Users/{id}one row, 404 outside the book
GET /Users/{id}/photothe photo (ETag = SHA-256)

The change stream is the directory's GET /v1/events (hello {groups, contacts, directory}, directory.changed {cursor}).

Registering a platform's extension​

PUT /control/schema/extensions/{platform} with {name, classes: [{Class, Types}], attrs: [ObjectDef…]} — the coordinator registers the extension whole when it switches the platform on (shared/dirschema.ExtensionByPlatform); the services register no schema.

Where what lives​

DataResourceWriter
the installation's policies (sign-in, sessions, guests, leavers, telephony, dial plan, app passwords, photos, branding…)/Policies by typethe console, the coordinator
the numbering plan, the main number/Policies type eq "telephony" (numbering, main_number)the coordinator
the dial plan/Policies type eq "dialplan" (plan, ETag)the coordinator; messaging reads
the quotas/Policies type eq "quota" (templates, shared_mailboxes), /ResultantPolicies?filter=class eq "quota"the console; mail, messaging and the drive read
the roles and assignments/Authzthe console, the coordinator; everybody reads through shared/auth.RBACPuller
the address lists/AddressLists (visible_to)the console
the rooms' devices/Legs (credentials reconciled by the directory: a new entry is issued, one marked removed is removed)the coordinator; the MCU reads /v1/legs/feed
the tunnel sessionsthe coordinator's (left the directory)—