Two endpoints, and the difference between them is the whole model.

- `GET /rbac/users/{userId}` - **one member's resolved permissions** for this application. Connect has already flattened their role, the implicit `admin` synthesis and any workspace-scoped custom role into one list.
- `GET /roles` - **the catalog itself**: the roles and permission ids you declared, not anybody's result.

You call the first on requests; you call the second when rendering a role picker.

```ts
const { granted } = await connect.access.getUserRoles(userId, { workspaceId });
if (!granted.includes("pulse_email:campaign:manage")) {
  return new Response("Forbidden", { status: 403 });
}
```

**Connect never enforces these.** It resolves them and hands them over; your backend is the only thing standing between a user and the action. Hiding a button is a courtesy, the check behind it is the control.

Permission ids are namespaced by your application key (`<key>:<resource>:<action>`) and are yours to define - see the Authorization section of your application in the Console.

## Endpoints

### GET `/api/v1/application/rbac/users/{userId}/roles`

Re-check a member's role for this application

**Scope** `rbac:read` &nbsp;·&nbsp; **Rate limit** 60 requests / min per key

On-demand counterpart of the SSO token exchange's `role`/`permissions` fields (see `/sso/token` above) - the same resolution, callable anytime instead of only at login.

#### When to use it
- Your own session outlives Connect's, so the login-time `role`/`permissions` snapshot has gone stale.
- A platform admin changed this member's role mid-session and you want to react immediately rather than waiting for their next sign-in.

#### Implementation

**1. Prerequisite** - an API key scoped to `rbac:read`.

**2. Send the request** with the Connect `userId` (from `id_token`'s `sub` claim) and the workspace:
```bash
curl "https://connect.cosmoops.com/api/v1/application/rbac/users/USER_ID/roles?workspaceId=WORKSPACE_ID" \
  -H "Authorization: Bearer YOUR_API_KEY_PLAINTEXT"
```

**3. Use `role`/`granted`** exactly like `role`/`permissions` at login - `granted` lists this member's permission ids from your Permission Catalog; `role` is `"admin"`, the key of one of your defined roles, or `null` if their granted set doesn't exactly match any single defined role.

#### Errors
`validation_error` (400 - missing `workspaceId`), `forbidden` (403 - key missing `rbac:read`), `rate_limited` (429 - generously limited since this is read-heavy by design, a partner may re-check on every one of its own requests).

**Parameters**

| Name | In | Required | Notes |
| --- | --- | --- | --- |
| `userId` | path | yes |  |
| `workspaceId` | query | yes |  |

**Responses**

| Status | Meaning |
| --- | --- |
| `200` | Resolved role |
| `400` | validation_error - missing workspaceId |
| `403` | forbidden - key missing rbac:read |
| `429` | rate_limited |

**Response body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `userId` | `string` | yes |  |
| `workspaceId` | `string` | yes |  |
| `role` | `any` | yes |  |
| `granted` | `array` | yes |  |

### GET `/api/v1/application/roles`

List this application's role catalog

**Scope** `roles:read` &nbsp;·&nbsp; **Rate limit** 60 requests / min per key

Lists this application's own grantable-role *definitions* - its Permission Catalog, plus every role that grants a subset of it: a synthesized `"admin"` entry (always full catalog access) and every role you've defined in the Integration tab. This is what `rbac:read` resolves a specific member against, not a specific member's result - use `rbac:read` above for that.

#### Implementation

**1. Prerequisite** - an API key scoped to `roles:read`.

**2. Send the request:**
```bash
curl "https://connect.cosmoops.com/api/v1/application/roles" \
  -H "Authorization: Bearer YOUR_API_KEY_PLAINTEXT"
```

**3. Read `permissionCatalog`/`roles`** - `roles` always includes `"admin"` first; `permissionCatalog` is empty until at least one permission is defined in the Integration tab.

#### Errors
`forbidden` (403 - key missing `roles:read`), `rate_limited` (429 - same read-heavy generous limit as `rbac:read`/`subscription:read`).

**Responses**

| Status | Meaning |
| --- | --- |
| `200` | Role catalog |
| `403` | forbidden - key missing roles:read |
| `429` | rate_limited |

**Response body**

| Field | Type | Required | Notes |
| --- | --- | --- | --- |
| `applicationId` | `string` | yes |  |
| `applicationKey` | `any` | yes |  |
| `permissionCatalog` | `array` | yes |  |
| `roles` | `array` | yes |  |