Users
Endpoints under the Users tag manage user accounts in organization context. Most routes require X-Org (see Organization context & pagination). Response bodies follow UserResponse, CreateUserRequest, UpdateUserRequest, AddRolesRequest, DeleteRolesRequest, and PaginatedResponse where applicable — see the API reference.
Related: Listing members inside an organization tree uses GET /orgs/{org_id}/users (list_org_users) under the Orgs tag — see Organizations.
Operations vs wire routes
Section titled “Operations vs wire routes”| Concern | HTTP | operationId | Notes |
|---|---|---|---|
| Create | POST /users | create_user | Header X-Org required; body CreateUserRequest (email and localized name fields per bundle). Query flags include_deleted, only_deleted, include_metadata. |
| Search | GET /users/search | search_users | Query q, org_id, scope (all searches globally — needs users:list on root org), limit, offset, exclude_ids. Returns PaginatedResponse. |
| Get | GET /users/{id} | get_user | Header X-Org; optional include_deleted, include_metadata. |
| Update | PUT /users/{id} | update_user | Body UpdateUserRequest. |
| Soft delete | DELETE /users/{id} | delete_user | |
| Hard delete | DELETE /users/{id}/purge | hard_delete_user | Irreversible purge. |
| Add roles | POST /users/{id}/roles | add_roles | Body AddRolesRequest (roles). |
| Remove roles | DELETE /users/{id}/roles | delete_roles | Body DeleteRolesRequest. |
Avatar uploads (POST /users/{id}/image, multipart) may be exposed by your deployment; the Python SDK includes upload_image / upload_image_from_url helpers when that route is available.
SDK coverage
Section titled “SDK coverage”| Capability | Python | Rust (openapp_sdk) | Go | TypeScript (AsyncClient) |
|---|---|---|---|---|
| Create / get / update / delete / purge | client.users.* | client.users() — create, get, update, delete, purge | UsersAPI (full) | Not on façade — extend via core transport or use another SDK |
| Search | search — passes q and optional filters; wire pagination uses limit / offset per OpenAPI (Python also accepts cursor as an extra query key — prefer offset alignment with the bundle) | search(query) — sends q only; use transport() + RequestSpec for scope, offset, etc. | SearchUsers | Not on façade |
| Roles | add_roles, remove_roles | add_roles, remove_roles | AddRoles, DeleteRoles | Not on façade |
| Avatar | upload_image, upload_image_from_url (when route exists) | Use transport or REST | Generated client if present in your bundle | Not on façade |
Typical errors
Section titled “Typical errors”401 / 403 when the caller lacks users:* permissions or X-Org context.404 for unknown user ids.409 on create/delete conflicts.422 on validation.400 on bad search parameters. Bodies follow ApiErrorResponse — see Errors & retries.
Examples
Section titled “Examples”Search users
Section titled “Search users”page = await client.users.search( "alice", org_id="org_123", limit=20,)resp, httpResp, err := client.UsersAPI.SearchUsers(ctx). Q("alice"). OrgId("org_123"). Limit(20). Execute()if err != nil { return err}defer httpResp.Body.Close()_ = respuse openapp_sdk::Client;
let client = Client::builder() .api_key("https://api.openapp.house/api/v1_openapp_YOUR_SECRET") .build()?;
let hits = client.users().search("alice").await?;For scope, offset, or exclude_ids, build a RequestSpec on client.transport() (see core/ARCHITECTURE.md in the SDK repo).
const url = new URL("https://api.openapp.house/api/v1/users/search");url.searchParams.set("q", "alice");url.searchParams.set("org_id", "org_123");url.searchParams.set("limit", "20");url.searchParams.set("offset", "0");
const page = await fetch(url, { headers: { authorization: "Bearer v1_openapp_YOUR_SECRET" },}).then( (r) => r.json() as Promise<{ items: Array<{ id: string }>; total: number }>,);/users/search carries scope on the query (org_id is the default; pass scope=all with users:list on the root org for cross-org search). Pagination is limit / offset — there is no cursor field. Replace with AsyncClient.searchUsers once the Node façade exposes it.
Create a user
Section titled “Create a user”user = await client.users.create( email="new.user@example.com", org_id="org_123", roles=["users:read"],)body := openapiclient.NewCreateUserRequest( "new.user@example.com", *openapiclient.NewLocalizedString(map[string]string{"en": "New User"}),)resp, httpResp, err := client.UsersAPI.CreateUser(ctx). XOrg("org_123"). CreateUserRequest(*body). Execute()if err != nil { return err}defer httpResp.Body.Close()_ = respuse openapp_sdk::Client;use serde_json::json;
let client = Client::builder() .api_key("https://api.openapp.house/api/v1_openapp_YOUR_SECRET") .build()?;
let user = client .users() .create(&json!({ "email": "new.user@example.com", "name": { "value": { "en": "New User" } } })) .await?;POST /users requires X-Org on the wire — supply org context the same way as other org-scoped routes (Organization context & pagination).
const orgId = "org_123";const user = await fetch("https://api.openapp.house/api/v1/users", { method: "POST", headers: { authorization: "Bearer v1_openapp_YOUR_SECRET", "content-type": "application/json", "x-org": orgId, }, body: JSON.stringify({ email: "new.user@example.com", name: { value: { en: "New User" } }, }),}).then((r) => r.json() as Promise<{ id: string; email: string }>);Body matches CreateUserRequest — name is wrapped in LocalizedString ({ value: { en: "…" } }). X-Org scopes the new account; pass org_id in the body only if your deployment forks creation by org. Replace with AsyncClient.createUser once the Node façade exposes it.
Get / update / delete / purge (/users/{id})
Section titled “Get / update / delete / purge (/users/{id})”update_user replaces the row with UpdateUserRequest (optional email, name as LocalizedString). delete_user is a soft delete; hard_delete_user is irrecoverable. All four require X-Org.
user = await client.users.get(user_id)updated = await client.users.update(user_id, name={"value": {"en": "Renamed"}})await client.users.delete(user_id)await client.users.purge(user_id)import ( "context"
openapiclient "github.com/tomers/openapp-sdk/go")
user, httpResp, err := client.UsersAPI.GetUser(context.Background(), userID). XOrg(orgID). Execute()if err != nil { return err}defer httpResp.Body.Close()_ = user
upd := openapiclient.NewUpdateUserRequest()upd.SetName(*openapiclient.NewLocalizedString(map[string]string{"en": "Renamed"}))updated, httpResp2, err := client.UsersAPI.UpdateUser(context.Background(), userID). XOrg(orgID). UpdateUserRequest(*upd). Execute()if err != nil { return err}defer httpResp2.Body.Close()_ = updated
_, httpResp3, err := client.UsersAPI.DeleteUser(context.Background(), userID). XOrg(orgID). Execute()if err != nil { return err}defer httpResp3.Body.Close()
_, httpResp4, err := client.UsersAPI.HardDeleteUser(context.Background(), userID). XOrg(orgID). Execute()if err != nil { return err}defer httpResp4.Body.Close()use openapp_sdk::Client;use serde_json::json;
let client = Client::builder() .api_key("https://api.openapp.house/api/v1_openapp_YOUR_SECRET") .build()?;
let user = client.users().get(user_id).await?;let updated = client .users() .update(user_id, &json!({ "name": { "value": { "en": "Renamed" } } })) .await?;client.users().delete(user_id).await?;client.users().purge(user_id).await?;const orgId = "org_123";const userUrl = `https://api.openapp.house/api/v1/users/${userId}`;const baseHeaders = { authorization: "Bearer v1_openapp_YOUR_SECRET", "x-org": orgId,};
const user = await fetch(userUrl, { headers: baseHeaders }).then((r) => r.json(),);
const updated = await fetch(userUrl, { method: "PUT", headers: { ...baseHeaders, "content-type": "application/json" }, body: JSON.stringify({ name: { value: { en: "Renamed" } } }),}).then((r) => r.json());
await fetch(userUrl, { method: "DELETE", headers: baseHeaders });await fetch(`${userUrl}/purge`, { method: "DELETE", headers: baseHeaders });PUT replaces the row using UpdateUserRequest (no merge); soft delete is recoverable until /purge is called. All four routes require X-Org. Replace each fetch with AsyncClient lifecycle helpers once the Node façade exposes them.
Add and remove roles (POST / DELETE /users/{id}/roles)
Section titled “Add and remove roles (POST / DELETE /users/{id}/roles)”Wire body AddRolesRequest / DeleteRolesRequest is a map of org id → role list (e.g. { "org_123": ["users:read"] }), letting one call grant or revoke roles across multiple orgs in a single request. Both routes require users:create (or admin) in each affected org.
await client.users.add_roles(user_id, ["users:read"])await client.users.remove_roles(user_id, ["users:read"])The Python helper sends a flattened {"roles": [...]} shape; for full OpenAPI parity (per-org grants in one call) issue POST|DELETE /users/{id}/roles through AsyncClient._request with the { "<org_id>": [...] } map directly.
roles := map[string][]string{ "org_123": {"users:read"},}added, httpResp, err := client.UsersAPI.AddRoles(ctx, userID). XOrg("org_123"). RequestBody(roles). Execute()if err != nil { return err}defer httpResp.Body.Close()_ = added
removed, httpResp2, err := client.UsersAPI.DeleteRoles(ctx, userID). XOrg("org_123"). RequestBody(roles). Execute()if err != nil { return err}defer httpResp2.Body.Close()_ = removeduse openapp_sdk::Client;use serde_json::json;
let client = Client::builder() .api_key("https://api.openapp.house/api/v1_openapp_YOUR_SECRET") .build()?;
let body = json!({ "org_123": ["users:read"] });client.users().add_roles(user_id, &body).await?;client.users().remove_roles(user_id, &body).await?;const orgId = "org_123";const rolesUrl = `https://api.openapp.house/api/v1/users/${userId}/roles`;const headers = { authorization: "Bearer v1_openapp_YOUR_SECRET", "content-type": "application/json", "x-org": orgId,};const grants = { [orgId]: ["users:read"] };
await fetch(rolesUrl, { method: "POST", headers, body: JSON.stringify(grants),});
await fetch(rolesUrl, { method: "DELETE", headers, body: JSON.stringify(grants),});The wire body is an org id → role list map ({ "<org_id>": ["users:read"] }), so a single call can grant or revoke roles in many orgs at once — X-Org still pins the calling principal’s permission check. Replace with AsyncClient.addRoles / removeRoles once the Node façade exposes them.