---
title: "Create a role"
description: "A name and a list of permissions. What comes back is longer than what you sent."
url: "https://openemail.uk/docs/api/roles/create"
area: "API"
category: "Roles & access"
---

# Create a role

A name and a list of permissions. What comes back is longer than what you sent.

`POST /roles`

## POST /roles

A name and a list of permissions. What comes back is longer than what you sent.

## Example

Needs `roles:write`. Returns 201. A custom role is `builtin: null`, `editable: true` and `deletable: true`, and is held by nobody until somebody is moved onto it.

**curl**

```
curl -X POST "$OE/roles" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{
    "name": "Support",
    "description": "Answers the shared inboxes and nothing else.",
    "permissions": ["emails:send", "threads:write", "labels:write", "contacts:read"]
  }'
```

**Response**

```
{
  "object": "role",
  "id": "role_2b81de079c1f0a4b7e05d386",
  "name": "Support",
  "description": "Answers the shared inboxes and nothing else.",
  "permissions": [
    "emails:send",
    "emails:read",
    "threads:read",
    "threads:write",
    "labels:read",
    "labels:write",
    "contacts:read"
  ],
  "builtin": null,
  "editable": true,
  "deletable": true,
  "members": 0,
  "apiKeys": 0,
  "createdAt": "2026-08-30T10:41:02.000Z",
  "updatedAt": "2026-08-30T10:41:02.000Z"
}
```

> Four permissions went in and seven came back. `emails:send` implies `emails:read`, `threads:write` implies `threads:read`, `labels:write` implies `labels:read`. A role that may archive a thread it cannot open is a checkbox somebody forgot rather than a policy anybody means, so the implication is stored rather than refused. The list also comes back in canonical order, which is what lets a client diff two roles as JSON and decide whether Save is enabled.

> An unknown permission IS refused here rather than dropped: `templates:writ` is `invalid_parameter`, a 422, naming the string. The service normalises silently because it is also the seeding path and the MCP path, where failing a whole role over one unrecognised word would be worse. It is wrong on a call somebody made deliberately. A 200 carrying a role that cannot edit templates has told them nothing, and they will spend the afternoon on it.

> A duplicate name on the same workspace is `role_name_taken`, a 409. The 25th custom role is `role_limit_reached`, a 422. It guards how big a matrix can get before nobody audits it, and is not a plan boundary.

> Creating a role gives it to nobody. Moving people onto it is `PATCH /members/{userId}`, and pointing a key at it is done where the key is issued.
