---
title: "Grant Membership"
method: POST
path: "/users/{user_id}/memberships"
tags: ["users"]
---

# Grant Membership

`POST /users/{user_id}/memberships`

## Request

- Base URL: `https://app.zeno-works.ch` (the document declares no server; this is the origin it was published from)
- URL: `https://app.zeno-works.ch/users/{user_id}/memberships`
- Auth: none declared

Add one company membership to a user, or change its role (admin).

The additive counterpart to ``PUT /users/{id}``, which replaces a user's **whole**
membership set. That was the only way to grant one tenant: read the user, add an entry,
send the entire desired list back. Two operators editing one person concurrently then
silently drop each other's grant — no conflict, no error, nothing in the response to
notice — and ``companies: []`` deletes every membership, so a client that mis-computes
the set removes all of somebody's access on a request meant to add one.

Idempotent on the role: granting a role the user already holds changes nothing, and
``granted_by`` is re-stamped only when the role actually changes, so re-sending a grant
does not re-attribute the original decision to whoever sent it last.

404 for an unknown user; **422 for an unknown company**, checked before the row is
written — the membership FK would otherwise raise a bare IntegrityError and answer 500
for a typo. (422 rather than 404 because the company arrives in the BODY, matching what
``PUT /users/{id}`` already answers for the same mistake.)

## Path parameters

- `user_id` string, required

## Headers

- `authorization` string, nullable

## Cookies

- `access_token` string, nullable

## Request body

- MembershipIn: One membership grant: company id + role. The body of ``POST /users/{id}/memberships`` as well as an element of ``UserCreate``/``UserUpdate``'s ``memberships`` array. Deliberately one model and not two identical ones — a second copy generates a duplicate TS type pair that compiles interchangeably only by structural luck, and diverges the first time somebody edits one of them.
  - `company_id` string, required
  - `role` 'owner' | 'accountant' | 'member'

## Response `200`

Successful Response

- UserResponse: ``GET/POST/PUT /users/...`` → ``{"user": ...}``.
  - `user` UserOut, required: Serialized user (no secrets). ``companies`` is an array of company ids; ``memberships`` adds the per-membership role.
    - `id` string, required
    - `first_name` string, required
    - `last_name` string, required
    - `email` string, email, required
    - `phone` string, nullable
    - `is_admin` boolean, required
    - `is_active` boolean, required
    - `ui_language` string, nullable
    - `companies` string[]
    - `memberships` MembershipOut[]
      - `company_id` string, required
      - `role` 'owner' | 'accountant' | 'member', required
  - `invite_mail_attempted` boolean, nullable

## Other responses

- `422`: Validation Error

---

[API](https://skmtc.dev/zeno-works/apis/zeno-api.md) · [All operations](https://skmtc.dev/zeno-works/apis/zeno-api/llms.txt) · [OpenAPI document](https://skmtc.dev/zeno-works/apis/zeno-api/revisions/1eb93dde146c?raw)
