Users & Groups
Accounts, organizational assignments, and personal data for the people in your organization — plus the authorization groups they belong to.
User accounts
GET /me
Get current user profile and tenant context
User keyExample request
curl -X GET "https://api.openstudio.one/v1/me" \
-H "X-Api-User-Key: YOUR_USER_KEY"<?php
$ch = curl_init('https://api.openstudio.one/v1/me');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);
$response = curl_exec($ch);
$data = json_decode($response, true);import requests
headers = {
"X-Api-User-Key": "YOUR_USER_KEY",
}
response = requests.get("https://api.openstudio.one/v1/me", headers=headers)
data = response.json()fetch('https://api.openstudio.one/v1/me', {
method: 'GET',
headers: {
'X-Api-User-Key': 'YOUR_USER_KEY'
}
})
.then(res => res.json())
.then(data => console.log(data));ⓘRuns a real request from your browser directly to me. Keys are only held in this page's memory while you're here — never stored, never sent anywhere else.
Response fields
| Field | Type | Flags | Notes |
|---|---|---|---|
| users | object | — | The basic account — login identity, status, connected sign-in providers. |
| ↳user_id | integer | — | The unique user ID. |
| ↳brand_id | integer | — | The unique ID of the user's tenant/organization. |
| ↳brand_name | string | — | Organization display name. |
| ↳username | string | — | The user's username. |
| ↳email | string | — | The user's primary (login) email address. |
| ↳status_code | integer | — | Account status, granular. See status_label for a simplified version.1 = Active0 = Inactive-1 = Pending activation-3 = Closed-4 = Closed (undeliverable)2 = Reported |
| ↳status_label | string | — | Simplified version of status_code, for consumers that don't need the distinction between the two "closed" reasons — both -3 and -4 collapse to disabled here. Use status_code if you need the granular reason.active = status_code 1inactive = status_code 0pending_activation = status_code -1disabled = status_code -3 or -4reported = status_code 2 |
| ↳is_founder | boolean | — | Whether this user is the founding admin of the organization. |
| ↳language | string | — | ISO language code (e.g., "en", "it") — matches the "lang" value from GET /reference/languages. |
| ↳timezone | string | — | IANA timezone value for this user, e.g. "Europe/Amsterdam". See GET /reference/timezones for the full list of valid values. |
| ↳personal_code | integer | — | Numeric identifier assigned to the user; also forms the base of sso_username. |
| ↳sso_id | string | — | Active SSO identifier if present. |
| ↳sso_username | string | — | The username used when authenticating via SSO, derived from personal_code. |
| ↳avatar_url | string | — | Mirrors identity.photo_url — always a real, loadable URL, a shared blank-avatar placeholder if no photo is on file. identity can be null (no identity record at all); this field still resolves to the placeholder in that case, never blank. Editable via PATCH as avatar_url — an https:// link only (no file upload through this API); an empty string clears it back to the placeholder. |
| ↳tone_style | string | — | The assistant/greeting tone preference selected by the user. Empty string means "inherit the organization's default" (itself default unless a tenant admin picked a custom one — see GET /tenant/settings). default/friendly/motivational are always valid; a tenant can also define its own custom tones (staff-configured), which appear here as custom:{id}, where id refers to a live, non-deleted custom tone on this tenant. Rejected via PATCH with a 422 if the organization has locked tone selection (ACP → Experience → "Lock tone for everyone"). = Inherit organization defaultdefault = Default tonefriendly = Friendlymotivational = Motivational |
| ↳mfa | object | — | Multi-factor authentication status. |
| ↳enabled | boolean | — | Whether multi-factor authentication is currently enabled for this user. |
| ↳type | string | The active MFA method. Null when enabled is false.otp_mail = One-time code by emailotp_app = TOTP authenticator app |
|
| ↳connected_accounts | object | — | Boolean flags indicating linked third-party providers. |
| ↳facebook | boolean | — | Whether a Facebook account is linked for sign-in. |
| ↳google | boolean | — | Whether a Google account is linked for sign-in. |
| ↳apple | boolean | — | Whether an Apple account is linked for sign-in. |
| ↳linkedin | boolean | — | Whether a LinkedIn account is linked for sign-in. |
| ↳microsoft | boolean | — | Whether a Microsoft account is linked for sign-in. |
| ↳timestamps | object | — | Key lifecycle dates. |
| ↳registered_at | string | — | YYYY-MM-DD HH:MM:SS — when the user account was created. |
| ↳last_login_at | string | — | Timestamp of the user's most recent successful login. |
| ↳last_activity_at | string | — | Timestamp of the user's most recent recorded activity in the dashboard. |
| identity | object | The "digital identity" extension of the basic account — personal/HR data beyond login credentials. null if this user has no identity record at all (e.g. a not-yet-recognized or externally-provisioned account). |
|
| ↳first_name | string | — | Your first name. |
| ↳middle_name | string | Your middle name(s), if any. | |
| ↳last_name | string | — | Your last name. |
| ↳is_privacy_mode | boolean | — | True when this account was created with "privacy mode" and no real identity data has been entered since — first_name/last_name/sex/birth_date/etc. above are all placeholder values (e.g. first_name equals the account's username, sex is O, birth_date is 01/01/1970), not this person's real information. Automatically clears to false the moment real identity data is saved, whichever app does it (this API's own PATCH /users/{id}/identity included). For accounts created before this flag existed, it's derived from the same placeholder pattern rather than a stored value — still accurate, just not backed by an explicit flag on those specific rows. |
| ↳photo_url | string | — | Absolute URL to the current profile photo. Falls back to a blank-avatar placeholder image (never null) if no photo has ever been set — the same placeholder the dashboard itself shows. |
| ↳pending_photo_url | string | A newly-uploaded photo awaiting approval, if any — see POST /users/{id}/photo/POST /users/{id}/photo/approve. null when there's no pending upload; does NOT replace photo_url (the live photo) until approved. |
|
| ↳sex | string | M/F/O, if recorded. |
|
| ↳birth_date | string | Format DD/MM/YYYY — deliberately NOT the YYYY-MM-DD format used everywhere else in this API, to avoid silently reformatting the source value. |
|
| ↳birth_state | string | Your birth country, if recorded. | |
| ↳birth_province | string | Your birth state/province, if recorded. | |
| ↳birth_city | string | Your birth city, if recorded. | |
| ↳citizenship | string | Your citizenship, if recorded. | |
| ↳citizenship_other | string | Free text, used when citizenship doesn't cover the actual value (e.g. dual/other citizenship not in the standard list). |
|
| ↳tax_code | string | National tax identification code (e.g. Italian codice fiscale). | |
| ↳recognition_status | string | — | The same 3-state verification status the dashboard itself shows on a person's record.recognized = Identity verified, with a digital ID document on file.recognized_paper = Identity verified against a physical/paper document — no digital document on file.not_recognized = Identity not yet verified. |
| ↳recognition_date | string | null when recognition_status is not_recognized. |
|
| ↳recognition_by_name | string | Real name of whoever performed the recognition. null when not recognized. |
|
| ↳recognition_notes | string | Free-text notes left at recognition time, if any. | |
| ↳recognition_document | object | The document used as proof, only present for document-based recognition (recognition_status: "recognized"). |
|
| ↳type | string | — | |
| ↳number | string | — | |
| ↳issued_on | string | — | |
| ↳file_url | string | — | |
| ↳phone_number | string | Landline. | |
| ↳mobile_number | string | Your mobile number, if recorded. | |
| ↳fax_number | string | Your fax number, if recorded. | |
| ↳home_address | object | — | Every field nullable independently — a partially-filled address is common (e.g. city known, street not). |
| ↳country | string | Your home country, if recorded. | |
| ↳district | string | State/province/region, depending on the country. | |
| ↳city | string | Your home city, if recorded. | |
| ↳address_type | string | e.g. VIA (street) — free text, not a fixed enum. |
|
| ↳address | string | Street name. | |
| ↳number | string | Street/house number. | |
| ↳postcode | string | Your home postcode, if recorded. | |
| ↳co | string | "Care of" — an additional name on the address (e.g. a family member's surname), when the address isn't solely in the user's own name. | |
| ↳since | string | When the user started living at this address. Format YYYY-MM-DD. |
|
| memberships | array of objects | — | Currently active organizational structure assignments, primary first. "Active" means not yet ended: until is null (open-ended) OR still in the future (a fixed-term assignment that hasn't lapsed) — same rule the dashboard's own Membership Management app uses. Ended assignments are not included here — see GET /users/{id}/memberships/history. This is the same array GET /users/{id}/memberships returns on its own, wrapped under a memberships key there instead of nested in this larger payload. |
| ↳id | integer | This assignment's own id — use this in PATCH /users/{id}/memberships/{membership_id}, DELETE /users/{id}/memberships/{membership_id}, and POST /users/{id}/memberships/{membership_id}/primary. null on a leadership-only row (no actual membership — see is_leader/is_manager). |
|
| ↳structure_acronym | string | — | Matches the acronym field on GET /departments. |
| ↳structure_title | string | — | Your primary structure's display name. |
| ↳level | integer | — | Your primary structure's level in the hierarchy.1 = Area2 = Department3 = Unit |
| ↳parent_title | string | Display title of the structure's immediate parent, resolved server-side. Null only for an L1 Area. | |
| ↳area_title | string | Display title of the structure's top-level Area — differs from parent_title for an L3 Unit, equal to it for an L2 Department. Null only for an L1 Area. |
|
| ↳is_primary | boolean | — | At most one primary assignment per user at a time. Always false on a leadership-only row. |
| ↳is_leader | boolean | — | Whether this person is this structure's leader, independent of whether they're also a member of it. |
| ↳is_manager | boolean | — | Whether this person is one of this structure's co-managers, independent of whether they're also a member of it. |
| ↳since | string | When this assignment started. Format YYYY-MM-DD. A small number of rows migrated from before this API existed may show a full YYYY-MM-DD HH:MM:SS timestamp instead — returned exactly as stored, never reformatted. null on a leadership-only row. |
|
| ↳until | string | null = open-ended. A future date = a fixed-term assignment, still active as of now. Format YYYY-MM-DD when set. Also always null on a leadership-only row. |
|
| ↳notes | string | Free-text note attached when this assignment was created, if any. | |
| director | object | Present ONLY when this user currently holds the tenant's General Director role. null for everyone else. |
|
| ↳role | string | — | Always this one value today.general_director |
| ↳from_date | string | null only when is_interim is true (no formal appointment record exists at all). Returned exactly as stored on the appointment record — this API has no write endpoint for director appointments, so unlike since/until above there's no validation step here to guarantee a single consistent format. |
|
| ↳to_date | string | Always null — this field is only ever populated for a PAST directorship, and this object only ever represents the CURRENT one. |
|
| ↳appointed_by_user_id | integer | null when is_interim is true, or when the appointment record itself has no appointing-user value. |
|
| ↳is_interim | boolean | — | true when no formal appointment record exists for this tenant at all, and this user holds the role only via the documented fallback (the founder acts as director by default until one is formally appointed). |
Example response
{
"success": true,
"data": {
"users": {
"user_id": 999,
"brand_id": 150,
"brand_name": "Acme Corporation",
"username": "j.doe",
"email": "j.doe@example.com",
"status_code": 1,
"status_label": "active",
"is_founder": false,
"language": "en",
"timezone": "Europe/London",
"personal_code": 10000999,
"sso_id": "10000999",
"sso_username": "10000999@openstudio.one",
"avatar_url": "https://www.openstudio.one/assets/img/avatars/user_uploads/avatar_999.jpg",
"tone_style": "friendly",
"mfa": {
"enabled": true,
"type": "otp_mail"
},
"connected_accounts": {
"facebook": false,
"google": true,
"apple": false,
"linkedin": false,
"microsoft": true
},
"timestamps": {
"registered_at": "2021-12-01 10:00:00",
"last_login_at": "2024-03-15 08:30:22",
"last_activity_at": "2024-03-15 11:45:01"
}
},
"identity": {
"first_name": "Jane",
"middle_name": null,
"last_name": "Doe",
"is_privacy_mode": false,
"photo_url": "https://client.openstudio.one/controller/abc123.jpg",
"pending_photo_url": null,
"sex": "F",
"birth_date": "17/01/1995",
"birth_state": "Italy",
"birth_province": "Milano",
"birth_city": "Milano",
"citizenship": "Italian",
"citizenship_other": null,
"tax_code": "DOEJNA95A57F205X",
"recognition_status": "recognized",
"recognition_date": "05/10/2018",
"phone_number": null,
"mobile_number": "+393460000000",
"fax_number": null,
"home_address": {
"country": "Italy",
"district": "Milano",
"city": "Milano",
"address_type": "VIA",
"address": "Garibaldi",
"number": "12",
"postcode": "20121",
"co": null,
"since": "2015-09-05"
}
},
"memberships": [
{
"id": 41,
"structure_acronym": "AFA",
"structure_title": "Administration Finance & Accounting",
"level": 2,
"parent_title": "Main Area",
"area_title": "Main Area",
"is_primary": true,
"is_leader": false,
"is_manager": false,
"since": "2019-08-19 15:40:15",
"until": null,
"notes": null
}
],
"director": null
},
"error": null
}PATCH /me
Update your own timezone, language, tone style, and avatar
User keyExample request
curl -X PATCH "https://api.openstudio.one/v1/me" \
-H "X-Api-User-Key: YOUR_USER_KEY" \
-H "Content-Type: application/json" \
-d '[]'<?php
$ch = curl_init('https://api.openstudio.one/v1/me');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY', 'Content-Type: application/json']);
$response = curl_exec($ch);
$data = json_decode($response, true);import requests
headers = {
"X-Api-User-Key": "YOUR_USER_KEY",
}
body = {
}
response = requests.patch("https://api.openstudio.one/v1/me", headers=headers, json=body)
data = response.json()fetch('https://api.openstudio.one/v1/me', {
method: 'PATCH',
headers: {
'X-Api-User-Key': 'YOUR_USER_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
})
})
.then(res => res.json())
.then(data => console.log(data));Response fields
| Field | Type | Flags | Notes |
|---|---|---|---|
| type | string | — | — |
| description | string | — | — |
Example response
{
"success": true,
"data": {
"users": {
"user_id": 999,
"brand_id": 150,
"brand_name": "Acme Corporation",
"username": "j.doe",
"email": "j.doe@example.com",
"status_code": 1,
"status_label": "active",
"is_founder": false,
"language": "it",
"timezone": "Europe/Rome",
"personal_code": 10000999,
"sso_id": "10000999",
"sso_username": "10000999@openstudio.one",
"avatar_url": "https://example.com/new-avatar.jpg",
"tone_style": "motivational",
"mfa": {
"enabled": true,
"type": "otp_mail"
},
"connected_accounts": {
"facebook": false,
"google": true,
"apple": false,
"linkedin": false,
"microsoft": true
},
"timestamps": {
"registered_at": "2021-12-01 10:00:00",
"last_login_at": "2024-03-15 08:30:22",
"last_activity_at": "2024-03-15 11:45:01"
}
},
"identity": null,
"memberships": [],
"director": null
},
"error": null
}Notifications
GET /notifications
List the caller's own notifications
User key PaginatedQuery parameters (2)
Pagination
| Parameter | Type | Required | Notes |
|---|---|---|---|
| ?page | integer | Optional | Page number, starting at 1. Defaults to 1. |
| ?limit | integer | Optional | Rows per page, 1-100. Defaults to 20. |
Example request
curl -X GET "https://api.openstudio.one/v1/notifications?page=123&limit=123" \
-H "X-Api-User-Key: YOUR_USER_KEY"<?php
$ch = curl_init('https://api.openstudio.one/v1/notifications?page=123&limit=123');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);
$response = curl_exec($ch);
$data = json_decode($response, true);import requests
headers = {
"X-Api-User-Key": "YOUR_USER_KEY",
}
response = requests.get("https://api.openstudio.one/v1/notifications?page=123&limit=123", headers=headers)
data = response.json()fetch('https://api.openstudio.one/v1/notifications?page=123&limit=123', {
method: 'GET',
headers: {
'X-Api-User-Key': 'YOUR_USER_KEY'
}
})
.then(res => res.json())
.then(data => console.log(data));ⓘRuns a real request from your browser directly to notifications. Keys are only held in this page's memory while you're here — never stored, never sent anywhere else.
Response fields
| Field | Type | Flags | Notes |
|---|---|---|---|
| items | array of objects | — | This page's notifications. |
| ↳id | integer | — | — |
| ↳read | boolean | — | — |
| ↳date | string | — | YYYY-MM-DD HH:MM:SS. |
| ↳icon | string | — | A Font Awesome icon name (e.g. check-circle), as used by the web dashboard's own notification rendering — not necessarily a valid icon name in a native client's own icon set. |
| ↳title | string | — | — |
| ↳content | string | — | — |
| ↳action_url | string | Always an absolute, directly-loadable URL — a bare relative link as stored (e.g. ?view=home, meaningful only relative to the web dashboard's own page) is expanded against CLIENT_URL first, unlike the web dropdown's own client-side fixup, which only handles a malformed http// prefix. null if the notification has no action at all. |
|
| ↳action_view | string | action_url's view query parameter alone, pulled out so a native client can map known values to an in-app screen instead of always falling back to a web view. null if action_url is null or has no view parameter. |
|
| unread_count | integer | — | Total unread notifications for this caller, independent of pagination. |
Pagination
| Field | Type | Notes |
|---|---|---|
| page | integer | Current page number. |
| limit | integer | Rows per page. |
| total | integer | Total matching rows across all pages. |
| total_pages | integer | Total number of pages. |
| next_page_url | string | Ready-to-call URL for the next page, same filters applied. Null on the last page. |
| prev_page_url | string | Ready-to-call URL for the previous page, same filters applied. Null on the first page. |
Example response
{
"success": true,
"data": {
"items": [
{
"id": 918,
"read": false,
"date": "2026-08-20 09:14:02",
"icon": "check-circle",
"title": "Ticket resolved",
"content": "Your support ticket #4021 was marked as resolved.",
"action_url": "https://client.openstudio.one/?view=trouble_ticket&id=4021",
"action_view": "trouble_ticket"
}
],
"unread_count": 3
},
"error": null,
"pagination": {
"page": 1,
"limit": 50,
"total": 134,
"total_pages": 3,
"next_page_url": "https://api.openstudio.one/v1/departments?page=2&limit=50",
"prev_page_url": null
}
}PATCH /notifications/{id}
Mark a notification as read or unread
User keyRequest body
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| read | boolean | — | Required | true to mark it read, false to mark it unread. |
Example request
curl -X PATCH "https://api.openstudio.one/v1/notifications/{id}" \
-H "X-Api-User-Key: YOUR_USER_KEY" \
-H "Content-Type: application/json" \
-d '{"read":true}'<?php
$ch = curl_init('https://api.openstudio.one/v1/notifications/{id}');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'read' => true,
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY', 'Content-Type: application/json']);
$response = curl_exec($ch);
$data = json_decode($response, true);import requests
headers = {
"X-Api-User-Key": "YOUR_USER_KEY",
}
body = {
'read': True,
}
response = requests.patch("https://api.openstudio.one/v1/notifications/{id}", headers=headers, json=body)
data = response.json()fetch('https://api.openstudio.one/v1/notifications/{id}', {
method: 'PATCH',
headers: {
'X-Api-User-Key': 'YOUR_USER_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
'read': true,
})
})
.then(res => res.json())
.then(data => console.log(data));Response fields
| Field | Type | Flags | Notes |
|---|---|---|---|
| id | integer | — | — |
| read | boolean | — | — |
| date | string | — | — |
| icon | string | — | — |
| title | string | — | — |
| content | string | — | — |
| action_url | string | — | |
| action_view | string | — |
Example response
{
"success": true,
"data": {
"id": 918,
"read": true,
"date": "2026-08-20 09:14:02",
"icon": "check-circle",
"title": "Ticket resolved",
"content": "Your support ticket #4021 was marked as resolved.",
"action_url": "https://client.openstudio.one/?view=trouble_ticket&id=4021",
"action_view": "trouble_ticket"
},
"error": null
}DELETE /notifications/{id}
Delete a notification
User keyExample request
curl -X DELETE "https://api.openstudio.one/v1/notifications/{id}" \
-H "X-Api-User-Key: YOUR_USER_KEY"<?php
$ch = curl_init('https://api.openstudio.one/v1/notifications/{id}');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);
$response = curl_exec($ch);
$data = json_decode($response, true);import requests
headers = {
"X-Api-User-Key": "YOUR_USER_KEY",
}
response = requests.delete("https://api.openstudio.one/v1/notifications/{id}", headers=headers)
data = response.json()fetch('https://api.openstudio.one/v1/notifications/{id}', {
method: 'DELETE',
headers: {
'X-Api-User-Key': 'YOUR_USER_KEY'
}
})
.then(res => res.json())
.then(data => console.log(data));Response fields
| Field | Type | Flags | Notes |
|---|
Example response
{
"success": true,
"data": {},
"error": null
}Address book
GET /contact-points/{userId}
List one person's contact points (phone/email records)
User keyExample request
curl -X GET "https://api.openstudio.one/v1/contact-points/{userId}" \
-H "X-Api-User-Key: YOUR_USER_KEY"<?php
$ch = curl_init('https://api.openstudio.one/v1/contact-points/{userId}');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);
$response = curl_exec($ch);
$data = json_decode($response, true);import requests
headers = {
"X-Api-User-Key": "YOUR_USER_KEY",
}
response = requests.get("https://api.openstudio.one/v1/contact-points/{userId}", headers=headers)
data = response.json()fetch('https://api.openstudio.one/v1/contact-points/{userId}', {
method: 'GET',
headers: {
'X-Api-User-Key': 'YOUR_USER_KEY'
}
})
.then(res => res.json())
.then(data => console.log(data));ⓘRuns a real request from your browser directly to contact-points/{userId}. Keys are only held in this page's memory while you're here — never stored, never sent anywhere else.
Response fields
| Field | Type | Flags | Notes |
|---|---|---|---|
| person | object | — | — |
| permission_level | string | — | One of: full, limited. |
| contact_points | array of objects | — | — |
| ↳id | integer | — | — |
| ↳type | string | — | One of: telephone_short, service_phone, business_email, functional_email, fax, other_internal_phone. |
| ↳type_label | string | — | — |
| ↳data | string | — | — |
| ↳structure | string | — | |
| ↳active | boolean | — | — |
| ↳visible | boolean | — | — |
| ↳created_at | string | — | |
| ↳modified_at | string | — | |
| ↳modified_by_name | string | — |
Example response
{
"success": true,
"data": {
"person": {
"id": 88,
"first_name": "Jane",
"last_name": "Doe"
},
"permission_level": "full",
"contact_points": [
{
"id": 12,
"type": "business_email",
"type_label": "Institutional Email",
"data": "jane.doe@example.com",
"structure": "HR",
"active": true,
"visible": true,
"created_at": "2026-01-10 09:00:00",
"modified_at": null,
"modified_by_name": null
}
]
},
"error": null
}POST /contact-points/{userId}
Add a new contact point for a person
User keyRequest body
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| type | string | — | Required | One of: telephone_short, service_phone, business_email, functional_email, fax, other_internal_phone. |
| data | string | — | Required | The phone number or email address. |
| structure | string | — | Optional | Required for every type except business_email (auto-assigned). |
| visible | boolean | — | Optional | Defaults to false. |
Example request
curl -X POST "https://api.openstudio.one/v1/contact-points/{userId}" \
-H "X-Api-User-Key: YOUR_USER_KEY" \
-H "Content-Type: application/json" \
-d '{"type":"...","data":"...","structure":"...","visible":true}'<?php
$ch = curl_init('https://api.openstudio.one/v1/contact-points/{userId}');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'type' => '...',
'data' => '...',
'structure' => '...',
'visible' => true,
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY', 'Content-Type: application/json']);
$response = curl_exec($ch);
$data = json_decode($response, true);import requests
headers = {
"X-Api-User-Key": "YOUR_USER_KEY",
}
body = {
'type': '...',
'data': '...',
'structure': '...',
'visible': True,
}
response = requests.post("https://api.openstudio.one/v1/contact-points/{userId}", headers=headers, json=body)
data = response.json()fetch('https://api.openstudio.one/v1/contact-points/{userId}', {
method: 'POST',
headers: {
'X-Api-User-Key': 'YOUR_USER_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
'type': '...',
'data': '...',
'structure': '...',
'visible': true,
})
})
.then(res => res.json())
.then(data => console.log(data));Response fields
| Field | Type | Flags | Notes |
|---|---|---|---|
| contact_points | array | — | — |
Example response
{
"success": true,
"data": {
"contact_points": []
},
"error": null
}PATCH /contact-points/{userId}/{contactId}
Edit, activate/deactivate, or toggle visibility of one contact point
User keyRequest body
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| active | boolean | — | Optional | Reactivate (true) or deactivate (false) this contact point. |
| visible | boolean | — | Optional | Show (true) or hide (false) this contact point from others. |
| data | string | — | Optional | The new contact value (phone number or email address). |
| structure | string | — | Optional | The structure this contact point belongs to — not settable for an institutional email, which is always auto-assigned. |
Example request
curl -X PATCH "https://api.openstudio.one/v1/contact-points/{userId}/{contactId}" \
-H "X-Api-User-Key: YOUR_USER_KEY" \
-H "Content-Type: application/json" \
-d '{"active":true,"visible":true,"data":"...","structure":"..."}'<?php
$ch = curl_init('https://api.openstudio.one/v1/contact-points/{userId}/{contactId}');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'active' => true,
'visible' => true,
'data' => '...',
'structure' => '...',
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY', 'Content-Type: application/json']);
$response = curl_exec($ch);
$data = json_decode($response, true);import requests
headers = {
"X-Api-User-Key": "YOUR_USER_KEY",
}
body = {
'active': True,
'visible': True,
'data': '...',
'structure': '...',
}
response = requests.patch("https://api.openstudio.one/v1/contact-points/{userId}/{contactId}", headers=headers, json=body)
data = response.json()fetch('https://api.openstudio.one/v1/contact-points/{userId}/{contactId}', {
method: 'PATCH',
headers: {
'X-Api-User-Key': 'YOUR_USER_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
'active': true,
'visible': true,
'data': '...',
'structure': '...',
})
})
.then(res => res.json())
.then(data => console.log(data));Response fields
| Field | Type | Flags | Notes |
|---|---|---|---|
| contact_points | array | — | — |
Example response
{
"success": true,
"data": {
"contact_points": []
},
"error": null
}GET /mailboxes/me
Your own mailbox view (verified/business emails and mailing-list roles)
User keyExample request
curl -X GET "https://api.openstudio.one/v1/mailboxes/me" \
-H "X-Api-User-Key: YOUR_USER_KEY"<?php
$ch = curl_init('https://api.openstudio.one/v1/mailboxes/me');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);
$response = curl_exec($ch);
$data = json_decode($response, true);import requests
headers = {
"X-Api-User-Key": "YOUR_USER_KEY",
}
response = requests.get("https://api.openstudio.one/v1/mailboxes/me", headers=headers)
data = response.json()fetch('https://api.openstudio.one/v1/mailboxes/me', {
method: 'GET',
headers: {
'X-Api-User-Key': 'YOUR_USER_KEY'
}
})
.then(res => res.json())
.then(data => console.log(data));ⓘRuns a real request from your browser directly to mailboxes/me. Keys are only held in this page's memory while you're here — never stored, never sent anywhere else.
Response fields
| Field | Type | Flags | Notes |
|---|---|---|---|
| person | object | — | — |
| structure | string | — | |
| emails | array of objects | — | — |
| ↳type | string | — | — |
| ↳address | string | — | — |
| ↳verified | boolean | — | — |
| mailing_lists | array of objects | — | — |
| ↳code | string | — | — |
| ↳description | string | — | |
| ↳address | string | — | — |
| ↳is_sender | boolean | — | — |
| ↳is_recipient | boolean | — | — |
| can_manage | boolean | — | Whether you (founder or the aunicamail_manage permission) may also view someone else's mailbox via GET /mailboxes/search and GET /mailboxes/{personal_code}. |
Example response
{
"success": true,
"data": {
"person": {
"personal_code": 10000001,
"first_name": "Jane",
"last_name": "Doe",
"avatar_url": null
},
"structure": "ICT (Information & Communication Technology)",
"emails": [
{
"type": "personal_email_address",
"address": "jane.doe@example.com",
"verified": true
}
],
"mailing_lists": [
{
"code": "ALL_STAFF",
"description": "All staff",
"address": "all-staff@example.com",
"is_sender": false,
"is_recipient": true
}
],
"can_manage": false
},
"error": null
}GET /mailboxes/search
Search for a person, to view someone else's mailbox (manage mode)
User keyQuery parameters (3)
Other
| Parameter | Type | Required | Notes |
|---|---|---|---|
| ?q_personal_code | integer | Optional | Exact match. |
| ?q_taxcode | string | Optional | Substring match. |
| ?q_surname | string | Optional | Substring match. |
Example request
curl -X GET "https://api.openstudio.one/v1/mailboxes/search?q_personal_code=123&q_taxcode=...&q_surname=..." \
-H "X-Api-User-Key: YOUR_USER_KEY"<?php
$ch = curl_init('https://api.openstudio.one/v1/mailboxes/search?q_personal_code=123&q_taxcode=...&q_surname=...');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);
$response = curl_exec($ch);
$data = json_decode($response, true);import requests
headers = {
"X-Api-User-Key": "YOUR_USER_KEY",
}
response = requests.get("https://api.openstudio.one/v1/mailboxes/search?q_personal_code=123&q_taxcode=...&q_surname=...", headers=headers)
data = response.json()fetch('https://api.openstudio.one/v1/mailboxes/search?q_personal_code=123&q_taxcode=...&q_surname=...', {
method: 'GET',
headers: {
'X-Api-User-Key': 'YOUR_USER_KEY'
}
})
.then(res => res.json())
.then(data => console.log(data));ⓘRuns a real request from your browser directly to mailboxes/search. Keys are only held in this page's memory while you're here — never stored, never sent anywhere else.
Response fields
| Field | Type | Flags | Notes |
|---|---|---|---|
| people | array of objects | — | — |
| ↳personal_code | integer | — | — |
| ↳first_name | string | — | — |
| ↳last_name | string | — | — |
| ↳taxcode | string | — | |
| ↳recognized | boolean | — | — |
Example response
{
"success": true,
"data": {
"people": [
{
"personal_code": 10000002,
"first_name": "John",
"last_name": "Smith",
"taxcode": null,
"recognized": true
}
]
},
"error": null
}GET /mailboxes/{personal_code}
Another person's mailbox view (manage mode)
User keyExample request
curl -X GET "https://api.openstudio.one/v1/mailboxes/{personal_code}" \
-H "X-Api-User-Key: YOUR_USER_KEY"<?php
$ch = curl_init('https://api.openstudio.one/v1/mailboxes/{personal_code}');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);
$response = curl_exec($ch);
$data = json_decode($response, true);import requests
headers = {
"X-Api-User-Key": "YOUR_USER_KEY",
}
response = requests.get("https://api.openstudio.one/v1/mailboxes/{personal_code}", headers=headers)
data = response.json()fetch('https://api.openstudio.one/v1/mailboxes/{personal_code}', {
method: 'GET',
headers: {
'X-Api-User-Key': 'YOUR_USER_KEY'
}
})
.then(res => res.json())
.then(data => console.log(data));ⓘRuns a real request from your browser directly to mailboxes/{personal_code}. Keys are only held in this page's memory while you're here — never stored, never sent anywhere else.
Response fields
| Field | Type | Flags | Notes |
|---|---|---|---|
| person | object | — | — |
| structure | string | — | |
| emails | array | — | — |
| mailing_lists | array | — | — |
Example response
{
"success": true,
"data": {
"person": {
"personal_code": 10000002,
"first_name": "John",
"last_name": "Smith",
"avatar_url": null
},
"structure": null,
"emails": [],
"mailing_lists": []
},
"error": null
}User accounts
GET /users
List/search other users
User key PaginatedQuery parameters (12)
Pagination
| Parameter | Type | Required | Notes |
|---|---|---|---|
| ?page | integer | Optional | Page number, starting at 1. Defaults to 1. |
| ?limit | integer | Optional | Rows per page, 1-200. Defaults to 50. |
Other
| Parameter | Type | Required | Notes |
|---|---|---|---|
| ?q_name | string | Optional | Substring match on first name. |
| ?q_surname | string | Optional | Substring match on last name. |
| ?q_taxcode | string | Optional | Substring match on tax code. |
| ?q_personal_code | integer | Optional | Exact match on personal_code. |
| ?q_username | string | Optional | Exact match on username. No match -> zero results, same as any other filter here (never silently ignored). |
| ?q_structure | string | Optional | A structure acronym (see GET /departments) — matches anyone with an active membership in it, or (with include_leaders) anyone who leads it. AND'd with every other filter above, same as any of them. |
| ?include_leaders | boolean | Optional | Only meaningful together with q_structure. When set, also matches this structure's leader (GET /departments' leader_user_id) even if they hold no membership row for it at all. |
| ?include_sub | boolean | Optional | Only meaningful together with q_structure. When set, also matches every structure nested underneath it (children and grandchildren — structures in this API are never nested more than 3 levels deep, so this always covers the full subtree). |
| ?unassigned | boolean | Optional | Lists only users with ZERO currently-active structure assignments. Deliberately exclusive with every other filter on this list — set this and every other param is ignored, matching the source dashboard's own search form (checking "unassigned only" there clears and disables the rest of the search). |
| ?expand | string | Optional | Set to memberships to inline each user's active memberships array directly in this response (one extra query for the whole page, not one per user) — avoids a follow-up GET /users/{id}/memberships per row. Omit for the lighter default shape (no memberships key at all). |
Example request
curl -X GET "https://api.openstudio.one/v1/users?q_name=...&q_surname=...&q_taxcode=...&q_personal_code=123&q_username=...&q_structure=...&include_leaders=true&include_sub=true&unassigned=true&expand=...&page=123&limit=123" \
-H "X-Api-User-Key: YOUR_USER_KEY"<?php
$ch = curl_init('https://api.openstudio.one/v1/users?q_name=...&q_surname=...&q_taxcode=...&q_personal_code=123&q_username=...&q_structure=...&include_leaders=true&include_sub=true&unassigned=true&expand=...&page=123&limit=123');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);
$response = curl_exec($ch);
$data = json_decode($response, true);import requests
headers = {
"X-Api-User-Key": "YOUR_USER_KEY",
}
response = requests.get("https://api.openstudio.one/v1/users?q_name=...&q_surname=...&q_taxcode=...&q_personal_code=123&q_username=...&q_structure=...&include_leaders=true&include_sub=true&unassigned=true&expand=...&page=123&limit=123", headers=headers)
data = response.json()fetch('https://api.openstudio.one/v1/users?q_name=...&q_surname=...&q_taxcode=...&q_personal_code=123&q_username=...&q_structure=...&include_leaders=true&include_sub=true&unassigned=true&expand=...&page=123&limit=123', {
method: 'GET',
headers: {
'X-Api-User-Key': 'YOUR_USER_KEY'
}
})
.then(res => res.json())
.then(data => console.log(data));ⓘRuns a real request from your browser directly to users. Keys are only held in this page's memory while you're here — never stored, never sent anywhere else.
Response fields
| Field | Type | Flags | Notes |
|---|---|---|---|
| users | array of objects | — | All query params are optional — omit every one to list everyone on the account, paginated, rather than requiring a search term. Requires manage_personal permission (any non-zero tier) or founder — 403 otherwise, the same access level the dashboard itself checks before letting you reach this data at all. |
| ↳user_id | integer | — | Use this in GET /users/{id}. |
| ↳personal_code | integer | — | A secondary numeric identifier — shown for reference only; user_id is what every other cross-reference in this API uses. |
| ↳first_name | string | — | Matches on the person's first name. |
| ↳last_name | string | — | Matches on the person's last name. |
| ↳photo_url | string | — | Same blank-avatar placeholder fallback as identity.photo_url under GET /me — never null. |
| ↳username | string | — | Matches on the person's username. |
| ↳email | string | — | Login email. |
| ↳status_code | integer | — | Account status — same values as the status_code field under GET /me's users section.1 = Active0 = Inactive-1 = Pending activation-3 = Closed-4 = Closed (undeliverable)2 = Reported |
| ↳status_label | string | — | Simplified version of status_code — same collapsing rule as the status_label field under GET /me's users section (-3/-4 both become disabled).activeinactivepending_activationdisabledreported |
| ↳primary_structure_acronym | string | This person's current primary organizational structure, if they have one. Matches the acronym field on GET /departments. |
|
| ↳primary_structure_title | string | The person's primary structure's display name, if they have one. | |
| ↳memberships | array of objects | — | Only present when expand=memberships was requested. Same shape as GET /users/{id}/memberships's own memberships array. |
Pagination
| Field | Type | Notes |
|---|---|---|
| page | integer | Current page number. |
| limit | integer | Rows per page. |
| total | integer | Total matching rows across all pages. |
| total_pages | integer | Total number of pages. |
| next_page_url | string | Ready-to-call URL for the next page, same filters applied. Null on the last page. |
| prev_page_url | string | Ready-to-call URL for the previous page, same filters applied. Null on the first page. |
Example response
{
"success": true,
"data": {
"users": [
{
"user_id": 999,
"personal_code": 10000999,
"first_name": "Jane",
"last_name": "Doe",
"photo_url": "https://client.openstudio.one/controller/abc123.jpg",
"username": "j.doe",
"email": "j.doe@example.com",
"status_code": 1,
"status_label": "active",
"primary_structure_acronym": "AFA",
"primary_structure_title": "Administration Finance & Accounting"
}
]
},
"error": null,
"pagination": {
"page": 1,
"limit": 50,
"total": 134,
"total_pages": 3,
"next_page_url": "https://api.openstudio.one/v1/departments?page=2&limit=50",
"prev_page_url": null
}
}GET /users/{id}
Get another user's identity/memberships/director data
User keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The target user's user_id — the same id space as user_id everywhere else in this API (e.g. GET /users' user_id field, author_user_id, operator_user_id, ...). This is NEVER the personal_code (a separate secondary reference number you'll see alongside user_id in several responses) — every {id}/*_user_id in this API always means user_id, with no exceptions. See the "Identifying users" guide section. |
Query parameters (1)
Other
| Parameter | Type | Required | Notes |
|---|---|---|---|
| ?include | string | Optional | Comma-separated subset of identity, memberships, director to return — e.g. include=identity to fetch just one. Defaults to all 3 if omitted. Any other value is rejected with a 422. |
Example request
curl -X GET "https://api.openstudio.one/v1/users/{id}?include=..." \
-H "X-Api-User-Key: YOUR_USER_KEY"<?php
$ch = curl_init('https://api.openstudio.one/v1/users/{id}?include=...');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);
$response = curl_exec($ch);
$data = json_decode($response, true);import requests
headers = {
"X-Api-User-Key": "YOUR_USER_KEY",
}
response = requests.get("https://api.openstudio.one/v1/users/{id}?include=...", headers=headers)
data = response.json()fetch('https://api.openstudio.one/v1/users/{id}?include=...', {
method: 'GET',
headers: {
'X-Api-User-Key': 'YOUR_USER_KEY'
}
})
.then(res => res.json())
.then(data => console.log(data));ⓘRuns a real request from your browser directly to users/{id}. Keys are only held in this page's memory while you're here — never stored, never sent anywhere else.
Response fields
| Field | Type | Flags | Notes |
|---|---|---|---|
| identity | object | Same shape as GET /me's identity, PLUS can_edit_identity and permission_level (GET /users/{id} only — GET /me has no equivalent, editing/viewing your own identity isn't gated the same way). permission_level is the CALLER's own access tier on this app (full / read_only / enabled_users_only / none), not anything about the target user. Omitted from the response entirely if excluded via include. |
|
| memberships | array of objects | — | Same shape as GET /me's memberships. Omitted from the response entirely if excluded via include. For write access to this data, see GET /users/{id}/memberships and its sibling routes below. |
| director | object | Same shape as GET /me's director. Omitted from the response entirely if excluded via include. |
Example response
{
"success": true,
"data": {
"identity": {
"first_name": "Jane",
"last_name": "Doe",
"recognition_status": "recognized",
"can_edit_identity": false
},
"memberships": [
{
"id": 41,
"structure_acronym": "AFA",
"structure_title": "Administration Finance & Accounting",
"level": 2,
"parent_title": "Main Area",
"area_title": "Main Area",
"is_primary": true,
"is_leader": false,
"is_manager": false,
"since": "2019-08-19 15:40:15",
"until": null,
"notes": null
}
],
"director": null
},
"error": null
}Identity & Data Management
PATCH /users/{id}/identity
Edit a user's identity fields (name, birth info, tax code)
User keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The target user's id — same as on GET /users/{id}. |
Request body
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| last_name | string | — | Optional | Cannot be set to an empty string. |
| first_name | string | — | Optional | Cannot be set to an empty string. |
| middle_name | string | — | Optional | Send an empty string to clear it. |
| sex | string | — | Optional | M/F/O.MFO |
| birth_date | string | — | Optional | Format DD/MM/YYYY — matches how GET /me's identity returns it, deliberately NOT YYYY-MM-DD. Validated as a real calendar date, rejected with a 422 otherwise. |
| birth_state | string | — | Optional | Birth country. |
| birth_province | string | — | Optional | Birth state/province. |
| birth_city | string | — | Optional | Birth city. |
| citizenship | string | — | Optional | Citizenship. |
| citizenship_other | string | — | Optional | Used only when citizenship is set to a value meaning "other" — free text. |
| taxcode | string | — | Optional | Cannot be set to an empty string. No format/checksum validation is applied — sent as-is once non-empty. |
Example request
curl -X PATCH "https://api.openstudio.one/v1/users/{id}/identity" \
-H "X-Api-User-Key: YOUR_USER_KEY" \
-H "Content-Type: application/json" \
-d '{"last_name":"...","first_name":"...","middle_name":"...","sex":"...","birth_date":"...","birth_state":"...","birth_province":"...","birth_city":"...","citizenship":"...","citizenship_other":"...","taxcode":"..."}'<?php
$ch = curl_init('https://api.openstudio.one/v1/users/{id}/identity');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'last_name' => '...',
'first_name' => '...',
'middle_name' => '...',
'sex' => '...',
'birth_date' => '...',
'birth_state' => '...',
'birth_province' => '...',
'birth_city' => '...',
'citizenship' => '...',
'citizenship_other' => '...',
'taxcode' => '...',
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY', 'Content-Type: application/json']);
$response = curl_exec($ch);
$data = json_decode($response, true);import requests
headers = {
"X-Api-User-Key": "YOUR_USER_KEY",
}
body = {
'last_name': '...',
'first_name': '...',
'middle_name': '...',
'sex': '...',
'birth_date': '...',
'birth_state': '...',
'birth_province': '...',
'birth_city': '...',
'citizenship': '...',
'citizenship_other': '...',
'taxcode': '...',
}
response = requests.patch("https://api.openstudio.one/v1/users/{id}/identity", headers=headers, json=body)
data = response.json()fetch('https://api.openstudio.one/v1/users/{id}/identity', {
method: 'PATCH',
headers: {
'X-Api-User-Key': 'YOUR_USER_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
'last_name': '...',
'first_name': '...',
'middle_name': '...',
'sex': '...',
'birth_date': '...',
'birth_state': '...',
'birth_province': '...',
'birth_city': '...',
'citizenship': '...',
'citizenship_other': '...',
'taxcode': '...',
})
})
.then(res => res.json())
.then(data => console.log(data));Response fields
| Field | Type | Flags | Notes |
|---|---|---|---|
| first_name | string | — | See GET /me's identity. |
| middle_name | string | See GET /me's identity. |
|
| last_name | string | — | See GET /me's identity. |
| sex | string | See GET /me's identity. |
|
| birth_date | string | See GET /me's identity. |
|
| birth_state | string | See GET /me's identity. |
|
| birth_province | string | See GET /me's identity. |
|
| birth_city | string | See GET /me's identity. |
|
| citizenship | string | See GET /me's identity. |
|
| citizenship_other | string | See GET /me's identity. |
|
| tax_code | string | See GET /me's identity. |
|
| recognition_status | string | — | See GET /me's identity. Unaffected by an identity edit.recognizedrecognized_papernot_recognized |
Example response
{
"success": true,
"data": {
"first_name": "Jane",
"last_name": "Doe",
"sex": "F",
"birth_date": "17/01/1995",
"taxcode": "DOEJNA95A57F205X",
"recognition_status": "recognized"
},
"error": null
}POST /users/{id}/recognition
Mark a user as identity-recognized
User keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The target user's id — same as on GET /users/{id}. |
Request body
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| method | string | — | Required | manual records recognition against a physical/paper document (no digital file on file) — the resulting status is recognized_paper. document requires doc_id and results in recognized.manualdocument |
| doc_id | integer | — | Optional | Required when method is document. Must already exist on file for this user — this endpoint only REFERENCES an existing document, it never uploads one (document upload isn't available via this API yet). |
| notes | string | — | Optional | Free text, truncated to 500 characters. |
Example request
curl -X POST "https://api.openstudio.one/v1/users/{id}/recognition" \
-H "X-Api-User-Key: YOUR_USER_KEY" \
-H "Content-Type: application/json" \
-d '{"method":"...","doc_id":123,"notes":"..."}'<?php
$ch = curl_init('https://api.openstudio.one/v1/users/{id}/recognition');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'method' => '...',
'doc_id' => 123,
'notes' => '...',
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY', 'Content-Type: application/json']);
$response = curl_exec($ch);
$data = json_decode($response, true);import requests
headers = {
"X-Api-User-Key": "YOUR_USER_KEY",
}
body = {
'method': '...',
'doc_id': 123,
'notes': '...',
}
response = requests.post("https://api.openstudio.one/v1/users/{id}/recognition", headers=headers, json=body)
data = response.json()fetch('https://api.openstudio.one/v1/users/{id}/recognition', {
method: 'POST',
headers: {
'X-Api-User-Key': 'YOUR_USER_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
'method': '...',
'doc_id': 123,
'notes': '...',
})
})
.then(res => res.json())
.then(data => console.log(data));Response fields
| Field | Type | Flags | Notes |
|---|---|---|---|
| first_name | string | — | See GET /me's identity. |
| last_name | string | — | See GET /me's identity. |
| recognition_status | string | — | See GET /me's identity. Reflects the outcome of this call.recognizedrecognized_papernot_recognized |
| recognition_date | string | See GET /me's identity. |
Example response
{
"success": true,
"data": {
"first_name": "Jane",
"last_name": "Doe",
"recognition_status": "recognized_paper",
"recognition_date": "21/08/2026"
},
"error": null
}PATCH /users/{id}/contacts
Edit a user's phone numbers
User keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The target user's id — same as on GET /users/{id}. |
Request body
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| mobile_number | string | — | Optional | Send an empty string to clear it. No format validation is applied server-side. |
| phone_number | string | — | Optional | Send an empty string to clear it. |
| fax_number | string | — | Optional | Send an empty string to clear it. |
Example request
curl -X PATCH "https://api.openstudio.one/v1/users/{id}/contacts" \
-H "X-Api-User-Key: YOUR_USER_KEY" \
-H "Content-Type: application/json" \
-d '{"mobile_number":"...","phone_number":"...","fax_number":"..."}'<?php
$ch = curl_init('https://api.openstudio.one/v1/users/{id}/contacts');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'mobile_number' => '...',
'phone_number' => '...',
'fax_number' => '...',
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY', 'Content-Type: application/json']);
$response = curl_exec($ch);
$data = json_decode($response, true);import requests
headers = {
"X-Api-User-Key": "YOUR_USER_KEY",
}
body = {
'mobile_number': '...',
'phone_number': '...',
'fax_number': '...',
}
response = requests.patch("https://api.openstudio.one/v1/users/{id}/contacts", headers=headers, json=body)
data = response.json()fetch('https://api.openstudio.one/v1/users/{id}/contacts', {
method: 'PATCH',
headers: {
'X-Api-User-Key': 'YOUR_USER_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
'mobile_number': '...',
'phone_number': '...',
'fax_number': '...',
})
})
.then(res => res.json())
.then(data => console.log(data));Response fields
| Field | Type | Flags | Notes |
|---|---|---|---|
| first_name | string | — | See GET /me's identity. |
| last_name | string | — | See GET /me's identity. |
| phone_number | string | See GET /me's identity. Reflects phone_number if it was part of this PATCH. |
|
| mobile_number | string | See GET /me's identity. Reflects mobile_number if it was part of this PATCH. |
|
| fax_number | string | See GET /me's identity. Reflects fax_number if it was part of this PATCH. |
Example response
{
"success": true,
"data": {
"first_name": "Jane",
"last_name": "Doe",
"mobile_number": "+393460000000",
"phone_number": null,
"fax_number": null
},
"error": null
}PATCH /users/{id}/residence
Edit a user's home address
User keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The target user's id — same as on GET /users/{id}. |
Request body
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| home_country | string | — | Optional | Home country. |
| home_district | string | — | Optional | Home state/province. |
| home_city | string | — | Optional | Home city. |
| home_postcode | string | — | Optional | Home postcode. |
| home_address | string | — | Optional | Street address. |
| home_address_type | string | — | Optional | Free text, e.g. "VIA". |
| home_number | string | — | Optional | Street number. |
| home_co | string | — | Optional | Care-of / additional address line. |
| home_date | string | — | Optional | Format YYYY-MM-DD — "since" date at this address. Unlike identity's birth_date, this one IS a real calendar-date format. Validated, rejected with a 422 if not a real date. |
Example request
curl -X PATCH "https://api.openstudio.one/v1/users/{id}/residence" \
-H "X-Api-User-Key: YOUR_USER_KEY" \
-H "Content-Type: application/json" \
-d '{"home_country":"...","home_district":"...","home_city":"...","home_postcode":"...","home_address":"...","home_address_type":"...","home_number":"...","home_co":"...","home_date":"..."}'<?php
$ch = curl_init('https://api.openstudio.one/v1/users/{id}/residence');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'home_country' => '...',
'home_district' => '...',
'home_city' => '...',
'home_postcode' => '...',
'home_address' => '...',
'home_address_type' => '...',
'home_number' => '...',
'home_co' => '...',
'home_date' => '...',
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY', 'Content-Type: application/json']);
$response = curl_exec($ch);
$data = json_decode($response, true);import requests
headers = {
"X-Api-User-Key": "YOUR_USER_KEY",
}
body = {
'home_country': '...',
'home_district': '...',
'home_city': '...',
'home_postcode': '...',
'home_address': '...',
'home_address_type': '...',
'home_number': '...',
'home_co': '...',
'home_date': '...',
}
response = requests.patch("https://api.openstudio.one/v1/users/{id}/residence", headers=headers, json=body)
data = response.json()fetch('https://api.openstudio.one/v1/users/{id}/residence', {
method: 'PATCH',
headers: {
'X-Api-User-Key': 'YOUR_USER_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
'home_country': '...',
'home_district': '...',
'home_city': '...',
'home_postcode': '...',
'home_address': '...',
'home_address_type': '...',
'home_number': '...',
'home_co': '...',
'home_date': '...',
})
})
.then(res => res.json())
.then(data => console.log(data));Response fields
| Field | Type | Flags | Notes |
|---|---|---|---|
| first_name | string | — | See GET /me's identity. |
| last_name | string | — | See GET /me's identity. |
| home_address | object | — | See GET /me's identity' own home_address object — same 9 fields, reflecting whichever ones were part of this PATCH. |
| ↳country | string | Home country, if recorded. | |
| ↳district | string | Home state/province, if recorded. | |
| ↳city | string | Home city, if recorded. | |
| ↳address_type | string | The kind of address this is (e.g. residence vs domicile), if recorded. | |
| ↳address | string | Street address, if recorded. | |
| ↳number | string | Street number, if recorded. | |
| ↳postcode | string | Postcode, if recorded. | |
| ↳co | string | Care-of / additional address line, if recorded. | |
| ↳since | string | Echoes home_date if it was part of this request, otherwise the existing value. Format YYYY-MM-DD. |
Example response
{
"success": true,
"data": {
"first_name": "Jane",
"last_name": "Doe",
"home_address": {
"country": "Italy",
"city": "Milano",
"address": "Garibaldi",
"since": "2015-09-05"
}
},
"error": null
}GET /users/{id}/history
Get a user's identity/contact/address/photo change history
User keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The target user's id — same as on GET /users/{id}. |
Query parameters (1)
Other
| Parameter | Type | Required | Notes |
|---|---|---|---|
| ?expand | string | Optional | Set to performed_by to also inline basic details for whoever made each change, instead of just the bare performed_by_personal_code. One extra batch query regardless of how many history rows come back — never a lookup per row. Any other value is rejected with a 422. |
Example request
curl -X GET "https://api.openstudio.one/v1/users/{id}/history?expand=..." \
-H "X-Api-User-Key: YOUR_USER_KEY"<?php
$ch = curl_init('https://api.openstudio.one/v1/users/{id}/history?expand=...');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);
$response = curl_exec($ch);
$data = json_decode($response, true);import requests
headers = {
"X-Api-User-Key": "YOUR_USER_KEY",
}
response = requests.get("https://api.openstudio.one/v1/users/{id}/history?expand=...", headers=headers)
data = response.json()fetch('https://api.openstudio.one/v1/users/{id}/history?expand=...', {
method: 'GET',
headers: {
'X-Api-User-Key': 'YOUR_USER_KEY'
}
})
.then(res => res.json())
.then(data => console.log(data));ⓘRuns a real request from your browser directly to users/{id}/history. Keys are only held in this page's memory while you're here — never stored, never sent anywhere else.
Response fields
| Field | Type | Flags | Notes |
|---|---|---|---|
| history | array of objects | — | Every recorded change across identity, recognition, contacts, residence, and photo edits for this user, newest first — a different table/scope from GET /users/{id}/memberships/history, which only covers structure assignments. |
| ↳field | string | — | Which field changed, e.g. taxcode, mobile_number, home_city, recognition_status, picture. |
| ↳old_value | string | The field's value before this change, if applicable. | |
| ↳new_value | string | The field's value after this change. | |
| ↳performed_by_personal_code | integer | The identifier of whoever made this change. See ?expand=performed_by for a resolved version instead of this bare id. |
|
| ↳performed_by | object | Only present when ?expand=performed_by was requested. null if performed_by_personal_code itself was null, or didn't resolve to a record on this tenant. |
|
| ↳user_id | integer | — | The same user_id used everywhere else in this API — NOT performed_by_personal_code itself, see the "Identifying users" guide. |
| ↳personal_code | integer | — | Same value as performed_by_personal_code on the row itself — included here too so this object is usable on its own without also reading the sibling field. |
| ↳first_name | string | — | The person who made this change — first name. |
| ↳last_name | string | — | The person who made this change — last name. |
| ↳created_at | string | — | When this change was recorded. |
Example response
{
"success": true,
"data": {
"history": [
{
"field": "mobile_number",
"old_value": null,
"new_value": "+393460000000",
"performed_by_personal_code": 10000001,
"created_at": "2026-08-21 09:00:00"
}
]
},
"error": null
}POST /users/{id}/photo
Upload a new profile photo (pending approval)
User keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The target user's id — same as on GET /users/{id}. |
Request body
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| photo_base64 | string | — | Required | Base64-encoded image (jpg/png/gif), optionally a data: URI — same convenience shape as PATCH /tenant/settings's logo_base64 field. Validated by actual file content (not just the claimed type), max 5 MB. A new upload replaces any existing PENDING upload — it does not touch the current live photo until approved. |
Example request
curl -X POST "https://api.openstudio.one/v1/users/{id}/photo" \
-H "X-Api-User-Key: YOUR_USER_KEY" \
-H "Content-Type: application/json" \
-d '{"photo_base64":"..."}'<?php
$ch = curl_init('https://api.openstudio.one/v1/users/{id}/photo');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'photo_base64' => '...',
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY', 'Content-Type: application/json']);
$response = curl_exec($ch);
$data = json_decode($response, true);import requests
headers = {
"X-Api-User-Key": "YOUR_USER_KEY",
}
body = {
'photo_base64': '...',
}
response = requests.post("https://api.openstudio.one/v1/users/{id}/photo", headers=headers, json=body)
data = response.json()fetch('https://api.openstudio.one/v1/users/{id}/photo', {
method: 'POST',
headers: {
'X-Api-User-Key': 'YOUR_USER_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
'photo_base64': '...',
})
})
.then(res => res.json())
.then(data => console.log(data));Response fields
| Field | Type | Flags | Notes |
|---|---|---|---|
| photo_url | string | — | The current LIVE (approved) photo, or the blank-avatar placeholder if none has ever been approved. Unchanged by this call — a fresh upload only ever affects pending_photo_url until approved. |
| pending_photo_url | string | The photo just uploaded, awaiting approval via POST /users/{id}/photo/approve. |
|
| is_validated | boolean | — | Whether photo_url (the live photo) has been approved. Unaffected by a new pending upload. |
Example response
{
"success": true,
"data": {
"photo_url": "https://www.openstudio.one/assets/img/avatars/blank_avatar.jpg",
"pending_photo_url": "https://client.openstudio.one/controller/abc123.jpg",
"is_validated": false
},
"error": null
}DELETE /users/{id}/photo
Remove the current live profile photo
User keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The target user's id — same as on GET /users/{id}. |
Example request
curl -X DELETE "https://api.openstudio.one/v1/users/{id}/photo" \
-H "X-Api-User-Key: YOUR_USER_KEY"<?php
$ch = curl_init('https://api.openstudio.one/v1/users/{id}/photo');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);
$response = curl_exec($ch);
$data = json_decode($response, true);import requests
headers = {
"X-Api-User-Key": "YOUR_USER_KEY",
}
response = requests.delete("https://api.openstudio.one/v1/users/{id}/photo", headers=headers)
data = response.json()fetch('https://api.openstudio.one/v1/users/{id}/photo', {
method: 'DELETE',
headers: {
'X-Api-User-Key': 'YOUR_USER_KEY'
}
})
.then(res => res.json())
.then(data => console.log(data));Response fields
| Field | Type | Flags | Notes |
|---|---|---|---|
| photo_url | string | — | Always the blank-avatar placeholder on success — this endpoint removes the live photo, it does not delete the identity record itself. |
| pending_photo_url | string | Unaffected — only the LIVE photo is removed by this call. | |
| is_validated | boolean | — | Always false on success. |
Example response
{
"success": true,
"data": {
"photo_url": "https://www.openstudio.one/assets/img/avatars/blank_avatar.jpg",
"pending_photo_url": null,
"is_validated": false
},
"error": null
}POST /users/{id}/photo/approve
Approve the pending photo upload, making it live
User keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The target user's id — same as on GET /users/{id}. |
Example request
curl -X POST "https://api.openstudio.one/v1/users/{id}/photo/approve" \
-H "X-Api-User-Key: YOUR_USER_KEY"<?php
$ch = curl_init('https://api.openstudio.one/v1/users/{id}/photo/approve');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);
$response = curl_exec($ch);
$data = json_decode($response, true);import requests
headers = {
"X-Api-User-Key": "YOUR_USER_KEY",
}
response = requests.post("https://api.openstudio.one/v1/users/{id}/photo/approve", headers=headers)
data = response.json()fetch('https://api.openstudio.one/v1/users/{id}/photo/approve', {
method: 'POST',
headers: {
'X-Api-User-Key': 'YOUR_USER_KEY'
}
})
.then(res => res.json())
.then(data => console.log(data));Response fields
| Field | Type | Flags | Notes |
|---|---|---|---|
| photo_url | string | — | Now equal to what pending_photo_url was before this call. |
| pending_photo_url | string | Always null on success — the pending upload has been promoted, so there's nothing left pending. |
|
| is_validated | boolean | — | Always true on success. 404 if there was no pending upload to approve. |
Example response
{
"success": true,
"data": {
"photo_url": "https://client.openstudio.one/controller/abc123.jpg",
"pending_photo_url": null,
"is_validated": true
},
"error": null
}DELETE /users/{id}/photo/pending
Reject the pending photo upload
User keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The target user's id — same as on GET /users/{id}. |
Example request
curl -X DELETE "https://api.openstudio.one/v1/users/{id}/photo/pending" \
-H "X-Api-User-Key: YOUR_USER_KEY"<?php
$ch = curl_init('https://api.openstudio.one/v1/users/{id}/photo/pending');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);
$response = curl_exec($ch);
$data = json_decode($response, true);import requests
headers = {
"X-Api-User-Key": "YOUR_USER_KEY",
}
response = requests.delete("https://api.openstudio.one/v1/users/{id}/photo/pending", headers=headers)
data = response.json()fetch('https://api.openstudio.one/v1/users/{id}/photo/pending', {
method: 'DELETE',
headers: {
'X-Api-User-Key': 'YOUR_USER_KEY'
}
})
.then(res => res.json())
.then(data => console.log(data));Response fields
| Field | Type | Flags | Notes |
|---|---|---|---|
| photo_url | string | — | Unaffected — only the PENDING upload is discarded by this call. May be the blank-avatar placeholder if no live photo was ever approved. |
| pending_photo_url | string | Always null on success. Rejecting when there was nothing pending is a harmless no-op, not an error. |
|
| is_validated | boolean | — | Unaffected. |
Example response
{
"success": true,
"data": {
"photo_url": "https://www.openstudio.one/assets/img/avatars/blank_avatar.jpg",
"pending_photo_url": null,
"is_validated": false
},
"error": null
}GET /users/{id}/documents
List a user's identity documents
User keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The target user's id — same as on GET /users/{id}. |
Example request
curl -X GET "https://api.openstudio.one/v1/users/{id}/documents" \
-H "X-Api-User-Key: YOUR_USER_KEY"<?php
$ch = curl_init('https://api.openstudio.one/v1/users/{id}/documents');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);
$response = curl_exec($ch);
$data = json_decode($response, true);import requests
headers = {
"X-Api-User-Key": "YOUR_USER_KEY",
}
response = requests.get("https://api.openstudio.one/v1/users/{id}/documents", headers=headers)
data = response.json()fetch('https://api.openstudio.one/v1/users/{id}/documents', {
method: 'GET',
headers: {
'X-Api-User-Key': 'YOUR_USER_KEY'
}
})
.then(res => res.json())
.then(data => console.log(data));ⓘRuns a real request from your browser directly to users/{id}/documents. Keys are only held in this page's memory while you're here — never stored, never sent anywhere else.
Response fields
| Field | Type | Flags | Notes |
|---|---|---|---|
| documents | array of objects | — | Requires full edit access to this user's data (not just view access) — matches the source dashboard app exactly, where even the document LIST sits behind the same permission as editing it, unlike identity/contacts/residence. |
| ↳id | integer | — | Use this in POST /users/{id}/documents/{id}/validate, GET /users/{id}/documents/{id}/download, and DELETE /users/{id}/documents/{id}. |
| ↳type | string | — | Free text, e.g. "ID CARD", "Passport", "Driving license" — not a fixed enum. |
| ↳authority | string | Who issued the document. | |
| ↳number | string | The document's own number/code, if recorded. | |
| ↳issued_on | string | Format DD/MM/YYYY. |
|
| ↳expires_on | string | Format DD/MM/YYYY. |
|
| ↳status | integer | — | How far along validation is — see the enum values above.0 = Unvalidated1 = File validated2 = Original validated in person3 = Fully validated (both) |
| ↳status_label | string | — | Lowercase name of status.unvalidatedfile_validatedoriginal_validatedfully_validated |
| ↳validation | string | Free-text validation record, e.g. "21/08/2026 (Jane Doe)" — null until validated at least once. |
|
| ↳used_for_recognition | boolean | — | Whether this document was used as the basis for POST /users/{id}/recognition's document method. |
| ↳has_file | boolean | — | Whether a file exists for this document AT ALL, via either storage mechanism — check file_url to tell which. false means there's genuinely nothing to view or download. |
| ↳file_url | string | A direct, public, already-downloadable URL — but ONLY for a document that predates this API's file-manager migration (uploaded via the legacy path, which stores a plain public URL instead of a file-manager id). null for a normal, file-manager-backed document (created via POST /users/{id}/documents) — fetch those via GET /users/{id}/documents/{id}/download instead, which needs the usual auth header this bare URL does not carry. |
|
| ↳created_at | string | — | When this document record was added. |
Example response
{
"success": true,
"data": {
"documents": [
{
"id": 26,
"type": "Personal ID",
"authority": "Comune di Ferno",
"number": "AZ 1848194",
"issued_on": "25/08/2022",
"expires_on": "06/09/2032",
"status": 3,
"status_label": "fully_validated",
"validation": "13/12/2025 (Jane Doe)",
"used_for_recognition": true,
"has_file": true,
"file_url": null,
"created_at": "2022-11-25 18:08:31"
}
]
},
"error": null
}POST /users/{id}/documents
Upload a new identity document
User keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The target user's id — same as on GET /users/{id}. |
Request body
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| type | string | — | Required | Free text, e.g. "ID CARD", "Passport". |
| authority | string | — | Optional | The issuing authority, if known. |
| number | string | — | Optional | The document's own number/code. |
| issued_on | string | — | Optional | Format DD/MM/YYYY. Validated as a real calendar date, rejected with a 422 otherwise. |
| expires_on | string | — | Optional | Format DD/MM/YYYY. Validated as a real calendar date, rejected with a 422 otherwise. |
| file_base64 | string | — | Required | Base64-encoded file (pdf/jpg/png), optionally a data: URI — same convenience shape as PATCH /tenant/settings's logo_base64 field. Validated by actual file content, max 10 MB. Stored via the file-manager system, not returned as a raw public URL — see has_file/GET /users/{id}/documents/{id}/download. |
Example request
curl -X POST "https://api.openstudio.one/v1/users/{id}/documents" \
-H "X-Api-User-Key: YOUR_USER_KEY" \
-H "Content-Type: application/json" \
-d '{"type":"...","authority":"...","number":"...","issued_on":"...","expires_on":"...","file_base64":"..."}'<?php
$ch = curl_init('https://api.openstudio.one/v1/users/{id}/documents');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'type' => '...',
'authority' => '...',
'number' => '...',
'issued_on' => '...',
'expires_on' => '...',
'file_base64' => '...',
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY', 'Content-Type: application/json']);
$response = curl_exec($ch);
$data = json_decode($response, true);import requests
headers = {
"X-Api-User-Key": "YOUR_USER_KEY",
}
body = {
'type': '...',
'authority': '...',
'number': '...',
'issued_on': '...',
'expires_on': '...',
'file_base64': '...',
}
response = requests.post("https://api.openstudio.one/v1/users/{id}/documents", headers=headers, json=body)
data = response.json()fetch('https://api.openstudio.one/v1/users/{id}/documents', {
method: 'POST',
headers: {
'X-Api-User-Key': 'YOUR_USER_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
'type': '...',
'authority': '...',
'number': '...',
'issued_on': '...',
'expires_on': '...',
'file_base64': '...',
})
})
.then(res => res.json())
.then(data => console.log(data));Response fields
| Field | Type | Flags | Notes |
|---|---|---|---|
| id | integer | — | See GET /users/{id}/documents. |
| type | string | — | Echoes type. |
| authority | string | Echoes authority, or null if omitted. |
|
| number | string | Echoes number, or null if omitted. |
|
| issued_on | string | Echoes issued_on, or null if omitted. |
|
| expires_on | string | Echoes expires_on, or null if omitted. |
|
| status | integer | — | Always 0 on creation — a newly uploaded document is always unvalidated. Use POST /users/{id}/documents/{id}/validate afterward. |
| status_label | string | — | Always unvalidated on creation. |
| validation | string | Always null on creation. |
|
| used_for_recognition | boolean | — | Always false on creation. |
| has_file | boolean | — | Always true on creation — this endpoint requires file_base64. |
| file_url | string | Always null on creation — a document created here is always file-manager-backed, never the legacy-URL shape. See GET /users/{id}/documents's own file_url description. |
|
| created_at | string | — | When this document record was added. |
Example response
{
"success": true,
"data": {
"id": 43,
"type": "Passport",
"authority": null,
"number": null,
"issued_on": null,
"expires_on": null,
"status": 0,
"status_label": "unvalidated",
"validation": null,
"used_for_recognition": false,
"has_file": true,
"file_url": null,
"created_at": "2026-08-21 12:00:00"
},
"error": null
}DELETE /users/{id}/documents/{document_id}
Delete an unvalidated document
User keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The target user's id — same as on GET /users/{id}. |
| {document_id} | integer | One document's own id, from the id field of an item in GET /users/{id}/documents's documents array (or from the response of POST /users/{id}/documents, right after uploading it). |
Example request
curl -X DELETE "https://api.openstudio.one/v1/users/{id}/documents/{document_id}" \
-H "X-Api-User-Key: YOUR_USER_KEY"<?php
$ch = curl_init('https://api.openstudio.one/v1/users/{id}/documents/{document_id}');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);
$response = curl_exec($ch);
$data = json_decode($response, true);import requests
headers = {
"X-Api-User-Key": "YOUR_USER_KEY",
}
response = requests.delete("https://api.openstudio.one/v1/users/{id}/documents/{document_id}", headers=headers)
data = response.json()fetch('https://api.openstudio.one/v1/users/{id}/documents/{document_id}', {
method: 'DELETE',
headers: {
'X-Api-User-Key': 'YOUR_USER_KEY'
}
})
.then(res => res.json())
.then(data => console.log(data));Response fields
| Field | Type | Flags | Notes |
|---|---|---|---|
| deleted | boolean | — | Always true on success. Rejected with a 403 if the document has already been validated at any level (status other than 0) — matches the source app's own rule exactly: only an unvalidated document can be deleted. |
Example response
{
"success": true,
"data": {
"deleted": true
},
"error": null
}POST /users/{id}/documents/{document_id}/validate
Validate a document
User keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The target user's id — same as on GET /users/{id}. |
| {document_id} | integer | Same id as on DELETE /users/{id}/documents/{document_id}. |
Request body
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| level | string | — | Required | Sets status to 1, 2, or 3 respectively.file = The uploaded file has been reviewed.original = The physical/original document has been checked in person.both = Both of the above. |
Example request
curl -X POST "https://api.openstudio.one/v1/users/{id}/documents/{document_id}/validate" \
-H "X-Api-User-Key: YOUR_USER_KEY" \
-H "Content-Type: application/json" \
-d '{"level":"..."}'<?php
$ch = curl_init('https://api.openstudio.one/v1/users/{id}/documents/{document_id}/validate');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'level' => '...',
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY', 'Content-Type: application/json']);
$response = curl_exec($ch);
$data = json_decode($response, true);import requests
headers = {
"X-Api-User-Key": "YOUR_USER_KEY",
}
body = {
'level': '...',
}
response = requests.post("https://api.openstudio.one/v1/users/{id}/documents/{document_id}/validate", headers=headers, json=body)
data = response.json()fetch('https://api.openstudio.one/v1/users/{id}/documents/{document_id}/validate', {
method: 'POST',
headers: {
'X-Api-User-Key': 'YOUR_USER_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
'level': '...',
})
})
.then(res => res.json())
.then(data => console.log(data));Response fields
| Field | Type | Flags | Notes |
|---|---|---|---|
| id | integer | — | See GET /users/{id}/documents. |
| status | integer | — | Reflects level from this request.1 = File validated2 = Original validated in person3 = Fully validated (both) |
| status_label | string | — | Lowercase name of status.file_validatedoriginal_validatedfully_validated |
| validation | string | — | The new validation record, "DD/MM/YYYY (Operator Name)" — today's date and your own name. |
Example response
{
"success": true,
"data": {
"id": 43,
"status": 3,
"status_label": "fully_validated",
"validation": "21/08/2026 (Jane Doe)"
},
"error": null
}GET /users/{id}/documents/{document_id}/download
Download a document's file
User keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The target user's id — same as on GET /users/{id}. |
| {document_id} | integer | Same id as on DELETE /users/{id}/documents/{document_id}. Must be file-manager-backed (has_file=true AND file_url=null — see GET /users/{id}/documents) or this returns a 404. |
Example request
curl -X GET "https://api.openstudio.one/v1/users/{id}/documents/{document_id}/download" \
-H "X-Api-User-Key: YOUR_USER_KEY"<?php
$ch = curl_init('https://api.openstudio.one/v1/users/{id}/documents/{document_id}/download');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);
$response = curl_exec($ch);
$data = json_decode($response, true);import requests
headers = {
"X-Api-User-Key": "YOUR_USER_KEY",
}
response = requests.get("https://api.openstudio.one/v1/users/{id}/documents/{document_id}/download", headers=headers)
data = response.json()fetch('https://api.openstudio.one/v1/users/{id}/documents/{document_id}/download', {
method: 'GET',
headers: {
'X-Api-User-Key': 'YOUR_USER_KEY'
}
})
.then(res => res.json())
.then(data => console.log(data));ⓘRuns a real request from your browser directly to users/{id}/documents/{document_id}/download. Keys are only held in this page's memory while you're here — never stored, never sent anywhere else.
Response fields
| Field | Type | Flags | Notes |
|---|
Example response
{
"success": true,
"data": {},
"error": null
}Memberships
GET /users/{id}/memberships
List a user's active organizational structure assignments
User keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The target user's id — same as on GET /users/{id}. |
Example request
curl -X GET "https://api.openstudio.one/v1/users/{id}/memberships" \
-H "X-Api-User-Key: YOUR_USER_KEY"<?php
$ch = curl_init('https://api.openstudio.one/v1/users/{id}/memberships');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);
$response = curl_exec($ch);
$data = json_decode($response, true);import requests
headers = {
"X-Api-User-Key": "YOUR_USER_KEY",
}
response = requests.get("https://api.openstudio.one/v1/users/{id}/memberships", headers=headers)
data = response.json()fetch('https://api.openstudio.one/v1/users/{id}/memberships', {
method: 'GET',
headers: {
'X-Api-User-Key': 'YOUR_USER_KEY'
}
})
.then(res => res.json())
.then(data => console.log(data));ⓘRuns a real request from your browser directly to users/{id}/memberships. Keys are only held in this page's memory while you're here — never stored, never sent anywhere else.
Response fields
| Field | Type | Flags | Notes |
|---|---|---|---|
| memberships | array of objects | — | Same shape and same "active" definition as the memberships array on GET /me / GET /users/{id} — this route exists on its own specifically so the id it returns can be used with the write routes below. |
| ↳id | integer | This assignment's own id — use this in PATCH /users/{id}/memberships/{membership_id}, DELETE /users/{id}/memberships/{membership_id}, and POST /users/{id}/memberships/{membership_id}/primary. null on a leadership-only row (no actual membership — see is_leader/is_manager). |
|
| ↳structure_acronym | string | — | Matches the acronym field on GET /departments. |
| ↳structure_title | string | — | This structure's display name. |
| ↳level | integer | — | This structure's level in the hierarchy.1 = Area2 = Department3 = Unit |
| ↳parent_title | string | Display title of the structure's immediate parent, resolved server-side. Null only for an L1 Area. | |
| ↳area_title | string | Display title of the structure's top-level Area — differs from parent_title for an L3 Unit, equal to it for an L2 Department. Null only for an L1 Area. |
|
| ↳is_primary | boolean | — | At most one primary assignment per user at a time. Always false on a leadership-only row. |
| ↳is_leader | boolean | — | Whether this person is this structure's leader, independent of whether they're also a member of it. |
| ↳is_manager | boolean | — | Whether this person is one of this structure's co-managers, independent of whether they're also a member of it. |
| ↳since | string | When this assignment started. Format YYYY-MM-DD. A small number of rows migrated from before this API existed may show a full YYYY-MM-DD HH:MM:SS timestamp instead — returned exactly as stored, never reformatted. null on a leadership-only row. |
|
| ↳until | string | null = open-ended. A future date = a fixed-term assignment, still active as of now. Format YYYY-MM-DD when set. |
|
| ↳notes | string | Free-text note attached when this assignment was created, if any. |
Example response
{
"success": true,
"data": {
"memberships": [
{
"id": 41,
"structure_acronym": "AFA",
"structure_title": "Administration Finance & Accounting",
"level": 2,
"parent_title": "Main Area",
"area_title": "Main Area",
"is_primary": true,
"is_leader": false,
"is_manager": false,
"since": "2019-08-19",
"until": null,
"notes": null
}
]
},
"error": null
}POST /users/{id}/memberships
Assign a user to a structure
User keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The target user's id — same as on GET /users/{id}. |
Request body
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| structure_acronym | string | — | Required | Must match an active, non-deleted structure acronym on this account (see GET /departments) — rejected with a 422 otherwise. Rejected with a 409 if this user already has an active assignment (open-ended or still-future-dated) to this same structure. |
| is_primary | boolean | — | Optional | Defaults to false. Forced to true regardless of what you send if this is the user's first active assignment ever — a user can never have an active structure without exactly one being primary, so there's nothing to choose from on the very first one. |
| since | string | — | Optional | Format YYYY-MM-DD. Defaults to today if omitted. |
| until | string | — | Optional | Format YYYY-MM-DD. Open-ended (no scheduled end) if omitted. Must be strictly after since or this is rejected with a 422 — unlike some legacy behavior this migrates, an invalid until is never silently dropped. |
| notes | string | — | Optional | Free text, truncated to 500 characters. |
Example request
curl -X POST "https://api.openstudio.one/v1/users/{id}/memberships" \
-H "X-Api-User-Key: YOUR_USER_KEY" \
-H "Content-Type: application/json" \
-d '{"structure_acronym":"...","is_primary":true,"since":"...","until":"...","notes":"..."}'<?php
$ch = curl_init('https://api.openstudio.one/v1/users/{id}/memberships');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'structure_acronym' => '...',
'is_primary' => true,
'since' => '...',
'until' => '...',
'notes' => '...',
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY', 'Content-Type: application/json']);
$response = curl_exec($ch);
$data = json_decode($response, true);import requests
headers = {
"X-Api-User-Key": "YOUR_USER_KEY",
}
body = {
'structure_acronym': '...',
'is_primary': True,
'since': '...',
'until': '...',
'notes': '...',
}
response = requests.post("https://api.openstudio.one/v1/users/{id}/memberships", headers=headers, json=body)
data = response.json()fetch('https://api.openstudio.one/v1/users/{id}/memberships', {
method: 'POST',
headers: {
'X-Api-User-Key': 'YOUR_USER_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
'structure_acronym': '...',
'is_primary': true,
'since': '...',
'until': '...',
'notes': '...',
})
})
.then(res => res.json())
.then(data => console.log(data));Response fields
| Field | Type | Flags | Notes |
|---|---|---|---|
| id | integer | — | The new assignment's id. |
| structure_acronym | string | — | Echoes structure_acronym. |
| structure_title | string | — | The new structure's display name. |
| level | integer | — | The new structure's level in the hierarchy.1 = Area2 = Department3 = Unit |
| is_primary | boolean | — | The actual value stored — may be true even if you sent false or omitted it, see is_primary's own request-field description. |
| since | string | — | Echoes since (or today's date, if omitted). Format YYYY-MM-DD. |
| until | string | Echoes until, or null if omitted. Format YYYY-MM-DD. |
|
| notes | string | Echoes notes, or null if omitted. |
Example response
{
"success": true,
"data": {
"id": 42,
"structure_acronym": "MKT",
"structure_title": "Marketing",
"level": 2,
"is_primary": false,
"since": "2026-08-21",
"until": null,
"notes": null
},
"error": null
}GET /users/{id}/memberships/history
Get a user's membership change history
User keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The target user's id — same as on GET /users/{id}. |
Query parameters (1)
Other
| Parameter | Type | Required | Notes |
|---|---|---|---|
| ?expand | string | Optional | Set to performed_by to also inline basic details for whoever made each change, instead of just the bare performed_by_personal_code. One extra batch query regardless of how many history rows come back — never a lookup per row. Any other value is rejected with a 422. |
Example request
curl -X GET "https://api.openstudio.one/v1/users/{id}/memberships/history?expand=..." \
-H "X-Api-User-Key: YOUR_USER_KEY"<?php
$ch = curl_init('https://api.openstudio.one/v1/users/{id}/memberships/history?expand=...');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);
$response = curl_exec($ch);
$data = json_decode($response, true);import requests
headers = {
"X-Api-User-Key": "YOUR_USER_KEY",
}
response = requests.get("https://api.openstudio.one/v1/users/{id}/memberships/history?expand=...", headers=headers)
data = response.json()fetch('https://api.openstudio.one/v1/users/{id}/memberships/history?expand=...', {
method: 'GET',
headers: {
'X-Api-User-Key': 'YOUR_USER_KEY'
}
})
.then(res => res.json())
.then(data => console.log(data));ⓘRuns a real request from your browser directly to users/{id}/memberships/history. Keys are only held in this page's memory while you're here — never stored, never sent anywhere else.
Response fields
| Field | Type | Flags | Notes |
|---|---|---|---|
| history | array of objects | — | Every recorded structure-assignment change for this user, newest first. Empty array (not an error) if this account's membership-history feature isn't set up yet. |
| ↳event_type | string | — | What kind of membership event this is.assignmentremovalscheduled_endprimary_change |
| ↳event_label | string | — | Short human-readable summary of what happened. |
| ↳structure_from | string | Meaning depends on event_type — e.g. the structure being removed/ended, or the previous primary on a primary_change. |
|
| ↳structure_to | string | Meaning depends on event_type, mirrors structure_from — e.g. the newly assigned structure, or the new primary. |
|
| ↳performed_by_personal_code | integer | The identifier of whoever made this change. See ?expand=performed_by for a resolved version instead of this bare id. |
|
| ↳performed_by | object | Only present when ?expand=performed_by was requested. null if performed_by_personal_code itself was null, or didn't resolve to a record on this tenant. |
|
| ↳user_id | integer | — | The same user_id used everywhere else in this API — NOT performed_by_personal_code itself, see the "Identifying users" guide. |
| ↳personal_code | integer | — | Same value as performed_by_personal_code on the row itself — included here too so this object is usable on its own without also reading the sibling field. |
| ↳first_name | string | — | The person this event is about — first name. |
| ↳last_name | string | — | The person this event is about — last name. |
| ↳created_at | string | — | When this change was recorded. |
Example response
{
"success": true,
"data": {
"history": [
{
"event_type": "assignment",
"event_label": "Assigned to MKT",
"structure_from": null,
"structure_to": "MKT",
"performed_by_personal_code": 10000001,
"created_at": "2026-08-21 09:00:00"
}
]
},
"error": null
}GET /users/{id}/memberships/roles
Get the structures a user leads, and whether that leadership is formalized
User keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The target user's id — same as on GET /users/{id}. |
Example request
curl -X GET "https://api.openstudio.one/v1/users/{id}/memberships/roles" \
-H "X-Api-User-Key: YOUR_USER_KEY"<?php
$ch = curl_init('https://api.openstudio.one/v1/users/{id}/memberships/roles');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);
$response = curl_exec($ch);
$data = json_decode($response, true);import requests
headers = {
"X-Api-User-Key": "YOUR_USER_KEY",
}
response = requests.get("https://api.openstudio.one/v1/users/{id}/memberships/roles", headers=headers)
data = response.json()fetch('https://api.openstudio.one/v1/users/{id}/memberships/roles', {
method: 'GET',
headers: {
'X-Api-User-Key': 'YOUR_USER_KEY'
}
})
.then(res => res.json())
.then(data => console.log(data));ⓘRuns a real request from your browser directly to users/{id}/memberships/roles. Keys are only held in this page's memory while you're here — never stored, never sent anywhere else.
Response fields
| Field | Type | Flags | Notes |
|---|---|---|---|
| roles | array of objects | — | A completely different concept from memberships above — this is every structure where leader on that structure names this user directly, independent of whether they hold an active membership row for it at all. |
| ↳structure_acronym | string | — | Matches the acronym field on GET /departments. |
| ↳structure_title | string | — | This structure's display name. |
| ↳level | integer | — | This structure's level in the hierarchy.1 = Area2 = Department3 = Unit |
| ↳is_formalized | boolean | — | Whether this leadership has a matching formal role record on file. false just means informal/unrecorded — it does not mean the leadership itself is invalid or pending. |
| ↳is_ad_interim | boolean | — | true if the formal role is recorded as a temporary/interim appointment. Always false when is_formalized is false. |
| ↳role_label | string | Free-text description from the formal role record, if one exists. null when is_formalized is false. |
Example response
{
"success": true,
"data": {
"roles": [
{
"structure_acronym": "MKT",
"structure_title": "Marketing",
"level": 2,
"is_formalized": true,
"is_ad_interim": false,
"role_label": "Assignment with Responsibility Indemnity to the Management and Coordination of the Structure \"MKT \u2014 Marketing\""
}
]
},
"error": null
}PATCH /users/{id}/memberships/{membership_id}
End or schedule the end of a structure assignment
User keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The target user's id — same as on GET /users/{id}. |
| {membership_id} | integer | One assignment's own id, from the id field of an item in GET /users/{id}/memberships's memberships array (or from the response of POST /users/{id}/memberships, right after creating it). |
Request body
| Field | Type | Flags | Required | Notes |
|---|---|---|---|---|
| until | string | Required | Format YYYY-MM-DD, or null to end it today. A date today or in the past ends the assignment immediately — if it was the user's primary, another active assignment is auto-promoted when there's exactly one candidate, or left unset with needs_primary_selection: true in the response when there are several (use POST /users/{id}/memberships/{membership_id}/primary to resolve it). A date in the future just schedules the end — the assignment (and its primary status, if any) stays active until then, nothing is reassigned yet. |
Example request
curl -X PATCH "https://api.openstudio.one/v1/users/{id}/memberships/{membership_id}" \
-H "X-Api-User-Key: YOUR_USER_KEY" \
-H "Content-Type: application/json" \
-d '{"until":"..."}'<?php
$ch = curl_init('https://api.openstudio.one/v1/users/{id}/memberships/{membership_id}');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PATCH');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'until' => '...',
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY', 'Content-Type: application/json']);
$response = curl_exec($ch);
$data = json_decode($response, true);import requests
headers = {
"X-Api-User-Key": "YOUR_USER_KEY",
}
body = {
'until': '...',
}
response = requests.patch("https://api.openstudio.one/v1/users/{id}/memberships/{membership_id}", headers=headers, json=body)
data = response.json()fetch('https://api.openstudio.one/v1/users/{id}/memberships/{membership_id}', {
method: 'PATCH',
headers: {
'X-Api-User-Key': 'YOUR_USER_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
'until': '...',
})
})
.then(res => res.json())
.then(data => console.log(data));Response fields
| Field | Type | Flags | Notes |
|---|---|---|---|
| membership | object | — | Same shape as one item of GET /users/{id}/memberships's memberships array, reflecting the new until value. |
| needs_primary_selection | boolean | — | true only when this ended the user's primary assignment immediately AND more than one other active assignment remains — none of them is auto-promoted in that case. See primary_candidates below. |
| primary_candidates | array of strings | — | Structure codes of the user's other currently-active assignments, only populated when needs_primary_selection is true. |
Example response
{
"success": true,
"data": {
"membership": {
"id": 41,
"structure_acronym": "AFA",
"until": "2026-08-21"
},
"needs_primary_selection": false,
"primary_candidates": []
},
"error": null
}DELETE /users/{id}/memberships/{membership_id}
End a structure assignment immediately
User keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The target user's id — same as on GET /users/{id}. |
| {membership_id} | integer | One assignment's own id, from the id field of an item in GET /users/{id}/memberships's memberships array (or from the response of POST /users/{id}/memberships, right after creating it). |
Example request
curl -X DELETE "https://api.openstudio.one/v1/users/{id}/memberships/{membership_id}" \
-H "X-Api-User-Key: YOUR_USER_KEY"<?php
$ch = curl_init('https://api.openstudio.one/v1/users/{id}/memberships/{membership_id}');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);
$response = curl_exec($ch);
$data = json_decode($response, true);import requests
headers = {
"X-Api-User-Key": "YOUR_USER_KEY",
}
response = requests.delete("https://api.openstudio.one/v1/users/{id}/memberships/{membership_id}", headers=headers)
data = response.json()fetch('https://api.openstudio.one/v1/users/{id}/memberships/{membership_id}', {
method: 'DELETE',
headers: {
'X-Api-User-Key': 'YOUR_USER_KEY'
}
})
.then(res => res.json())
.then(data => console.log(data));Response fields
| Field | Type | Flags | Notes |
|---|---|---|---|
| deleted | boolean | — | Always true on success. Despite the field name (kept for the usual REST delete-response shape), nothing is removed from storage — this is a soft close, identical to PATCH /users/{id}/memberships/{membership_id} with until set to today: the assignment row is kept, just marked ended, so it still appears in GET /users/{id}/memberships/history. Same primary-reassignment behavior applies. |
| needs_primary_selection | boolean | — | Same meaning as on PATCH /users/{id}/memberships/{membership_id}. |
| primary_candidates | array of strings | — | Same meaning as on PATCH /users/{id}/memberships/{membership_id}. |
Example response
{
"success": true,
"data": {
"deleted": true,
"needs_primary_selection": false,
"primary_candidates": []
},
"error": null
}POST /users/{id}/memberships/{membership_id}/primary
Switch a user's primary structure assignment
User keyPath parameters
| Parameter | Type | Notes |
|---|---|---|
| {id} | integer | The target user's id — same as on GET /users/{id}. |
| {membership_id} | integer | The assignment to switch to primary — same id as on PATCH /users/{id}/memberships/{membership_id}. Must currently be active (not already ended) or this is rejected with a 404. |
Example request
curl -X POST "https://api.openstudio.one/v1/users/{id}/memberships/{membership_id}/primary" \
-H "X-Api-User-Key: YOUR_USER_KEY"<?php
$ch = curl_init('https://api.openstudio.one/v1/users/{id}/memberships/{membership_id}/primary');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);
$response = curl_exec($ch);
$data = json_decode($response, true);import requests
headers = {
"X-Api-User-Key": "YOUR_USER_KEY",
}
response = requests.post("https://api.openstudio.one/v1/users/{id}/memberships/{membership_id}/primary", headers=headers)
data = response.json()fetch('https://api.openstudio.one/v1/users/{id}/memberships/{membership_id}/primary', {
method: 'POST',
headers: {
'X-Api-User-Key': 'YOUR_USER_KEY'
}
})
.then(res => res.json())
.then(data => console.log(data));Response fields
| Field | Type | Flags | Notes |
|---|---|---|---|
| id | integer | — | Echoes {membership_id} from the URL. |
| structure_acronym | string | — | The membership's own structure, by acronym. |
| structure_title | string | — | That structure's display name. |
| level | integer | — | That structure's level in the hierarchy.1 = Area2 = Department3 = Unit |
| is_primary | boolean | — | Always true on success. |
| since | string | — | When this assignment started. Format YYYY-MM-DD. A small number of rows migrated from before this API existed may show a full YYYY-MM-DD HH:MM:SS timestamp instead — returned exactly as stored, never reformatted. |
| until | string | null = open-ended. Format YYYY-MM-DD when set. |
|
| notes | string | Free-text notes on why this is now the primary structure, if any were sent. |
Example response
{
"success": true,
"data": {
"id": 41,
"structure_acronym": "AFA",
"structure_title": "Administration Finance & Accounting",
"level": 2,
"is_primary": true,
"since": "2019-08-19",
"until": null,
"notes": null
},
"error": null
}Groups
GET /groups
List the tenant's selectable authorization groups
User keyExample request
curl -X GET "https://api.openstudio.one/v1/groups" \
-H "X-Api-User-Key: YOUR_USER_KEY"<?php
$ch = curl_init('https://api.openstudio.one/v1/groups');
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Api-User-Key: YOUR_USER_KEY']);
$response = curl_exec($ch);
$data = json_decode($response, true);import requests
headers = {
"X-Api-User-Key": "YOUR_USER_KEY",
}
response = requests.get("https://api.openstudio.one/v1/groups", headers=headers)
data = response.json()fetch('https://api.openstudio.one/v1/groups', {
method: 'GET',
headers: {
'X-Api-User-Key': 'YOUR_USER_KEY'
}
})
.then(res => res.json())
.then(data => console.log(data));ⓘRuns a real request from your browser directly to groups. Keys are only held in this page's memory while you're here — never stored, never sent anywhere else.
Response fields
| Field | Type | Flags | Notes |
|---|---|---|---|
| groups | array of objects | — | Every group visible to this caller's tenant, platform-wide groups included. |
| ↳code | string | — | The value to send as a rule's groups:<code> value. |
| ↳name | string | — | Human-readable group name. |
| ↳is_global | boolean | — | Whether this group is a platform-wide group available to every tenant, rather than one scoped to this tenant specifically — real feedback asked for this distinction to show in the picker, not just a bare code/name pair. |
Example response
{
"success": true,
"data": {
"groups": [
{
"code": "OS_RAS",
"name": "OpenStudio Remote Assistance",
"is_global": true
},
{
"code": "OS_BETA-TESTERS",
"name": "Beta Testers",
"is_global": false
}
]
},
"error": null
}