---
title: "Create a custom role"
method: POST
path: "/roles"
tags: ["Roles and members"]
---

# Create a custom role

`POST /roles`

## Request

- Base URL: `https://api.primitive.dev/v1`
- URL: `https://api.primitive.dev/v1/roles`
- Auth: HTTP bearer

A role nobody can use to gain access they do not hold: its permissions and mail scope must be within the author's own. mail_scope is "org" (the default) or { addresses, domains, depth }; a scope that names mailboxes must state its depth (metadata, content or raw), otherwise 400 `validation_error` with `details.reason` `depth_required`; any other value is refused with 400 `validation_error`. Its mail scope names no other member's personal mailbox: otherwise 403 `grant_forbidden` with `details.reason` `personal_mailbox_member_only`. Person only: a dashboard session, or the person's own OAuth or CLI token with a session step-up of purpose `roles.write` (the person confirms at the step_up_url a 403 step_up_required answer carries, which names this client, then send the token in the Primitive-Step-Up header; the token works only with the client it was confirmed for). Needs roles:assign. API keys are refused.

## Headers

- `Idempotency-Key` string

## Request body

- CreateRoleRequest
  - `name` string, required
  - `description` string, nullable
  - `permissions` string[], required
  - `mail_scope` union
    - 'org'
    - object
      - `addresses` string[]
      - `domains` string[]
      - `depth` 'metadata' | 'content' | 'raw', required

## Response `201`

Create a custom role

- object
  - `success` true, required
  - `data` CreateRoleResult, required
    - `id` string, required
    - `key` string, required
    - `kind` 'builtin' | 'template' | 'custom', required
    - `name` string, required
    - `description` string, nullable, required
    - `permissions` string[], required
    - `mail_scope` unknown, required
    - `version` number, required
    - `catalog_version` string, nullable, required
    - `predates_catalog` boolean, required

## Other responses

- `400`: Invalid request parameters
- `401`: Invalid or missing API key
- `403`: Authenticated caller lacks permission for the operation
- `404`: feature_disabled: the feature is not enabled for the organization; or not_found: the item the path names does not exist.
- `409`: conflict: the item changed while you edited it, or a member was removed and added back. Look again, then retry.
- `429`: Rate limit exceeded. The `ratelimit-*` headers describe the limiter that rejected this request, with its own limit and window, not the API budget that successful responses report. A limiter that reports only a wait sends `Retry-After` alone.

---

[API](https://skmtc.dev/primitive/apis/primitive-api.md) - [All operations](https://skmtc.dev/primitive/apis/primitive-api/llms.txt) - [OpenAPI document](https://skmtc.dev/primitive/apis/primitive-api/revisions/bce409c97acb?raw)
