Create or update a user by membership code
Collection-level create-or-update keyed by membership code in the body (v4 addresses resources by id in the path). Field semantics match PATCH (absent = unchanged, null = clear). Not a full replace. Never writes activity, roles, or cohorts. Created memberships follow POST /users defaults (inactive).
Adopt: if the code is unused but email matches a member of this institution whose code is null, the code is set and the API returns 200. The stored email casing is kept; the body’s casing is not written onto the global User.email.
Conflict: same email with a different non-null code (including an auto-generated code) → 409 application/problem+json with existingUserId when that user is a member of this institution.
Attach: email exists only in another institution → new membership on the existing user, like POST. The stored email casing is kept.
Email on update: email is match/create-only. On the update path a differing email is ignored. Change the email via PATCH /users/{id}.
dataFields: applied on both create and update, in the same transaction as the profile/membership write.
No-op: if every requested field matches the stored values (code compared case-insensitively), the API returns 200 without bumping updatedAt and without an UPDATED audit row. Unchanged codes skip user_code_format validation so a nightly full PUT does not 422 on legacy codes and leaves the next updatedSince empty.
A provided code on create is persisted and is not overwritten by later user_code_format auto-assign. Idempotency-Key is accepted but not required and is scoped to PUT /users (separate from POST /users). On any unique-key error (P2002), the API re-reads by (institutionId, code) then by email inside the caller institution and decides from those rows. The adapter index (meta.driverAdapterError.cause.constraint.index; meta.target is unset on Prisma 7.9 + PlanetScale) is only a hint. If the winner’s email differs from the loser’s body, the retry returns 409 instead of applying the loser’s body. Invite mail is sent only after a winning create commits; a mail failure still returns 201. code: "" on PUT is 422 because code is required.
/usersAuthorizationBasic auth · headerrequiredBasic Auth with clientId as username and clientSecret as password
Idempotency-KeystringOptional. Accepted for replay, not required — PUT is idempotent.
application/jsonemailstring<email>firstNamestringlastNamestringimagestring<uri> | nullURL as stored; no transcoding. null or an empty string clears the stored image.
dataFieldsUserDataFieldEntry[]Show propertiesHide properties
UserDataFieldEntryfieldNamestringrequiredvaluestring | nullrequiredField value. null or an empty string clears the stored value.
sendInvitationbooleanHonoured only on a winning create. When false, skip only the mail. The Invite row, default role, SSO auto-sync, and alias pre-attach still run.
codestringrequiredLookup and write key. Required. Trimmed, non-empty, max 191. Must match user_code_format when configured.
preferredNamestring | nulldateOfBirthstring<date> | nullgenderstring | nullMALEFEMALEDIVERSEnationalitiesstring[] | nullbirthNamestring | nullcountryOfBirthstring | nullplaceOfBirthCitystring | nullExisting member updated (including adopt of a null code)
dataUserrequiredShow propertiesHide properties
idstringrequirednamestringrequiredemailstring<email>requiredimagestring | nullrequiredURL as stored; no transcoding, resolution, or format conversion.
codestring | nullrequiredInstitution membership code. One namespace per institution across students, staff, lecturers, and badge numbers. Unique per institution. Trimmed on write; empty becomes none. Case-insensitive under the database collation (FHM-1 equals fhm-1). When the institution has user_code_format, writes must match that format.
activityStatusUserActivityStatusrequiredShow propertiesHide properties
isActivebooleanrequiredisActiveFromstring<date-time> | nullrequiredisActiveUntilstring<date-time> | nullrequiredmetaResponseMetarequiredShow propertiesHide properties
requestIdstringrequiredtimestampstring<date-time>requiredversionstringrequiredv4watermarkstring<date-time>Present on GET /users?updatedSince= pages. Taken at the first page minus a 60s overlap. Bound into pagination.next_cursor. Use it as the next updatedSince after deduping by user id. Delivery is inclusive >= and at-least-once.
User or membership created
dataUserrequiredShow propertiesHide properties
idstringrequirednamestringrequiredemailstring<email>requiredimagestring | nullrequiredURL as stored; no transcoding, resolution, or format conversion.
codestring | nullrequiredInstitution membership code. One namespace per institution across students, staff, lecturers, and badge numbers. Unique per institution. Trimmed on write; empty becomes none. Case-insensitive under the database collation (FHM-1 equals fhm-1). When the institution has user_code_format, writes must match that format.
activityStatusUserActivityStatusrequiredShow propertiesHide properties
isActivebooleanrequiredisActiveFromstring<date-time> | nullrequiredisActiveUntilstring<date-time> | nullrequiredmetaResponseMetarequiredShow propertiesHide properties
requestIdstringrequiredtimestampstring<date-time>requiredversionstringrequiredv4watermarkstring<date-time>Present on GET /users?updatedSince= pages. Taken at the first page minus a 60s overlap. Bound into pagination.next_cursor. Use it as the next updatedSince after deduping by user id. Delivery is inclusive >= and at-least-once.
Bad Request
typestringrequirederrorcodestringrequiredbad_requestunauthorizedforbiddenresource_not_foundconflictvalidation_failedidempotency_key_reusedidempotency_key_in_progressservice_unavailableretryablerate_limit_exceededinternal_errortitlestringrequiredstatusintegerrequireddetailstringrequiredinstancestringerrorsobject[]Show propertiesHide properties
objectfieldstringmessagestringexistingUserIdstringPresent on 409 email or membership-code conflicts. The user id that already holds the email or code.
activeSessionobjectPresent on time-tracking start 409 when a session is already running. Stop this session, then retry start.
Show propertiesHide properties
idstringrequiredstartTimestring<date-time>requiredconflictingLogobjectPresent on time-tracking 409 when the interval being written overlaps an existing log or open session. Start uses [punch, ∞). Stop uses [sessionStart, stop). endTime is null while that session is still open.
Show propertiesHide properties
idstringrequiredstartTimestring<date-time>requiredendTimestring<date-time> | nullrequiredUnauthorized
typestringrequirederrorcodestringrequiredbad_requestunauthorizedforbiddenresource_not_foundconflictvalidation_failedidempotency_key_reusedidempotency_key_in_progressservice_unavailableretryablerate_limit_exceededinternal_errortitlestringrequiredstatusintegerrequireddetailstringrequiredinstancestringerrorsobject[]Show propertiesHide properties
objectfieldstringmessagestringexistingUserIdstringPresent on 409 email or membership-code conflicts. The user id that already holds the email or code.
activeSessionobjectPresent on time-tracking start 409 when a session is already running. Stop this session, then retry start.
Show propertiesHide properties
idstringrequiredstartTimestring<date-time>requiredconflictingLogobjectPresent on time-tracking 409 when the interval being written overlaps an existing log or open session. Start uses [punch, ∞). Stop uses [sessionStart, stop). endTime is null while that session is still open.
Show propertiesHide properties
idstringrequiredstartTimestring<date-time>requiredendTimestring<date-time> | nullrequiredConflict — duplicate email or membership code (body includes existingUserId when known), a time-tracking session that is already running (activeSession), a time-tracking interval that overlaps an existing log (conflictingLog), idempotency key reused with a different body (idempotency_key_reused), or concurrent request with the same key still in progress (idempotency_key_in_progress)
typestringrequirederrorcodestringrequiredbad_requestunauthorizedforbiddenresource_not_foundconflictvalidation_failedidempotency_key_reusedidempotency_key_in_progressservice_unavailableretryablerate_limit_exceededinternal_errortitlestringrequiredstatusintegerrequireddetailstringrequiredinstancestringerrorsobject[]Show propertiesHide properties
objectfieldstringmessagestringexistingUserIdstringPresent on 409 email or membership-code conflicts. The user id that already holds the email or code.
activeSessionobjectPresent on time-tracking start 409 when a session is already running. Stop this session, then retry start.
Show propertiesHide properties
idstringrequiredstartTimestring<date-time>requiredconflictingLogobjectPresent on time-tracking 409 when the interval being written overlaps an existing log or open session. Start uses [punch, ∞). Stop uses [sessionStart, stop). endTime is null while that session is still open.
Show propertiesHide properties
idstringrequiredstartTimestring<date-time>requiredendTimestring<date-time> | nullrequiredUnprocessable Entity — validation or business rule error
typestringrequirederrorcodestringrequiredbad_requestunauthorizedforbiddenresource_not_foundconflictvalidation_failedidempotency_key_reusedidempotency_key_in_progressservice_unavailableretryablerate_limit_exceededinternal_errortitlestringrequiredstatusintegerrequireddetailstringrequiredinstancestringerrorsobject[]Show propertiesHide properties
objectfieldstringmessagestringexistingUserIdstringPresent on 409 email or membership-code conflicts. The user id that already holds the email or code.
activeSessionobjectPresent on time-tracking start 409 when a session is already running. Stop this session, then retry start.
Show propertiesHide properties
idstringrequiredstartTimestring<date-time>requiredconflictingLogobjectPresent on time-tracking 409 when the interval being written overlaps an existing log or open session. Start uses [punch, ∞). Stop uses [sessionStart, stop). endTime is null while that session is still open.
Show propertiesHide properties
idstringrequiredstartTimestring<date-time>requiredendTimestring<date-time> | nullrequiredToo Many Requests — 1000 requests per minute per institution. Includes X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset (ISO-8601), and Retry-After (seconds).
typestringrequirederrorcodestringrequiredbad_requestunauthorizedforbiddenresource_not_foundconflictvalidation_failedidempotency_key_reusedidempotency_key_in_progressservice_unavailableretryablerate_limit_exceededinternal_errortitlestringrequiredstatusintegerrequireddetailstringrequiredinstancestringerrorsobject[]Show propertiesHide properties
objectfieldstringmessagestringexistingUserIdstringPresent on 409 email or membership-code conflicts. The user id that already holds the email or code.
activeSessionobjectPresent on time-tracking start 409 when a session is already running. Stop this session, then retry start.
Show propertiesHide properties
idstringrequiredstartTimestring<date-time>requiredconflictingLogobjectPresent on time-tracking 409 when the interval being written overlaps an existing log or open session. Start uses [punch, ∞). Stop uses [sessionStart, stop). endTime is null while that session is still open.
Show propertiesHide properties
idstringrequiredstartTimestring<date-time>requiredendTimestring<date-time> | nullrequiredInternal Server Error
typestringrequirederrorcodestringrequiredbad_requestunauthorizedforbiddenresource_not_foundconflictvalidation_failedidempotency_key_reusedidempotency_key_in_progressservice_unavailableretryablerate_limit_exceededinternal_errortitlestringrequiredstatusintegerrequireddetailstringrequiredinstancestringerrorsobject[]Show propertiesHide properties
objectfieldstringmessagestringexistingUserIdstringPresent on 409 email or membership-code conflicts. The user id that already holds the email or code.
activeSessionobjectPresent on time-tracking start 409 when a session is already running. Stop this session, then retry start.
Show propertiesHide properties
idstringrequiredstartTimestring<date-time>requiredconflictingLogobjectPresent on time-tracking 409 when the interval being written overlaps an existing log or open session. Start uses [punch, ∞). Stop uses [sessionStart, stop). endTime is null while that session is still open.
Show propertiesHide properties
idstringrequiredstartTimestring<date-time>requiredendTimestring<date-time> | nullrequired