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 key

Example 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

FieldTypeFlagsNotes
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 = Active
0 = 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 1
inactive = status_code 0
pending_activation = status_code -1
disabled = status_code -3 or -4
reported = 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 default
default = Default tone
friendly = Friendly
motivational = 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 email
otp_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 = Area
2 = Department
3 = 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 key

Example 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

FieldTypeFlagsNotes
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 Paginated
Query parameters (2)

Pagination

ParameterTypeRequiredNotes
?pageintegerOptionalPage number, starting at 1. Defaults to 1.
?limitintegerOptionalRows 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

FieldTypeFlagsNotes
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

FieldTypeNotes
pageintegerCurrent page number.
limitintegerRows per page.
totalintegerTotal matching rows across all pages.
total_pagesintegerTotal number of pages.
next_page_urlstringReady-to-call URL for the next page, same filters applied. Null on the last page.
prev_page_urlstringReady-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 key

Request body

FieldTypeFlagsRequiredNotes
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

FieldTypeFlagsNotes
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 key

Example 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

FieldTypeFlagsNotes

Example response

{
    "success": true,
    "data": {},
    "error": null
}

Address book

GET /contact-points/{userId}

List one person's contact points (phone/email records)

User key

Example 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

FieldTypeFlagsNotes
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 key

Request body

FieldTypeFlagsRequiredNotes
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

FieldTypeFlagsNotes
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 key

Request body

FieldTypeFlagsRequiredNotes
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

FieldTypeFlagsNotes
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 key

Example 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

FieldTypeFlagsNotes
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 key
Query parameters (3)

Other

ParameterTypeRequiredNotes
?q_personal_codeintegerOptionalExact match.
?q_taxcodestringOptionalSubstring match.
?q_surnamestringOptionalSubstring 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

FieldTypeFlagsNotes
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 key

Example 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

FieldTypeFlagsNotes
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 Paginated
Query parameters (12)

Pagination

ParameterTypeRequiredNotes
?pageintegerOptionalPage number, starting at 1. Defaults to 1.
?limitintegerOptionalRows per page, 1-200. Defaults to 50.

Other

ParameterTypeRequiredNotes
?q_namestringOptionalSubstring match on first name.
?q_surnamestringOptionalSubstring match on last name.
?q_taxcodestringOptionalSubstring match on tax code.
?q_personal_codeintegerOptionalExact match on personal_code.
?q_usernamestringOptionalExact match on username. No match -> zero results, same as any other filter here (never silently ignored).
?q_structurestringOptionalA 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_leadersbooleanOptionalOnly 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_subbooleanOptionalOnly 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).
?unassignedbooleanOptionalLists 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).
?expandstringOptionalSet 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

FieldTypeFlagsNotes
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 = Active
0 = 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).
active
inactive
pending_activation
disabled
reported
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

FieldTypeNotes
pageintegerCurrent page number.
limitintegerRows per page.
totalintegerTotal matching rows across all pages.
total_pagesintegerTotal number of pages.
next_page_urlstringReady-to-call URL for the next page, same filters applied. Null on the last page.
prev_page_urlstringReady-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 key

Path parameters

ParameterTypeNotes
{id}integerThe 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

ParameterTypeRequiredNotes
?includestringOptionalComma-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

FieldTypeFlagsNotes
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 key

Path parameters

ParameterTypeNotes
{id}integerThe target user's id — same as on GET /users/{id}.

Request body

FieldTypeFlagsRequiredNotes
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.
M
F
O
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

FieldTypeFlagsNotes
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.
recognized
recognized_paper
not_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 key

Path parameters

ParameterTypeNotes
{id}integerThe target user's id — same as on GET /users/{id}.

Request body

FieldTypeFlagsRequiredNotes
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.
manual
document
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

FieldTypeFlagsNotes
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.
recognized
recognized_paper
not_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 key

Path parameters

ParameterTypeNotes
{id}integerThe target user's id — same as on GET /users/{id}.

Request body

FieldTypeFlagsRequiredNotes
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

FieldTypeFlagsNotes
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 key

Path parameters

