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:

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" }
] }

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 }
] }

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
There is deliberately no "replace all members" call. Group membership drives both access and billing attribution; a single faulty sync run against a replace-all endpoint would empty a group. Diff on your side and apply individual add/remove calls — both are idempotent, so partial failures are safely re-runnable.

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 }
] }
This surface is read-only. Adding or removing a participant fires approval notifications and writes request history, so it stays in the OpenIRIS UI for now. Tell us on the forum if a write path would help.

Job hygiene