Directory sync LIVE
Three nightly jobs most institutions want: keep cost-center / WBS codes valid automatically, administer group memberships when your directory can't map users to OpenIRIS groups on its own, and derive an access list from who is on your active projects.
Cost centers (WBS codes)
Scopes: billing:read for reads,
cost-centers:write for create/inactivate.
The sync recipe — diff by code, create on miss, inactivate on invalidation:
# 1. Does the code exist?
GET /v1/cost-centers?filter[code]=WBS4711
# 2a. Miss → create it (provider_id only needed on multi-provider keys)
POST /v1/cost-centers
Idempotency-Key: …
{ "code": "WBS4711", "name": "Microscopy core 2026" }
# 2b. Code no longer valid in SAP → inactivate (never delete)
GET /v1/cost-centers/4501 # take the ETag from the response
PATCH /v1/cost-centers/4501
Idempotency-Key: …
If-Match: W/"9F27C1D44A50B1E3…"
{ "status": "inactive" }
Rules the API enforces for you:
codeis ≤10 alphanumeric characters (SAP's cost-center limit) and unique per provider — a duplicate returns409.- Cost centers are never deleted. Inactivation only stops the code being
selectable for new bookings and charges; every historical charge keeps its
reference. Reactivation (
"status": "active") is allowed. PATCHrequiresIf-Matchwith the ETag from a prior GET — missing returns428, stale returns412(re-GET and retry). This stops two admins/jobs from silently overwriting each other.
Resolving a directory identity to a user
Scope: users:read. Every membership write takes a
user_id; your directory knows an email. Look it up first — one call per
identity, or a batch of up to a few hundred with filter[email][in]:
GET /v1/users?filter[email]=a.researcher@unibas.ch
GET /v1/users?filter[email][in]=a.researcher@unibas.ch,b.postdoc@unibas.ch
{ "data": [
{ "id": 20117, "email": "a.researcher@unibas.ch", "first_name": "Anna", "last_name": "Researcher",
"organization_id": 7, "is_verified": true, "is_disabled": false, "created_at": "2024-03-11T09:02:44Z" }
] }
- Your key sees users of the organisations that trust your provider(s) — the same rule as groups. An identity outside those organisations is simply absent; nothing is created.
- Email comparison is case-insensitive. An email is not guaranteed unique in OpenIRIS; if a lookup returns two rows, decide on your side (typically the enabled account).
- The response never carries credentials or verification codes.
Organisations you can see
Scope: users:read. The institutions that trust your provider(s) — the
same set that scopes every user, group and project read here. Useful for labelling, or to discover
which organisation a group or project belongs to.
GET /v1/organizations?filter[name][contains]=basel&sort=name
{ "data": [
{ "id": 248, "guid": "0b8e4b7c-…", "name": "University of Basel", "short_name": "UniBas",
"country": "Switzerland", "created_at": "2020-01-05T00:00:00Z" }
] }
Fetch one with GET /v1/organizations/248; an id outside the trusted set returns
404. Filter by name (eq / contains /
starts_with) and sort by id or name.
A user's current memberships
Scope: users:read. Everything a user belongs to — groups and projects
— in one call, the reverse of the per-container member lists. Reconcile a directory account against
OpenIRIS before deciding which add/remove calls a sync needs.
GET /v1/users/20117/memberships
{ "data": [
{ "type": "group", "id": 8100, "name": "Biology", "role": "member", "status": "active", "since": "2024-03-11T09:02:44Z" },
{ "type": "project", "id": 4711, "name": "Grant 2026-17", "role": "admin", "status": null, "since": null }
] }
roleisadminwhen the user administers the group/project, otherwisemember. An admin with no membership row of their own hasstatusandsincenull.- Only groups and projects in organisations that trust your provider(s) appear — the same
visibility rule as everywhere else. A
user_idoutside that set returns404. - Cursor-paginated (
cursor_next), so a user on hundreds of projects pages like any other collection.
Group memberships
Scopes: users:read for reads, groups:write for
member administration. Your key sees the groups of organisations that trust your
provider(s).
GET /v1/groups?filter[name][contains]=biology
GET /v1/groups/8100/members
POST /v1/groups/8100/members # add — idempotent
Idempotency-Key: …
{ "user_id": 20117 }
DELETE /v1/groups/8100/members/20117 # remove — idempotent
- Adding an existing member returns
200with the current membership — not an error. Removing a non-member returns204. Safe to re-run blindly. - Users are never auto-created. An unknown
user_idreturns422. Users join OpenIRIS through the normal signup/SSO flow first; the API only manages their group placement. - Removals are archived exactly as removals made in the OpenIRIS UI are — the audit trail is identical.
- Membership changes never retro-affect existing bookings or charge attribution.
Project (request) membership → IAM group
Scope: requests:read. Institutions that treat the owners
and participants of active projects as their active user population — the people who get
network storage, virtual workstations, a licence seat — can build that list in two calls
per project.
# 1. The active projects of your provider(s)
GET /v1/requests?filter[status]=active&limit=200
# 2. Everyone on each one
GET /v1/requests/4711/participants
{ "data": [
{ "user_id": 1001, "role": "owner", "status": "approved",
"email": "a.owner@example.org", "first_name": "Alex", "last_name": "Owner",
"organization_id": 7, "added_at": "2026-08-27T06:39:23Z", "added_by_user_id": 1000 },
{ "user_id": 1002, "role": "participant", "status": "approved",
"email": "s.member@example.org", "first_name": "Sam", "last_name": "Member",
"organization_id": 7, "added_at": "2026-08-27T06:40:59Z", "added_by_user_id": 1000 }
] }
- The owner is in the list, first, with
role: "owner"— whether or not they also appear on the project's Participants tab. Only about 30% of projects that have participants carry a row for their own owner, so this collection, not a raw membership table, is the complete answer to "who can reach this project?". - Take every row; don't filter on
status. OpenIRIS grants project access on the participant link existing, not on its status, and rejecting a participant removes the row outright.statusisnullfor an owner with no participant row of their own and for a small number of rows that predate the field. emailandorganization_idare on every row, so mapping to a directory account needs no second lookup.- Filter the project list however your policy defines "active" —
filter[status]=activeis the request lifecycle state;filter[created_at][gte]=…andfilter[service_id]=…narrow it further.
Job hygiene
- Run off-peak; both surfaces are small, a full diff of thousands of codes/members is a few hundred requests, comfortably within one key's rate budget.
- Use a dedicated key per job (e.g. "SAP WBS sync") with only the scopes above — see the handbook on scope minimisation.
- Log the
trace_idof any non-2xx response; it is the fastest path through support.