> ## Documentation Index
> Fetch the complete documentation index at: https://docs.safeclose.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Workspace actions: orgs, roles, members, billing

> POST /v1/workspace/actions — create and configure organizations, manage locations, roles, members, and billing through a single polymorphic action endpoint.

The workspace actions endpoint is a single `POST` route that accepts a discriminated action body. Every mutating operation on organizations — creating one, updating its settings or branding, managing locations, roles, members, billing, and even deleting the org — is dispatched through this one endpoint. The `action` field in the request body determines which operation runs and which additional fields are required.

All requests require a valid Bearer token. Most actions that target an existing organization also require your role to have the corresponding permission. See [Org permission model](#org-permission-model) below.

***

## Workspace actions endpoint

```http theme={null}
POST /v1/workspace/actions
```

### Request headers

<ParamField header="Authorization" type="string" required>
  Bearer token from your Clerk session. Example: `Bearer eyJhbGc...`
</ParamField>

<ParamField header="Content-Type" type="string" required>
  Must be `application/json`.
</ParamField>

### Request body

<ParamField body="action" type="string" required>
  The action to perform. Must be one of the action type strings listed in the sections below.
</ParamField>

Additional fields depend on the `action` value. Each action is documented in its own section below.

### Response

On success, returns `{ "ok": true }` plus any action-specific fields (e.g., `createdOrgId`).

On failure, returns HTTP `400` with an error body:

```json theme={null}
{ "error": "Human-readable error message." }
```

Validation failures also include a structured `issues` field:

```json theme={null}
{
  "error": "Invalid body.",
  "issues": { ... }
}
```

***

## Create organization

**action:** `createOrganization`

Creates a new organization and sets it as the caller's active organization. The caller is automatically assigned the built-in `owner` role and a free-tier billing account is provisioned.

### Body fields

<ParamField body="action" type="string" required>
  `"createOrganization"`
</ParamField>

<ParamField body="name" type="string" required>
  Organization display name. 1–120 characters.
</ParamField>

<ParamField body="slug" type="string">
  URL-safe slug for the organization. Lowercase alphanumeric with hyphens; 2–48 characters; must start and end with an alphanumeric character. If omitted, a slug is auto-generated from `name`. Must be unique across all organizations.
</ParamField>

### Response fields

<ResponseField name="ok" type="boolean" required>
  `true` on success.
</ResponseField>

<ResponseField name="createdOrgId" type="string" required>
  ID of the newly created organization.
</ResponseField>

### Example

<CodeGroup>
  ```bash Request theme={null}
  curl --request POST \
    --url https://your-api.example.com/v1/workspace/actions \
    --header 'Authorization: Bearer <your-session-token>' \
    --header 'Content-Type: application/json' \
    --data '{
      "action": "createOrganization",
      "name": "Acme Auto Finance",
      "slug": "acme-auto-finance"
    }'
  ```

  ```json Response theme={null}
  {
    "ok": true,
    "createdOrgId": "org_01j9z8aabbccdd"
  }
  ```
</CodeGroup>

***

## Set active organization

**action:** `setActiveOrganization`

Sets the active organization for your user session. You must already be an active member of the target organization. The `touchActiveOrganizationFromNavigation` action is an alias with identical behavior, used internally by the navigation shell.

### Body fields

<ParamField body="action" type="string" required>
  `"setActiveOrganization"` or `"touchActiveOrganizationFromNavigation"`
</ParamField>

<ParamField body="orgId" type="string" required>
  ID of the organization to set as active. Must be an org you are an active member of.
</ParamField>

### Example

<CodeGroup>
  ```bash Request theme={null}
  curl --request POST \
    --url https://your-api.example.com/v1/workspace/actions \
    --header 'Authorization: Bearer <your-session-token>' \
    --header 'Content-Type: application/json' \
    --data '{
      "action": "setActiveOrganization",
      "orgId": "org_01j9z8aabbccdd"
    }'
  ```

  ```json Response theme={null}
  { "ok": true }
  ```
</CodeGroup>

***

## Update organization settings

**action:** `updateOrganizationSettings`

Updates the display name and slug for an existing organization. Requires the `orgSettings` permission.

<Warning>
  Changing the slug affects any external links or integrations that reference the organization by slug. Confirm downstream dependencies before updating.
</Warning>

### Body fields

<ParamField body="action" type="string" required>
  `"updateOrganizationSettings"`
</ParamField>

<ParamField body="orgId" type="string" required>
  ID of the organization to update.
</ParamField>

<ParamField body="name" type="string" required>
  New display name. 1–120 characters.
</ParamField>

<ParamField body="slug" type="string" required>
  New slug. Lowercase alphanumeric with hyphens; 2–48 characters; must start and end with an alphanumeric character. Must be unique across all organizations.
</ParamField>

***

## Update organization branding

**action:** `updateOrganizationBrand`

Updates the custom brand name, logo URL, and primary color for an organization. Requires the `brand` permission. All fields are optional; pass `null` to clear a value.

### Body fields

<ParamField body="action" type="string" required>
  `"updateOrganizationBrand"`
</ParamField>

<ParamField body="orgId" type="string" required>
  ID of the organization to update.
</ParamField>

<ParamField body="brandName" type="string or null">
  Custom brand display name. Maximum 120 characters. Pass `null` to clear.
</ParamField>

<ParamField body="brandLogoUrl" type="string or null">
  URL to the organization logo. Maximum 2000 characters. Pass `null` to clear.
</ParamField>

<ParamField body="brandPrimaryHex" type="string or null">
  Primary brand color as a `#RRGGBB` hex string (e.g. `"#1A56DB"`). Maximum 7 characters. Pass `null` to clear.
</ParamField>

***

## Create location

**action:** `createOrgLocation`

Adds a new location to the organization. Locations are appended at the end of the existing sort order. Requires the `locations` permission.

### Body fields

<ParamField body="action" type="string" required>
  `"createOrgLocation"`
</ParamField>

<ParamField body="orgId" type="string" required>
  ID of the organization.
</ParamField>

<ParamField body="name" type="string" required>
  Location display name. 1–120 characters.
</ParamField>

<ParamField body="addressLine1" type="string or null">
  Street address. Maximum 200 characters.
</ParamField>

<ParamField body="city" type="string or null">
  City. Maximum 80 characters.
</ParamField>

<ParamField body="region" type="string or null">
  State, province, or region. Maximum 80 characters.
</ParamField>

<ParamField body="postalCode" type="string or null">
  Postal code. Maximum 20 characters.
</ParamField>

<ParamField body="countryCode" type="string or null">
  ISO 3166-1 alpha-2 country code (e.g. `"US"`). Stored in uppercase. Maximum 2 characters.
</ParamField>

<ParamField body="phone" type="string or null">
  Phone number. Maximum 40 characters.
</ParamField>

***

## Update location

**action:** `updateOrgLocation`

Updates an existing location. Requires the `locations` permission.

### Body fields

<ParamField body="action" type="string" required>
  `"updateOrgLocation"`
</ParamField>

<ParamField body="orgId" type="string" required>
  ID of the organization.
</ParamField>

<ParamField body="locationId" type="string" required>
  ID of the location to update. Must belong to the specified organization.
</ParamField>

<ParamField body="name" type="string" required>
  Updated location name. 1–120 characters.
</ParamField>

<ParamField body="addressLine1" type="string or null">
  Updated street address. Maximum 200 characters.
</ParamField>

<ParamField body="city" type="string or null">
  Updated city. Maximum 80 characters.
</ParamField>

<ParamField body="region" type="string or null">
  Updated region. Maximum 80 characters.
</ParamField>

<ParamField body="postalCode" type="string or null">
  Updated postal code. Maximum 20 characters.
</ParamField>

<ParamField body="countryCode" type="string or null">
  Updated country code. Stored in uppercase. Maximum 2 characters.
</ParamField>

<ParamField body="phone" type="string or null">
  Updated phone number. Maximum 40 characters.
</ParamField>

***

## Delete location

**action:** `deleteOrgLocation`

Permanently removes a location from the organization. Requires the `locations` permission.

### Body fields

<ParamField body="action" type="string" required>
  `"deleteOrgLocation"`
</ParamField>

<ParamField body="orgId" type="string" required>
  ID of the organization.
</ParamField>

<ParamField body="locationId" type="string" required>
  ID of the location to delete. Must belong to the specified organization.
</ParamField>

***

## Create role

**action:** `createOrgRole`

Creates a new custom role for the organization with a defined permission set. Requires the `roles` permission. The slug `"owner"` is reserved and cannot be used for custom roles.

### Body fields

<ParamField body="action" type="string" required>
  `"createOrgRole"`
</ParamField>

<ParamField body="orgId" type="string" required>
  ID of the organization.
</ParamField>

<ParamField body="name" type="string" required>
  Role display name. 1–80 characters.
</ParamField>

<ParamField body="slug" type="string">
  URL-safe slug for the role. Lowercase alphanumeric with hyphens; 2–32 characters; must start and end with alphanumeric. If omitted, auto-generated from `name`. Cannot be `"owner"`.
</ParamField>

<ParamField body="permissions" type="object" required>
  An object mapping permission keys to boolean values. Any key not included defaults to `false`. See [Org permission model](#org-permission-model) for the full list of keys.
</ParamField>

### Example

<CodeGroup>
  ```bash Request theme={null}
  curl --request POST \
    --url https://your-api.example.com/v1/workspace/actions \
    --header 'Authorization: Bearer <your-session-token>' \
    --header 'Content-Type: application/json' \
    --data '{
      "action": "createOrgRole",
      "orgId": "org_01j9z8aabbccdd",
      "name": "Compliance Agent",
      "slug": "compliance-agent",
      "permissions": {
        "orgSettings": false,
        "brand": false,
        "locations": true,
        "roles": false,
        "members": false,
        "billing": false
      }
    }'
  ```

  ```json Response theme={null}
  { "ok": true }
  ```
</CodeGroup>

***

## Update role permissions

**action:** `updateOrgRolePermissions`

Updates the permission set for an existing custom role. Requires the `roles` permission. The built-in `owner` role cannot be modified through this action.

### Body fields

<ParamField body="action" type="string" required>
  `"updateOrgRolePermissions"`
</ParamField>

<ParamField body="orgId" type="string" required>
  ID of the organization.
</ParamField>

<ParamField body="roleId" type="string" required>
  ID of the role to update. Must belong to the specified organization. Cannot be the `owner` role.
</ParamField>

<ParamField body="permissions" type="object" required>
  Updated permission map. Any key not included defaults to `false`.
</ParamField>

***

## Delete role

**action:** `deleteOrgRole`

Permanently removes a custom role from the organization. The built-in `owner` role cannot be deleted. Roles with active members must have all members reassigned before deletion. Requires the `roles` permission.

### Body fields

<ParamField body="action" type="string" required>
  `"deleteOrgRole"`
</ParamField>

<ParamField body="orgId" type="string" required>
  ID of the organization.
</ParamField>

<ParamField body="roleId" type="string" required>
  ID of the role to delete. Must have zero assigned members.
</ParamField>

***

## Invite member by email

**action:** `inviteOrgMember`

Creates a pending invitation for an email address. The invited user appears in the member list with `status: "INVITED"` and a synthetic `userId` of `"invite:<email>"`. Requires the `members` permission.

### Body fields

<ParamField body="action" type="string" required>
  `"inviteOrgMember"`
</ParamField>

<ParamField body="orgId" type="string" required>
  ID of the organization.
</ParamField>

<ParamField body="email" type="string" required>
  Email address to invite. Must be a valid email; maximum 254 characters.
</ParamField>

<ParamField body="roleSlug" type="string">
  Slug of the role to assign to the invited member. Defaults to `"owner"` if omitted. The role must already exist in the organization.
</ParamField>

***

## Add member by user ID

**action:** `addOrgMemberByUserId`

Directly adds an existing Clerk user to the organization as an active member. Use this when you already know the user's Clerk ID rather than their email address. Requires the `members` permission.

### Body fields

<ParamField body="action" type="string" required>
  `"addOrgMemberByUserId"`
</ParamField>

<ParamField body="orgId" type="string" required>
  ID of the organization.
</ParamField>

<ParamField body="newUserId" type="string" required>
  Clerk user ID of the user to add. Must start with `"user_"`.
</ParamField>

<ParamField body="roleSlug" type="string">
  Slug of the role to assign. Defaults to `"owner"` if omitted.
</ParamField>

***

## Update member role

**action:** `updateOrgMemberRole`

Changes the role assigned to an existing member. Requires the `members` permission.

### Body fields

<ParamField body="action" type="string" required>
  `"updateOrgMemberRole"`
</ParamField>

<ParamField body="orgId" type="string" required>
  ID of the organization.
</ParamField>

<ParamField body="memberId" type="string" required>
  ID of the membership record to update.
</ParamField>

<ParamField body="roleSlug" type="string" required>
  Slug of the new role to assign. 1–40 characters. The role must exist in the organization.
</ParamField>

***

## Remove member

**action:** `removeOrgMember`

Removes a member from the organization. The organization owner cannot be removed. Requires the `members` permission.

### Body fields

<ParamField body="action" type="string" required>
  `"removeOrgMember"`
</ParamField>

<ParamField body="orgId" type="string" required>
  ID of the organization.
</ParamField>

<ParamField body="memberId" type="string" required>
  ID of the membership record to remove.
</ParamField>

***

## Update billing account

**action:** `updateBillingAccount`

Updates the billing account for an organization. Requires the `billing` permission.

<Note>
  This action updates billing records directly. For Stripe-managed subscriptions, use the [Stripe integration](/integrations/stripe) to initiate checkout or portal sessions rather than writing Stripe IDs directly.
</Note>

### Body fields

<ParamField body="action" type="string" required>
  `"updateBillingAccount"`
</ParamField>

<ParamField body="orgId" type="string" required>
  ID of the organization.
</ParamField>

<ParamField body="planTier" type="string" required>
  Subscription tier. One of `"FREE"`, `"STARTER"`, `"PRO"`, `"ENTERPRISE"`.
</ParamField>

<ParamField body="status" type="string" required>
  Billing status. One of `"NONE"`, `"ACTIVE"`, `"PAST_DUE"`, `"CANCELED"`.
</ParamField>

<ParamField body="stripeCustomerId" type="string or null">
  Stripe customer ID. Maximum 120 characters. Pass `null` to clear.
</ParamField>

<ParamField body="stripeSubscriptionId" type="string or null">
  Stripe subscription ID. Maximum 120 characters. Pass `null` to clear.
</ParamField>

***

## Delete organization

**action:** `deleteOrganization`

Permanently deletes the organization and all associated data. Only the organization owner can perform this action. It also requires the `orgSettings` permission.

<Warning>
  This action is irreversible. All organization data — locations, roles, members, billing records, and documents — will be permanently deleted.
</Warning>

### Body fields

<ParamField body="action" type="string" required>
  `"deleteOrganization"`
</ParamField>

<ParamField body="orgId" type="string" required>
  ID of the organization to delete.
</ParamField>

### Response fields

<ResponseField name="ok" type="boolean" required>
  `true` on success.
</ResponseField>

<ResponseField name="redirectTo" type="string" required>
  Always `"/orgs"`. Clients should redirect the user here after a successful deletion.
</ResponseField>

***

## Upsert user profile

**action:** `upsertUserProfile`

Creates or updates your own user profile. All fields are optional; omitted fields are set to `null`. The `publicSlug`, if provided, must be unique across all users and follow the same format rules as org slugs (lowercase alphanumeric with hyphens, 2–48 characters).

### Body fields

<ParamField body="action" type="string" required>
  `"upsertUserProfile"`
</ParamField>

<ParamField body="displayName" type="string or null">
  Preferred display name. Maximum 120 characters.
</ParamField>

<ParamField body="title" type="string or null">
  Job title or role description. Maximum 120 characters.
</ParamField>

<ParamField body="bio" type="string or null">
  Short biography. Maximum 2000 characters.
</ParamField>

<ParamField body="avatarUrl" type="string or null">
  URL to your avatar image. Maximum 2000 characters.
</ParamField>

<ParamField body="tileAccent" type="string or null">
  Accent color or style token for profile tile display. Maximum 32 characters.
</ParamField>

<ParamField body="publicSlug" type="string or null">
  Public profile slug. Lowercase alphanumeric with hyphens; 2–48 characters; must start and end with alphanumeric. Must be globally unique.
</ParamField>

### Example

<CodeGroup>
  ```bash Request theme={null}
  curl --request POST \
    --url https://your-api.example.com/v1/workspace/actions \
    --header 'Authorization: Bearer <your-session-token>' \
    --header 'Content-Type: application/json' \
    --data '{
      "action": "upsertUserProfile",
      "displayName": "Jordan Smith",
      "title": "Compliance Officer",
      "bio": "Specializing in chattel document compliance for manufactured housing.",
      "avatarUrl": "https://cdn.example.com/avatars/jordan.jpg",
      "tileAccent": "blue",
      "publicSlug": "jordan-smith"
    }'
  ```

  ```json Response theme={null}
  { "ok": true }
  ```
</CodeGroup>

***

## Org permission model

Every organization member is assigned a role. Each role carries a `permissions` object with six boolean keys. Permissions default to `false` for custom roles; the built-in `owner` role always has all permissions set to `true`.

| Permission key | What it controls                                                        |
| -------------- | ----------------------------------------------------------------------- |
| `orgSettings`  | Update the organization name and slug; delete the organization          |
| `brand`        | Customize brand name, logo URL, and primary color                       |
| `locations`    | Add, edit, and remove locations                                         |
| `roles`        | Create custom roles, update role permissions, delete roles              |
| `members`      | Invite members by email or user ID, update member roles, remove members |
| `billing`      | Update billing account tier, status, and Stripe identifiers             |

<Note>
  Organization owners always have all six permissions regardless of what is stored in the role's `permissions` field. The `owner` role is a built-in role (`isBuiltIn: true`) with slug `"owner"` and cannot be deleted or have its permissions modified through the API.
</Note>

To check your current permissions for a specific organization, call [`GET /v1/workspace/org-bundle/:orgId`](/api/workspace/memberships#get-organization-bundle). The response includes your assigned `role` object with its full `permissions` map, as well as the complete list of roles defined for the organization.

***

## Error cases

| Status | Error message                                                                                  | Cause                                                          |
| ------ | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| `400`  | `"Invalid body."` with `issues`                                                                | Request body failed schema validation.                         |
| `400`  | `"That slug is already taken."`                                                                | A different organization already uses the requested org slug.  |
| `400`  | `"Slug must be 2–48 chars, lowercase letters, numbers, hyphens; start/end with alphanumeric."` | Org slug format is invalid.                                    |
| `400`  | `"Not a member of this organization."`                                                         | You are not an active member of the target org.                |
| `400`  | `"You do not have permission for this action."`                                                | Your role does not include the required permission key.        |
| `400`  | `"Cannot remove the organization owner."`                                                      | Attempted to remove the member who owns the org.               |
| `400`  | `"Reassign members before deleting this role."`                                                | Role still has members assigned.                               |
| `400`  | `"Cannot delete owner role."`                                                                  | Attempted to delete the built-in owner role.                   |
| `400`  | `"Cannot change owner permissions here."`                                                      | Attempted to modify the owner role's permissions.              |
| `400`  | `"Only the owner can delete the workspace."`                                                   | Attempted `deleteOrganization` without being the org owner.    |
| `400`  | `"Primary color must be #RRGGBB hex."`                                                         | `brandPrimaryHex` was not a valid 6-digit hex color.           |
| `400`  | `"That public slug is already taken."`                                                         | Another user's profile already uses the requested public slug. |
| `401`  | `Missing Authorization Bearer token.`                                                          | No `Authorization` header was sent.                            |
| `401`  | `Invalid or expired token.`                                                                    | The Bearer token is malformed or has expired.                  |
