
# Add a user to an organization

`POST /v1/organizations/{org_id}/memberships` · permission `organizations:write`

Adds an existing user to the organization directly (source `admin_api`, status `active`). Without `role_ids` the organization's default member role is assigned; each role must be an environment role or a role of this organization. The response carries an `ETag`; the write is audited as `organization_membership.created`. `Idempotency-Key` is optional.

## Request

| Field | Type | Required | Description |
|---|---|---|---|
| `org_id` | path | yes | Organization ID (`org_…`). |
| `user_id` | string | yes | User ID (`usr_…`). |
| `role_ids` | string[] | no | Role IDs (`rol_…`), at most 50. |
| `expires_at` | string | no | RFC 3339 time in the future when the membership ends. |

## Response

`201 Created` with the membership.

```json
{
  "id": "mem_01j9xn5p9s0t4v6w8x0y2z4a6c",
  "organization_id": "org_01j9xk3m7q8r2s4t6v8w0y2z4a",
  "user_id": "usr_01j9xh2k6p7q1r3s5t7v9w1x3y",
  "status": "active",
  "source": "admin_api",
  "reason": null,
  "roles": [
    {
      "role_id": "rol_01j9xr7s1t2w6x8y0z2a4b6c8e",
      "source": "admin_api",
      "assigned_at": "2026-10-12T09:31:00.123456Z"
    }
  ],
  "expires_at": null,
  "created_at": "2026-10-12T09:31:00.123456Z",
  "activated_at": "2026-10-12T09:31:00.123456Z",
  "suspended_at": null,
  "removed_at": null,
  "updated_at": "2026-10-12T09:31:00.123456Z",
  "version": 1
}
```

## Errors

| HTTP | code | When |
|---|---|---|
| 404 | `not_found` | The organization or user does not exist in this environment. |
| 403 | `role_not_assignable` | A role is not an environment role or a role of this organization. |
| 403 | `user_not_active` | The user is not active. |
| 409 | `membership_already_exists` | The user is already a member of the organization. |
| 409 | `organization_disabled` | The organization is disabled. |
| 409 | `organization_membership_limit_reached` | The organization's membership limit is reached. |
| 409 | `membership_limit_reached` | The user's membership limit is reached. |
| 422 | `anonymous_user_not_allowed` | The user is anonymous. |
| 422 | `validation_failed` | A field is missing or invalid. |
| 401 | `unauthenticated` | The credential is missing, invalid or revoked. |
| 403 | `insufficient_permissions` | The credential lacks the required permission. Assigning a privileged (`is_privileged`) role also requires `roles:write`; without it nothing is written and `required_permissions` lists both permissions. |
| 429 | `rate_limited` | The rate limit was exceeded; retry after the `Retry-After` period. |
| 503 | `service_unavailable` | A dependency is temporarily unavailable; retry after the `Retry-After` period. |

## Example

```bash
curl -i -X POST "https://api.eu.keenauth.com/v1/organizations/org_01j9xk3m7q8r2s4t6v8w0y2z4a/memberships" \
  -H "Authorization: Bearer $KEENAUTH_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"user_id":"usr_01j9xh2k6p7q1r3s5t7v9w1x3y","role_ids":["rol_01j9xr7s1t2w6x8y0z2a4b6c8e"]}'
```

## Personal data

This endpoint carries personal data: organization memberships and invitations. [Personal data in the KeenAuth APIs](/docs/policies/personal-data) describes what is stored, how long it is kept and how it is deleted. Do not send special categories of personal data (health, biometric data, religion, political opinions, sexual orientation) in any field.