ParameterTypeNotes
{id}integerThe target user's id — same as on GET /users/{id}.

Request body

FieldTypeFlagsRequiredNotes
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

FieldTypeFlagsNotes
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 key

Path parameters

ParameterTypeNotes
{id}integerThe target user's id — same as on GET /users/{id}.
Query parameters (1)

Other

ParameterTypeRequiredNotes
?expandstringOptionalSet 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

FieldTypeFlagsNotes
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 key

Path parameters

ParameterTypeNotes
{id}integerThe target user's id — same as on GET /users/{id}.

Request body

FieldTypeFlagsRequiredNotes
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

FieldTypeFlagsNotes
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 key

Path parameters

ParameterTypeNotes
{id}integerThe 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

FieldTypeFlagsNotes
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 key

Path parameters

ParameterTypeNotes
{id}integerThe 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

FieldTypeFlagsNotes
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 key

Path parameters

ParameterTypeNotes
{id}integerThe 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

FieldTypeFlagsNotes
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 key

Path parameters

ParameterTypeNotes
{id}integerThe 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

FieldTypeFlagsNotes
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 = Unvalidated
1 = File validated
2 = Original validated in person
3 = Fully validated (both)
status_label string Lowercase name of status.
unvalidated
file_validated
original_validated
fully_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 key

Path parameters

ParameterTypeNotes
{id}integerThe target user's id — same as on GET /users/{id}.

Request body

FieldTypeFlagsRequiredNotes
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

FieldTypeFlagsNotes
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 key

Path parameters

ParameterTypeNotes
{id}integerThe target user's id — same as on GET /users/{id}.
{document_id}integerOne 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

FieldTypeFlagsNotes
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 key

Path parameters

ParameterTypeNotes
{id}integerThe target user's id — same as on GET /users/{id}.
{document_id}integerSame id as on DELETE /users/{id}/documents/{document_id}.

Request body

FieldTypeFlagsRequiredNotes
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

FieldTypeFlagsNotes
id integer See GET /users/{id}/documents.
status integer Reflects level from this request.
1 = File validated
2 = Original validated in person
3 = Fully validated (both)
status_label string Lowercase name of status.
file_validated
original_validated
fully_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 key

Path parameters

ParameterTypeNotes
{id}integerThe target user's id — same as on GET /users/{id}.
{document_id}integerSame 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

FieldTypeFlagsNotes

Example response

{
    "success": true,
    "data": {},
    "error": null
}

Memberships

GET /users/{id}/memberships

List a user's active organizational structure assignments

User key

Path parameters

ParameterTypeNotes
{id}integerThe 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

FieldTypeFlagsNotes
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 = Area
2 = Department
3 = 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 key

Path parameters

ParameterTypeNotes
{id}integerThe target user's id — same as on GET /users/{id}.

Request body

FieldTypeFlagsRequiredNotes
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

FieldTypeFlagsNotes
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 = Area
2 = Department
3 = 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 key

Path parameters

ParameterTypeNotes
{id}integerThe target user's id — same as on GET /users/{id}.
Query parameters (1)

Other

ParameterTypeRequiredNotes
?expandstringOptionalSet 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

FieldTypeFlagsNotes
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.
assignment
removal
scheduled_end
primary_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 key

Path parameters

ParameterTypeNotes
{id}integerThe 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

FieldTypeFlagsNotes
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 = Area
2 = Department
3 = 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 key

Path parameters

ParameterTypeNotes
{id}integerThe target user's id — same as on GET /users/{id}.
{membership_id}integerOne 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

FieldTypeFlagsRequiredNotes
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

FieldTypeFlagsNotes
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 key

Path parameters

ParameterTypeNotes
{id}integerThe target user's id — same as on GET /users/{id}.
{membership_id}integerOne 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

FieldTypeFlagsNotes
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 key

Path parameters

ParameterTypeNotes
{id}integerThe target user's id — same as on GET /users/{id}.
{membership_id}integerThe 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

FieldTypeFlagsNotes
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 = Area
2 = Department
3 = 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 key

Example 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

FieldTypeFlagsNotes
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
}
ESC