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

# Resource permissions

> Control access to LLM resources with role-based authorization. Assign owner, writer, or reader roles to users and verify permissions before every action.

The MKA1 API provides resource-level role-based access control (RBAC) through the authorization endpoints.
Use these endpoints to grant, check, and revoke permissions on LLM resources for specific users.

<Warning>
  Every endpoint in this guide is gated by dedicated API key scopes: `write:fine-grained-authorization` (grant, revoke) and `read:fine-grained-authorization` (check).
  Requests made with a key that lacks them fail with `403 Forbidden` regardless of resource ownership.
  These are **admin scopes** — only organization admins can put them on an API key, so if you're not an admin, you'll need a key issued by one to run through this guide.
  See [Required API key scopes](#required-api-key-scopes) for how to enable them.
</Warning>

## Required API key scopes

API keys carry a fixed set of scopes, and the fine-grained authorization scopes are not part of every key by default:

| Scope | Required for |
| - | - |
| `write:fine-grained-authorization` | [Grant a role](#grant-a-role-to-a-user), [revoke a role](#revoke-a-role), [grant public access](#grant-public-access) |
| `read:fine-grained-authorization` | [Check a user's permission](#check-a-users-permission) |

Both are **admin scopes** — only organization admins can enable them on an API key.
If you're not an admin, the toggles aren't available to you: ask an admin to issue you a key with these scopes, or to add them to your existing key.

If your key is missing a scope, the API rejects the request before any ownership check happens:

```json theme={null}
{
  "code": "FORBIDDEN",
  "status": 403,
  "message": "API key is missing required scope(s): write:fine-grained-authorization",
  "data": {
    "missing_scopes": ["write:fine-grained-authorization"],
    "required_scopes": ["write:fine-grained-authorization"]
  }
}
```

### Enable the scopes in the platform console

As an organization admin, you can create a key with these scopes, or add them to an existing key, at [platform.mka1.com/admin/api-keys](https://platform.mka1.com/admin/api-keys):

1. In the console, go to **Access → API Keys**.
2. Create a new key, or open an existing key to edit it.
3. In the scope list, find the **Fine-grained authorization** row and enable both checkboxes (read and write).
4. Click **Save**.

<img src="https://mintcdn.com/meetkaiinc/QhBs-l8jT0P0xIwA/images/authorization/fine-grained-authorization-scopes.png?fit=max&auto=format&n=QhBs-l8jT0P0xIwA&q=85&s=907cacb2e01f4521875286c418445556" alt="The API key scope editor with both Fine-grained authorization checkboxes enabled and highlighted" width="713" height="915" data-path="images/authorization/fine-grained-authorization-scopes.png" />

For more on creating API keys and how scopes work in general, see [the platform getting-started guide](/docs/platform-getting-started).

## How resource authorization works

Every LLM resource (completion, file, vector store, conversation, response, or skill) can have per-user roles assigned to it.

Three roles form a strict hierarchy:

| Role | Can do |
| - | - |
| **owner** | Grant and revoke roles, plus everything writer can do |
| **writer** | Modify the resource, plus everything reader can do |
| **reader** | Read the resource |

Resource IDs are created when you make LLM API calls.
For example, creating a conversation returns a `conv_` ID, creating a response returns a `resp_` ID, and uploading a file returns a `file_` ID.
The authenticated caller (or the end user specified via `X-On-Behalf-Of`) automatically becomes the **owner** of that resource.

Use the returned resource ID with the authorization endpoints below to manage access for other users.
Only owners can grant or revoke roles.
If a non-owner attempts to grant or revoke, the API returns `403 Forbidden`.

These authorizations are enforced by the MKA1 backend on every request.
Any attempt to read, modify, or delete a resource that the caller does not have access to is rejected with an appropriate error response.

You can also grant public access by using "\*" as the user ID, but only for `writer` or `reader` roles.

## Set up: create a resource as Alice

The examples in this guide build on each other and use two end users:

* **Alice** (`user_alice`) creates a conversation, which makes her its owner.
* **Bob** (`user_bob`) is granted access, exercises it, and has it revoked.

The `X-On-Behalf-Of` header controls which end user a call acts as.
Start by creating a conversation as Alice and capturing its ID — every snippet below uses it:

<CodeGroup>
  ```ts TypeScript SDK theme={null}
  import { SDK } from '@meetkai/mka1';
  import { SDKError } from '@meetkai/mka1/models/errors';

  const sdk = new SDK({ bearerAuth: 'Bearer <mka1-api-key>' });

  // Alice creates a conversation and becomes its owner
  const conv = await sdk.llm.conversations.create({
    xOnBehalfOf: 'user_alice',
    createConversationRequest: {
      metadata: { project: 'acme' },
    },
  })
  const resourceId = conv.id // e.g. conv_abc123
  ```

  ```python Python SDK theme={null}
  from meetkai_mka1 import SDK

  sdk = SDK(bearer_auth="Bearer <mka1-api-key>")

  # Alice creates a conversation and becomes its owner
  conv = sdk.llm.conversations.create(
      metadata={"project": "acme"},
      x_on_behalf_of="user_alice",
  )
  resource_id = conv.id
  ```

  ```csharp C# SDK theme={null}
  using MeetKai.MKA1;
  using MeetKai.MKA1.Types.Components;
  using MeetKai.MKA1.Types.Errors;

  var sdk = new SDK(bearerAuth: "Bearer <mka1-api-key>");

  // Alice creates a conversation and becomes its owner
  var conv = await sdk.Llm.Conversations.CreateAsync(
      body: new CreateConversationRequest()
      {
          Metadata = new Dictionary<string, string> { { "project", "acme" } },
      },
      xOnBehalfOf: "user_alice"
  );
  var resourceId = conv.ConversationObject!.Id;
  ```

  ```bash CLI theme={null}
  RESOURCE_ID=$(mka1 llm conversations create \
    --body '{ "metadata": { "project": "acme" } }' \
    -H 'X-On-Behalf-Of: user_alice' \
    -o json | jq -r '.id')
  ```

  ```bash Bash theme={null}
  RESOURCE_ID=$(curl -s https://apigw.mka1.com/api/v1/llm/conversations \
    --request POST \
    --header 'Content-Type: application/json' \
    --header 'Authorization: Bearer <mka1-api-key>' \
    --header 'X-On-Behalf-Of: user_alice' \
    --data '{ "metadata": { "project": "acme" } }' | jq -r '.id')
  ```
</CodeGroup>

<Note>
  Grant, check, and revoke only work against a real resource ID that the acting user owns.
  Two common mistakes produce a `403 Forbidden` "is not an owner" error: passing a made-up ID (like `conv-abc-123`), or omitting `X-On-Behalf-Of` — without it the call acts as the API key's own service user, not as Alice.
</Note>

## Grant a role to a user

Use [`POST /api/v1/authorization/llm/grant`](/api-reference/resource-authorization/grant-permission-to-a-user-or-make-public) to assign a role to a user on a resource.
The caller must be the resource owner, so Alice does the granting:

<CodeGroup>
  ```ts TypeScript SDK theme={null}
  // Alice grants reader access to user_bob on her conversation
  await sdk.permissions.llm.grant({
    xOnBehalfOf: 'user_alice',
    grantPermissionRequest: {
      resourceType: 'conversation',
      resourceId,
      userId: 'user_bob',
      role: 'reader',
    },
  })
  ```

  ```python Python SDK theme={null}
  # Alice grants reader access to user_bob on her conversation
  sdk.permissions.llm.grant(
      resource_type="conversation",
      resource_id=resource_id,
      user_id="user_bob",
      role="reader",
      x_on_behalf_of="user_alice",
  )
  ```

  ```csharp C# SDK theme={null}
  // Alice grants reader access to user_bob on her conversation
  await sdk.Permissions.Llm.GrantAsync(
      body: new GrantPermissionRequest()
      {
          ResourceType = LlmResourceType.Conversation,
          ResourceId = resourceId,
          UserId = "user_bob",
          Role = ResourceRole.Reader,
      },
      xOnBehalfOf: "user_alice"
  );
  ```

  ```bash CLI theme={null}
  mka1 permissions llm grant \
    --resource-type conversation \
    --resource-id "$RESOURCE_ID" \
    --user-id user_bob \
    --role reader \
    -H 'X-On-Behalf-Of: user_alice'
  ```

  ```bash Bash theme={null}
  curl https://apigw.mka1.com/api/v1/authorization/llm/grant \
    --request POST \
    --header 'Content-Type: application/json' \
    --header 'Authorization: Bearer <mka1-api-key>' \
    --header 'X-On-Behalf-Of: user_alice' \
    --data "{
      \"resourceType\": \"conversation\",
      \"resourceId\": \"$RESOURCE_ID\",
      \"userId\": \"user_bob\",
      \"role\": \"reader\"
    }"
  ```
</CodeGroup>

A successful grant returns `204 No Content`.

The `resourceType` must be one of: `completion`, `file`, `vector_store`, `conversation`, `response`, or `skill`.
The `role` must be one of: `owner`, `writer`, or `reader`.

## Check a user's permission

Use [`GET /api/v1/authorization/llm/check`](/api-reference/resource-authorization/check-user-permission) to verify whether the authenticated caller has a specific role on a resource.
The response contains an `allowed` boolean.
Here Bob — who was just granted `reader` — checks his own access:

<CodeGroup>
  ```ts TypeScript SDK theme={null}
  // Bob checks whether he has reader access
  const result = await sdk.permissions.llm.check({
    resourceType: 'conversation',
    resourceId,
    role: 'reader',
    xOnBehalfOf: 'user_bob',
  })

  console.log(result.allowed) // true
  ```

  ```python Python SDK theme={null}
  # Bob checks whether he has reader access
  result = sdk.permissions.llm.check(
      resource_type="conversation",
      resource_id=resource_id,
      role="reader",
      x_on_behalf_of="user_bob",
  )

  print(result.allowed)  # True
  ```

  ```csharp C# SDK theme={null}
  // Bob checks whether he has reader access
  var result = await sdk.Permissions.Llm.CheckAsync(
      resourceType: LlmResourceType.Conversation,
      resourceId: resourceId,
      role: ResourceRole.Reader,
      xOnBehalfOf: "user_bob"
  );

  Console.WriteLine(result.CheckPermissionResponse!.Allowed); // true
  ```

  ```bash CLI theme={null}
  mka1 permissions llm check \
    --resource-type conversation \
    --resource-id "$RESOURCE_ID" \
    --role reader \
    -H 'X-On-Behalf-Of: user_bob'
  ```

  ```bash Bash theme={null}
  curl "https://apigw.mka1.com/api/v1/authorization/llm/check?resourceType=conversation&resourceId=$RESOURCE_ID&role=reader" \
    --header 'Authorization: Bearer <mka1-api-key>' \
    --header 'X-On-Behalf-Of: user_bob'
  ```
</CodeGroup>

```json theme={null}
{ "allowed": true }
```

Because roles are hierarchical, an owner also passes a `reader` or `writer` check.

## Revoke a role

Use [`POST /api/v1/authorization/llm/revoke`](/api-reference/resource-authorization/revoke-permission-from-a-user-or-remove-public-access) to remove a role from a user.
Only the resource owner can revoke, so Alice removes Bob's access:

<CodeGroup>
  ```ts TypeScript SDK theme={null}
  // Alice revokes Bob's reader access
  await sdk.permissions.llm.revoke({
    xOnBehalfOf: 'user_alice',
    revokePermissionRequest: {
      resourceType: 'conversation',
      resourceId,
      userId: 'user_bob',
      role: 'reader',
    },
  })
  ```

  ```python Python SDK theme={null}
  # Alice revokes Bob's reader access
  sdk.permissions.llm.revoke(
      resource_type="conversation",
      resource_id=resource_id,
      user_id="user_bob",
      role="reader",
      x_on_behalf_of="user_alice",
  )
  ```

  ```csharp C# SDK theme={null}
  // Alice revokes Bob's reader access
  await sdk.Permissions.Llm.RevokeAsync(
      body: new RevokePermissionRequest()
      {
          ResourceType = LlmResourceType.Conversation,
          ResourceId = resourceId,
          UserId = "user_bob",
          Role = ResourceRole.Reader,
      },
      xOnBehalfOf: "user_alice"
  );
  ```

  ```bash CLI theme={null}
  mka1 permissions llm revoke \
    --resource-type conversation \
    --resource-id "$RESOURCE_ID" \
    --user-id user_bob \
    --role reader \
    -H 'X-On-Behalf-Of: user_alice'
  ```

  ```bash Bash theme={null}
  curl https://apigw.mka1.com/api/v1/authorization/llm/revoke \
    --request POST \
    --header 'Content-Type: application/json' \
    --header 'Authorization: Bearer <mka1-api-key>' \
    --header 'X-On-Behalf-Of: user_alice' \
    --data "{
      \"resourceType\": \"conversation\",
      \"resourceId\": \"$RESOURCE_ID\",
      \"userId\": \"user_bob\",
      \"role\": \"reader\"
    }"
  ```
</CodeGroup>

A successful revoke returns `204 No Content`.

## Grant public access

Grant a role to all authenticated users by setting `userId` to "\*".
Public access is restricted to `writer` and `reader` roles — you cannot make someone a public owner.
As with any grant, the caller must be the resource owner (Alice):

<CodeGroup>
  ```ts TypeScript SDK theme={null}
  // Alice makes her conversation publicly readable
  await sdk.permissions.llm.grant({
    xOnBehalfOf: 'user_alice',
    grantPermissionRequest: {
      resourceType: 'conversation',
      resourceId,
      userId: '*',
      role: 'reader',
    },
  })
  ```

  ```python Python SDK theme={null}
  # Alice makes her conversation publicly readable
  sdk.permissions.llm.grant(
      resource_type="conversation",
      resource_id=resource_id,
      user_id="*",
      role="reader",
      x_on_behalf_of="user_alice",
  )
  ```

  ```csharp C# SDK theme={null}
  // Alice makes her conversation publicly readable
  await sdk.Permissions.Llm.GrantAsync(
      body: new GrantPermissionRequest()
      {
          ResourceType = LlmResourceType.Conversation,
          ResourceId = resourceId,
          UserId = "*",
          Role = ResourceRole.Reader,
      },
      xOnBehalfOf: "user_alice"
  );
  ```

  ```bash CLI theme={null}
  mka1 permissions llm grant \
    --resource-type conversation \
    --resource-id "$RESOURCE_ID" \
    --user-id '*' \
    --role reader \
    -H 'X-On-Behalf-Of: user_alice'
  ```

  ```bash Bash theme={null}
  curl https://apigw.mka1.com/api/v1/authorization/llm/grant \
    --request POST \
    --header 'Content-Type: application/json' \
    --header 'Authorization: Bearer <mka1-api-key>' \
    --header 'X-On-Behalf-Of: user_alice' \
    --data "{
      \"resourceType\": \"conversation\",
      \"resourceId\": \"$RESOURCE_ID\",
      \"userId\": \"*\",
      \"role\": \"reader\"
    }"
  ```
</CodeGroup>

## Handle authorization errors

The authorization endpoints return `403 Forbidden` for two distinct reasons — check the `message` to tell them apart.

**The API key is missing a required scope.** The request is rejected before any ownership check.
Fix this by enabling the fine-grained authorization scopes on your key — see [Required API key scopes](#required-api-key-scopes).

```json theme={null}
{
  "code": "FORBIDDEN",
  "status": 403,
  "message": "API key is missing required scope(s): write:fine-grained-authorization",
  "data": {
    "missing_scopes": ["write:fine-grained-authorization"],
    "required_scopes": ["write:fine-grained-authorization"]
  }
}
```

**The caller is not the resource owner.** Only owners can grant or revoke:

```json theme={null}
{
  "code": "FORBIDDEN",
  "status": 403,
  "message": "User vzEeDJQ39D631rCPWvekWENlJP8WEAgF is not an owner of conversation:conv-abc-123"
}
```

Besides a genuine permissions problem, this error has two common causes:

* **The resource ID doesn't exist** — you must [create the resource first](#set-up-create-a-resource-as-alice) and use its real ID; placeholder IDs are rejected as not-owned.
* **`X-On-Behalf-Of` is missing** — the call then acts as the API key's own service user (the opaque user ID in the message above), not as the end user who owns the resource.

Always check the caller's role before attempting permission changes, or handle the 403 response in your application.

## End-to-end example: two users with different roles

This walkthrough demonstrates distinct user profiles with differentiated permissions.
Alice creates a conversation (becoming its owner), then grants read access to Bob.
We verify that Bob can read but cannot write, and that Bob cannot grant permissions to others.

<CodeGroup>
  ```ts TypeScript SDK theme={null}
  // Step 1 — Alice creates a conversation (she becomes the owner)
  const conv = await sdk.llm.conversations.create({
    xOnBehalfOf: 'user_alice',
    createConversationRequest: {
      metadata: { project: 'acme' },
    },
  })
  const resourceId = conv.id

  // Step 2 — Alice grants reader access to Bob
  await sdk.permissions.llm.grant({
    xOnBehalfOf: 'user_alice',
    grantPermissionRequest: {
      resourceType: 'conversation',
      resourceId,
      userId: 'user_bob',
      role: 'reader',
    },
  })

  // Step 3 — Check Bob's permissions
  // Bob has reader access → allowed
  const bobReader = await sdk.permissions.llm.check({
    resourceType: 'conversation',
    resourceId,
    role: 'reader',
    xOnBehalfOf: 'user_bob',
  })
  console.log('Bob reader:', bobReader.allowed) // true

  // Bob does NOT have writer access → denied
  const bobWriter = await sdk.permissions.llm.check({
    resourceType: 'conversation',
    resourceId,
    role: 'writer',
    xOnBehalfOf: 'user_bob',
  })
  console.log('Bob writer:', bobWriter.allowed) // false

  // Step 4 — Bob tries to grant (fails with 403)
  try {
    await sdk.permissions.llm.grant({
      xOnBehalfOf: 'user_bob',
      grantPermissionRequest: {
        resourceType: 'conversation',
        resourceId,
        userId: 'user_charlie',
        role: 'reader',
      },
    })
  } catch (err) {
    if (err instanceof SDKError) {
      console.log('Bob grant rejected:', err.statusCode) // 403
    }
  }

  // Step 5 — Alice revokes Bob's access
  await sdk.permissions.llm.revoke({
    xOnBehalfOf: 'user_alice',
    revokePermissionRequest: {
      resourceType: 'conversation',
      resourceId,
      userId: 'user_bob',
      role: 'reader',
    },
  })

  // Step 6 — Verify Bob is now denied
  const bobAfterRevoke = await sdk.permissions.llm.check({
    resourceType: 'conversation',
    resourceId,
    role: 'reader',
    xOnBehalfOf: 'user_bob',
  })
  console.log('Bob after revoke:', bobAfterRevoke.allowed) // false
  ```

  ```python Python SDK theme={null}
  # Step 1 — Alice creates a conversation (she becomes the owner)
  conv = sdk.llm.conversations.create(
      metadata={"project": "acme"},
      x_on_behalf_of="user_alice",
  )
  resource_id = conv.id

  # Step 2 — Alice grants reader access to Bob
  sdk.permissions.llm.grant(
      resource_type="conversation",
      resource_id=resource_id,
      user_id="user_bob",
      role="reader",
      x_on_behalf_of="user_alice",
  )

  # Step 3 — Check Bob's permissions
  # Bob has reader access -> allowed
  bob_reader = sdk.permissions.llm.check(
      resource_type="conversation",
      resource_id=resource_id,
      role="reader",
      x_on_behalf_of="user_bob",
  )
  print("Bob reader:", bob_reader.allowed)  # True

  # Bob does NOT have writer access -> denied
  bob_writer = sdk.permissions.llm.check(
      resource_type="conversation",
      resource_id=resource_id,
      role="writer",
      x_on_behalf_of="user_bob",
  )
  print("Bob writer:", bob_writer.allowed)  # False

  # Step 4 — Bob tries to grant (fails with 403)
  try:
      sdk.permissions.llm.grant(
          resource_type="conversation",
          resource_id=resource_id,
          user_id="user_charlie",
          role="reader",
          x_on_behalf_of="user_bob",
      )
  except Exception as err:
      print("Bob grant rejected:", err)  # 403

  # Step 5 — Alice revokes Bob's access
  sdk.permissions.llm.revoke(
      resource_type="conversation",
      resource_id=resource_id,
      user_id="user_bob",
      role="reader",
      x_on_behalf_of="user_alice",
  )

  # Step 6 — Verify Bob is now denied
  bob_after_revoke = sdk.permissions.llm.check(
      resource_type="conversation",
      resource_id=resource_id,
      role="reader",
      x_on_behalf_of="user_bob",
  )
  print("Bob after revoke:", bob_after_revoke.allowed)  # False
  ```

  ```csharp C# SDK theme={null}
  // Step 1 — Alice creates a conversation (she becomes the owner)
  var conv = await sdk.Llm.Conversations.CreateAsync(
      body: new CreateConversationRequest()
      {
          Metadata = new Dictionary<string, string> { { "project", "acme" } },
      },
      xOnBehalfOf: "user_alice"
  );
  var resourceId = conv.ConversationObject!.Id;

  // Step 2 — Alice grants reader access to Bob
  await sdk.Permissions.Llm.GrantAsync(
      body: new GrantPermissionRequest()
      {
          ResourceType = LlmResourceType.Conversation,
          ResourceId = resourceId,
          UserId = "user_bob",
          Role = ResourceRole.Reader,
      },
      xOnBehalfOf: "user_alice"
  );

  // Step 3 — Check Bob's permissions
  // Bob has reader access → allowed
  var bobReader = await sdk.Permissions.Llm.CheckAsync(
      resourceType: LlmResourceType.Conversation,
      resourceId: resourceId,
      role: ResourceRole.Reader,
      xOnBehalfOf: "user_bob"
  );
  Console.WriteLine($"Bob reader: {bobReader.CheckPermissionResponse!.Allowed}"); // true

  // Bob does NOT have writer access → denied
  var bobWriter = await sdk.Permissions.Llm.CheckAsync(
      resourceType: LlmResourceType.Conversation,
      resourceId: resourceId,
      role: ResourceRole.Writer,
      xOnBehalfOf: "user_bob"
  );
  Console.WriteLine($"Bob writer: {bobWriter.CheckPermissionResponse!.Allowed}"); // false

  // Step 4 — Bob tries to grant (fails with 403)
  try
  {
      await sdk.Permissions.Llm.GrantAsync(
          body: new GrantPermissionRequest()
          {
              ResourceType = LlmResourceType.Conversation,
              ResourceId = resourceId,
              UserId = "user_charlie",
              Role = ResourceRole.Reader,
          },
          xOnBehalfOf: "user_bob"
      );
  }
  catch (APIException ex)
  {
      Console.WriteLine($"Bob grant rejected: {(int)ex.Response.StatusCode}"); // 403
  }

  // Step 5 — Alice revokes Bob's access
  await sdk.Permissions.Llm.RevokeAsync(
      body: new RevokePermissionRequest()
      {
          ResourceType = LlmResourceType.Conversation,
          ResourceId = resourceId,
          UserId = "user_bob",
          Role = ResourceRole.Reader,
      },
      xOnBehalfOf: "user_alice"
  );

  // Step 6 — Verify Bob is now denied
  var bobAfterRevoke = await sdk.Permissions.Llm.CheckAsync(
      resourceType: LlmResourceType.Conversation,
      resourceId: resourceId,
      role: ResourceRole.Reader,
      xOnBehalfOf: "user_bob"
  );
  Console.WriteLine($"Bob after revoke: {bobAfterRevoke.CheckPermissionResponse!.Allowed}"); // false
  ```

  ```bash CLI theme={null}
  # Step 1 — Alice creates a conversation (she becomes the owner)
  CONV_JSON=$(mka1 llm conversations create \
    --body '{ "metadata": { "project": "acme" } }' \
    -H 'X-On-Behalf-Of: user_alice' \
    -o json)
  RESOURCE_ID=$(echo "$CONV_JSON" | jq -r '.id')

  # Step 2 — Alice grants reader access to Bob
  mka1 permissions llm grant \
    --resource-type conversation \
    --resource-id "$RESOURCE_ID" \
    --user-id user_bob \
    --role reader \
    -H 'X-On-Behalf-Of: user_alice'

  # Step 3 — Check Bob has reader access (allowed: true)
  mka1 permissions llm check \
    --resource-type conversation \
    --resource-id "$RESOURCE_ID" \
    --role reader \
    -H 'X-On-Behalf-Of: user_bob'

  # Step 3b — Check Bob does NOT have writer access (allowed: false)
  mka1 permissions llm check \
    --resource-type conversation \
    --resource-id "$RESOURCE_ID" \
    --role writer \
    -H 'X-On-Behalf-Of: user_bob'

  # Step 4 — Bob tries to grant (returns 403 Forbidden)
  mka1 permissions llm grant \
    --resource-type conversation \
    --resource-id "$RESOURCE_ID" \
    --user-id user_charlie \
    --role reader \
    -H 'X-On-Behalf-Of: user_bob'

  # Step 5 — Alice revokes Bob's access
  mka1 permissions llm revoke \
    --resource-type conversation \
    --resource-id "$RESOURCE_ID" \
    --user-id user_bob \
    --role reader \
    -H 'X-On-Behalf-Of: user_alice'

  # Step 6 — Verify Bob is now denied (allowed: false)
  mka1 permissions llm check \
    --resource-type conversation \
    --resource-id "$RESOURCE_ID" \
    --role reader \
    -H 'X-On-Behalf-Of: user_bob'
  ```

  ```bash Bash theme={null}
  # Step 1 — Alice creates a conversation (she becomes the owner)
  CONV=$(curl -s https://apigw.mka1.com/api/v1/llm/conversations \
    --request POST \
    --header 'Content-Type: application/json' \
    --header 'Authorization: Bearer <mka1-api-key>' \
    --header 'X-On-Behalf-Of: user_alice' \
    --data '{ "metadata": { "project": "acme" } }')
  RESOURCE_ID=$(echo "$CONV" | jq -r '.id')

  # Step 2 — Alice grants reader access to Bob
  curl https://apigw.mka1.com/api/v1/authorization/llm/grant \
    --request POST \
    --header 'Content-Type: application/json' \
    --header 'Authorization: Bearer <mka1-api-key>' \
    --header 'X-On-Behalf-Of: user_alice' \
    --data "{
      \"resourceType\": \"conversation\",
      \"resourceId\": \"$RESOURCE_ID\",
      \"userId\": \"user_bob\",
      \"role\": \"reader\"
    }"

  # Step 3 — Check Bob has reader access (allowed: true)
  curl "https://apigw.mka1.com/api/v1/authorization/llm/check?resourceType=conversation&resourceId=$RESOURCE_ID&role=reader" \
    --header 'Authorization: Bearer <mka1-api-key>' \
    --header 'X-On-Behalf-Of: user_bob'

  # Step 3b — Check Bob does NOT have writer access (allowed: false)
  curl "https://apigw.mka1.com/api/v1/authorization/llm/check?resourceType=conversation&resourceId=$RESOURCE_ID&role=writer" \
    --header 'Authorization: Bearer <mka1-api-key>' \
    --header 'X-On-Behalf-Of: user_bob'

  # Step 4 — Bob tries to grant (returns 403 Forbidden)
  curl -s -o /dev/null -w "%{http_code}" \
    https://apigw.mka1.com/api/v1/authorization/llm/grant \
    --request POST \
    --header 'Content-Type: application/json' \
    --header 'Authorization: Bearer <mka1-api-key>' \
    --header 'X-On-Behalf-Of: user_bob' \
    --data "{
      \"resourceType\": \"conversation\",
      \"resourceId\": \"$RESOURCE_ID\",
      \"userId\": \"user_charlie\",
      \"role\": \"reader\"
    }"
  # Output: 403

  # Step 5 — Alice revokes Bob's access
  curl https://apigw.mka1.com/api/v1/authorization/llm/revoke \
    --request POST \
    --header 'Content-Type: application/json' \
    --header 'Authorization: Bearer <mka1-api-key>' \
    --header 'X-On-Behalf-Of: user_alice' \
    --data "{
      \"resourceType\": \"conversation\",
      \"resourceId\": \"$RESOURCE_ID\",
      \"userId\": \"user_bob\",
      \"role\": \"reader\"
    }"

  # Step 6 — Verify Bob is now denied (allowed: false)
  curl "https://apigw.mka1.com/api/v1/authorization/llm/check?resourceType=conversation&resourceId=$RESOURCE_ID&role=reader" \
    --header 'Authorization: Bearer <mka1-api-key>' \
    --header 'X-On-Behalf-Of: user_bob'
  ```
</CodeGroup>

## Next steps

* Review the [Authentication](/docs/authentication) guide for API key and JWT usage.
* API Reference for the authorization endpoints:
  * [`POST /api/v1/authorization/llm/grant`](/api-reference/resource-authorization/grant-permission-to-a-user-or-make-public) — Grant a role
  * [`POST /api/v1/authorization/llm/revoke`](/api-reference/resource-authorization/revoke-permission-from-a-user-or-remove-public-access) — Revoke a role
  * [`GET /api/v1/authorization/llm/check`](/api-reference/resource-authorization/check-user-permission) — Check permission


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.