---
title: Access Policies | Keycard
description: Configure fine grained access control policies
---

Access policies control what users and applications can access within your Keycard zones.

The policy system has two levels:

- **Policies** are individual authorization rules (for example, “permit user Alice access to Google Calendar”)
- **Policy sets** bundle policies together and deploy them across the entire zone

Keycard uses **default-deny**: every authorization request needs an explicit `permit` for both the user and the application acting on their behalf. Without a matching permit, Keycard denies access. A `forbid` policy always overrides a `permit`, so you can layer restrictive rules on top of permissive baselines.

Postman collection

Download the [Postman collection](/access-policies.postman_collection.json) to import all requests with pre-configured scripts that automatically chain variables (token, zone ID, policy IDs, etc.) between steps. Set `client_id` and `client_secret` in your Postman environment before running.

## Policy Language

Keycard uses [Cedar](https://www.cedarpolicy.com/) by AWS as its policy language. Cedar is an open-source authorization language built for fine grained access control. It is formally verified and statically analyzable.

You author policies in Cedar and submit them through the API in one of two formats:

- **`cedar_raw`** is human readable Cedar syntax, concise and easy to write
- **`cedar_json`** is Cedar’s JSON AST representation, verbose but machine-friendly

Keycard validates both formats against the schema before storing them. On retrieval, use the `?format=cedar` query parameter to get human readable output, or `?format=json` (default) for the JSON representation.

Cedar supports two effects (`permit` and `forbid`), uses deny-wins conflict resolution, and provides `when`/`unless` conditions for attribute based rules.

### Why Cedar

**Safety guarantees.** Keycard validates policies against a typed schema before storing them. The system rejects invalid policies at authoring time, not at runtime. The Cedar engine can prove properties about policy sets without executing them.

**Constrained language.** Cedar is intentionally not a general purpose language. Its small, well defined grammar keeps policy authoring straightforward. Cedar has no loops and no side effects, only declarative rules.

**Model friendly.** The constrained semantics and well defined schema make Cedar excellent for AI assisted policy authoring. LLMs can generate correct policies reliably when given the schema as context.

## Keycard Schema

Note

This section refers to schema version `2026-03-16`. Your zone may have a different schema version available. Check your zone’s policy schemas via the API to confirm the current version.

The Cedar schema maps entity types to the Keycard data model:

| Entity Type                 | Maps To                                                                                 |
| --------------------------- | --------------------------------------------------------------------------------------- |
| Keycard::User               | Zone users are the people who authenticate into a zone                                  |
| Keycard::Application        | Registered applications and OAuth clients that act on behalf of users, or on its own    |
| Keycard::Resource           | Third party API resources configured in the zone (for example, Google Calendar, GitHub) |
| Keycard::RegistrationMethod | Enum entity for application registration methods                                        |
| Keycard::CredentialType     | Enum entity for application credential types                                            |

### Entity properties

Each entity type has attributes you can reference in Cedar `when` and `unless` conditions.

#### Keycard::Application

| Property             | Type               | Description                                                                                                                                                                                |
| -------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| name                 | String             | The application’s display name                                                                                                                                                             |
| registration\_method | RegistrationMethod | How the application was registered. See [Keycard::RegistrationMethod](#keycardregistrationmethod) for values                                                                               |
| credential\_type     | CredentialType?    | The credential type used for authentication. Optional, absent when not yet determined. See [Keycard::CredentialType](#keycardcredentialtype) for values                                    |
| traits               | Set\<String>       | Behavioral traits that activate trait-specific experiences and workflows. Possible values: “gateway” (application acts as an API gateway), “mcp-provider” (application provides MCP tools) |
| dependencies         | Set\<Resource>     | Resources the application is configured to access directly (service-to-service)                                                                                                            |

#### Keycard::User

| Property | Type   | Description                                           |
| -------- | ------ | ----------------------------------------------------- |
| email    | String | The user’s email address from their identity provider |

#### Keycard::Resource

| Property   | Type         | Description                               |
| ---------- | ------------ | ----------------------------------------- |
| identifier | String       | The resource’s unique identifier          |
| name       | String       | Human-readable resource name              |
| scopes     | Set\<String> | OAuth scopes associated with the resource |

#### Keycard::RegistrationMethod

Enum entity for application registration methods. Reference in Cedar as Keycard::RegistrationMethod::“value”.

| Value                                  | Description                                                      |
| -------------------------------------- | ---------------------------------------------------------------- |
| Keycard::RegistrationMethod::“managed” | Created via the management API                                   |
| Keycard::RegistrationMethod::“dcr”     | Registered dynamically via OAuth 2.0 Dynamic Client Registration |

#### Keycard::CredentialType

Enum entity for application credential types. Reference in Cedar as Keycard::CredentialType::“value”.

| Value                                 | Description                            |
| ------------------------------------- | -------------------------------------- |
| Keycard::CredentialType::“token”      | Workload identity (short-lived tokens) |
| Keycard::CredentialType::“password”   | Client ID and secret                   |
| Keycard::CredentialType::“public-key” | Key-based assertion                    |
| Keycard::CredentialType::“url”        | URL-based identity                     |
| Keycard::CredentialType::“public”     | Public client (no secret)              |

#### Claims

The schema defines a Claims type used for JWT claim-based policy conditions.

| Property | Type          | Description                                                                  |
| -------- | ------------- | ---------------------------------------------------------------------------- |
| email    | String?       | Email claim from the JWT, if present                                         |
| groups   | Set\<String>? | Group memberships from the upstream IdP (e.g., Okta groups, Entra ID groups) |

#### Context

Policies have access to a **context** object carrying runtime authorization state:

| Attribute       | Type          | Description                                                                                                 |
| --------------- | ------------- | ----------------------------------------------------------------------------------------------------------- |
| on\_behalf      | Bool          | Whether this is a delegated request (application acting for a user)                                         |
| subject         | User?         | The end user on whose behalf an application acts. Optional, absent for direct application access            |
| scopes          | Set\<String>? | OAuth scopes in the current request. Optional, absent when no scopes are requested                          |
| actor\_claims   | Claims?       | JWT claims for the actor making the request. Optional, absent when claims are not available                 |
| subject\_claims | Claims?       | JWT claims for the subject that an application acts on behalf of. Optional, absent when there is no subject |

Keycard manages schema versions using a date based format (for example, `2026-03-16`). Policies you write against a schema version are guaranteed to continue working. Keycard only makes additive changes to a published version, such as new entity types or attributes, and never removes or alters existing definitions.

## Who can manage policies

Platform RBAC roles control who can manage policies through the [management API](/api/index.md):

- **Organization Administrators and Zone Managers** can create, modify, and activate policies (full CRUD on policies, policy versions, policy sets, and policy set versions)
- **Zone Members** can view policies and policy sets but can’t create, modify, or activate them

[Roles & Permissions ](/console/roles-and-permissions/index.md)Full RBAC reference for management API operations

## Managed policies

The Keycard platform creates and manages these policies. You can identify them by `owner_type: "platform"` in the API. You can’t modify or delete managed policies. Keycard groups them into a managed policy set called **default-zone-policies**.

Expand each entry to view the full Cedar content.

**default-user-grants** : Permits all authenticated users access to all resources

The permissive baseline. This policy grants every authenticated user access to every resource in the zone, regardless of the specific resource or action.

```
@id("default-user-grants")
permit (
  principal is Keycard::User,
  action,
  resource
);
```

**default-app-delegation** : Permits applications to act on behalf of users

Allows applications to perform actions on behalf of a user when the request is a delegated request (`context.on_behalf == true`). This is the foundation for OAuth based delegation flows where an application acts for a user.

```
@id("default-app-delegation")
permit (
  principal is Keycard::Application,
  action,
  resource
) when {
  context.on_behalf == true
};
```

**default-app-direct-access** : Permits applications to access resources directly

Allows [applications as consumers](/getting-started/core-concepts/#applications-as-consumers/index.md) to access resources directly (without acting on behalf of a user), scoped to the application’s configured resource dependencies. The application can only access resources that appear in its `dependencies` set.

```
@id("default-app-direct-access")
permit (
  principal is Keycard::Application,
  action,
  resource
) when {
  principal.dependencies.contains(resource)
};
```

## Customer policies

Customer policies (`owner_type: "customer"`) let you define your own authorization rules and assemble them into policy sets that supersede the default managed policy set. When you activate a customer policy set, it replaces **default-zone-policies** as the active set for your zone.

You can include managed policies alongside your custom policies in the same policy set. This is safe because all policy versions, including managed ones, are immutable. When Keycard updates a managed policy, it issues a new version rather than modifying an existing one. Because your policy set pins exact version IDs, there’s no risk of breaking changes from upstream updates. You control when to adopt a newer managed policy version by creating a new policy set version with an updated manifest.

## Policy lifecycle

The governance system has four resource types: policies, policy versions, policy sets, and policy set versions.

### Policies

A *policy* is a named container for authorization rules. It has metadata (name, description) but no Cedar content. The actual rules live in policy versions.

**Invariants:**

- Policy names are unique within a zone
- Metadata (name, description) can be updated at any time without affecting active authorization
- Policies are soft deleted (archived), never hard deleted
- Platform owned policies can’t be updated or archived by customers

### Policy versions

A *policy version* is an immutable snapshot of Cedar content, validated against a specific schema version. Once created, the Cedar content and schema reference never change.

**Invariants:**

- Policy versions are **immutable**. You can’t modify the Cedar content or schema version after creation
- Each version is validated against the Cedar schema before Keycard stores it
- Keycard computes and stores a SHA-256 hash of the canonicalized policy content for integrity verification
- You can archive a policy version only if it isn’t referenced by an active policy set
- Version numbers auto-increment within each policy

### Policy sets

A *policy set* is a named deployment unit that bundles policies together. Like policies, it has metadata but no policy content. The actual bundle is defined in policy set versions.

**Invariants:**

- Policy set names are unique within a zone
- Platform owned policy sets can’t be updated or archived by customers
- A policy set can’t be archived while it has an active binding

### Policy set versions

A *policy set version* is an immutable manifest that pins exact policy version IDs. When you activate a policy set version, Keycard evaluates exactly the policies listed in that manifest.

**Invariants:**

- Policy set versions are **immutable**. The manifest can’t change after creation
- The manifest must contain at least one entry
- Each manifest entry references a specific policy ID and policy version ID, both of which must exist and not be archived
- Keycard computes a SHA-256 hash of the canonicalized manifest for integrity verification and audit
- You can archive a policy set version only if it isn’t the currently active version
- Activating a version atomically replaces the previously active version
- Version numbers auto increment within each policy set

### How the pieces fit together

The lifecycle for creating and deploying a custom policy is:

1. **Create a policy** : a named container for your rule
2. **Author a policy version** : the immutable Cedar content, validated against a schema
3. **Assemble into a policy set** : bundle policies into a deployment unit, optionally including managed policy versions
4. **Activate the policy set** : bind and activate it for your zone, replacing the default set

Because both policy versions and policy set versions are immutable, you can always trace exactly which Cedar content was active at any point in time. Rolling back means activating a previous policy set version.

## Security guarantees

### No silent policy changes

Policy versions are immutable. Once you create a version, nobody can alter its Cedar content. To change a rule, create a new version. The content hash stored at creation time remains valid forever, and any tampering is detectable.

### Atomic deployment

A policy set version pins exact policy version IDs in an immutable manifest. When Keycard evaluates an authorization request, it uses exactly the policies in the active manifest. There is no window where a partially updated set of policies could be evaluated.

### Safe rollback

Because every policy set version is immutable and preserved, you can revert to any previous configuration by activating an earlier policy set version. The previous manifest still references the same immutable policy versions it always did.

### Full auditability

Every version of every policy and every manifest is preserved with SHA-256 content hashes. You can reconstruct exactly which Cedar content was active at any point in time, and verify that the content hasn’t been modified since creation.

### Separation of authoring and activation

Creating a policy version doesn’t affect live authorization. You can author, validate, and review policies without risk. Changes only take effect when you explicitly activate a policy set version.

## Setup

This walkthrough shows you how to create and activate a custom policy. You’ll authenticate with the API, write a Cedar policy, bundle it into a policy set, and deploy it to your zone.

1. **Authenticate**

   - [Python](#tab-panel-79)
   - [HTTP](#tab-panel-80)
   - [Postman](#tab-panel-81)
   - [Console](#tab-panel-82)

   All management API requests require a Bearer token. Obtain one by exchanging your service account’s client ID and client secret:

   ```
   import requests


   token_response = requests.post(
       "https://api.keycard.ai/service-account-token",
       data={
           "grant_type": "client_credentials",
           "client_id": KEYCARD_CLIENT_ID,
           "client_secret": KEYCARD_CLIENT_SECRET,
       },
   ).json()


   base = "https://api.keycard.ai"
   headers = {"Authorization": f"Bearer {token_response['access_token']}"}
   ```

   Example response

   ```
   {
     "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
     "token_type": "Bearer",
     "expires_in": 3600
   }
   ```

   All management API requests require a Bearer token. Obtain one by exchanging your service account’s client ID and client secret:

   ```
   ACCESS_TOKEN=$(curl -s -X POST https://api.keycard.ai/service-account-token \
     -d "grant_type=client_credentials" \
     -d "client_id=$KEYCARD_CLIENT_ID" \
     -d "client_secret=$KEYCARD_CLIENT_SECRET" \
     | jq -r '.access_token')
   ```

   Example response

   ```
   {
     "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
     "token_type": "Bearer",
     "expires_in": 3600
   }
   ```

   All management API requests require a Bearer token. Obtain one by exchanging your service account’s client ID and client secret:

   ```
   curl -X POST https://api.keycard.ai/service-account-token \
     -d "grant_type=client_credentials" \
     -d "client_id={{client_id}}" \
     -d "client_secret={{client_secret}}"
   ```

   Save the `access_token` from the response as the `{{token}}` variable for subsequent requests.

   Example response

   ```
   {
     "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
     "token_type": "Bearer",
     "expires_in": 3600
   }
   ```

   Sign in to the Keycard Console at **console.keycard.ai**. You’ll be redirected to your identity provider to authenticate.

   ![Keycard Console login page](/images/light/policies/login.png)![Keycard Console login page](/images/dark/policies/login.png)

   Once signed in, navigate to your zone and select **Policies** from the sidebar.

2. **Review the schema**

   - [Python](#tab-panel-95)
   - [HTTP](#tab-panel-96)
   - [Postman](#tab-panel-97)
   - [Console](#tab-panel-98)

   List the available Cedar schemas for your zone to find the latest schema version.

   ```
   schemas = requests.get(
       f"{base}/zones/{zone_id}/policy-schemas",
       headers=headers,
   ).json()


   print(schemas)
   ```

   Example response

   ```
   {
     "items": [
       {
         "id": "01JKXYZ123ABC456DEF789GH",
         "version": "2026-03-16",
         "cedar_schema": "namespace Keycard {\n  entity User = {\n    \"email\": String,\n  };\n  entity Application = {\n    \"credential_type\": String,\n    \"dependencies\": Set<Resource>,\n  };\n  entity Resource = {\n    \"name\": String,\n  };\n};\ntype Claims = {\n  \"email\": String?,\n  \"groups\": Set<String>?,\n};\ncontext = {\n  \"on_behalf\": Bool,\n  \"subject\": User,\n  \"scopes\": Set<String>,\n  \"actor_claims\": Claims,\n  \"subject_claims\": Claims,\n};\n",
         "created_at": "2026-03-16T10:00:00Z"
       }
     ]
   }
   ```

   List the available Cedar schemas for your zone to find the latest schema version.

   ```
   curl https://api.keycard.ai/zones/$ZONE_ID/policy-schemas \
     -H "Authorization: Bearer $ACCESS_TOKEN"
   ```

   Example response

   ```
   {
     "items": [
       {
         "id": "01JKXYZ123ABC456DEF789GH",
         "version": "2026-03-16",
         "cedar_schema": "namespace Keycard {\n  entity User = {\n    \"email\": String,\n  };\n  entity Application = {\n    \"credential_type\": String,\n    \"dependencies\": Set<Resource>,\n  };\n  entity Resource = {\n    \"name\": String,\n  };\n};\ntype Claims = {\n  \"email\": String?,\n  \"groups\": Set<String>?,\n};\ncontext = {\n  \"on_behalf\": Bool,\n  \"subject\": User,\n  \"scopes\": Set<String>,\n  \"actor_claims\": Claims,\n  \"subject_claims\": Claims,\n};\n",
         "created_at": "2026-03-16T10:00:00Z"
       }
     ]
   }
   ```

   List the available Cedar schemas for your zone to find the latest schema version.

   ```
   curl https://api.keycard.ai/zones/{{zone_id}}/policy-schemas \
     -H "Authorization: Bearer {{token}}"
   ```

   Example response

   ```
   {
     "items": [
       {
         "id": "01JKXYZ123ABC456DEF789GH",
         "version": "2026-03-16",
         "cedar_schema": "namespace Keycard {\n  entity User = {\n    \"email\": String,\n  };\n  entity Application = {\n    \"credential_type\": String,\n    \"dependencies\": Set<Resource>,\n  };\n  entity Resource = {\n    \"name\": String,\n  };\n};\ntype Claims = {\n  \"email\": String?,\n  \"groups\": Set<String>?,\n};\ncontext = {\n  \"on_behalf\": Bool,\n  \"subject\": User,\n  \"scopes\": Set<String>,\n  \"actor_claims\": Claims,\n  \"subject_claims\": Claims,\n};\n",
         "created_at": "2026-03-16T10:00:00Z"
       }
     ]
   }
   ```

   The Console policy editor validates against the latest schema automatically. When editing a policy, the Cedar preview sidebar displays available entity types and context attributes.

   ![Policies page in the Keycard Console](/images/light/policies/review-schema.png)![Policies page in the Keycard Console](/images/dark/policies/review-schema.png)

   Tip

   You don’t need to review the schema separately in the Console. The editor handles schema validation inline as you author policies.

   Example response

   ```
   {
     "items": [
       {
         "id": "01JKXYZ123ABC456DEF789GH",
         "version": "2026-03-16",
         "cedar_schema": "namespace Keycard {\n  entity RegistrationMethod enum [\"managed\", \"dcr\"];\n  entity CredentialType enum [\"token\", \"password\", \"public-key\", \"url\", \"public\"];\n\n  entity User {\n    email: String,\n  };\n\n  entity Application {\n    name: String,\n    registration_method: RegistrationMethod,\n    credential_type?: CredentialType,\n    traits: Set<String>,\n    dependencies: Set<Resource>,\n  };\n\n  entity Resource {\n    identifier: String,\n    name: String,\n    scopes: Set<String>,\n  };\n\n  type Claims = {\n    email?: String,\n    groups?: Set<String>,\n  };\n\n  action any appliesTo {\n    principal: [User, Application],\n    resource: Resource,\n    context: {\n      on_behalf: Bool,\n      subject?: User,\n      scopes?: Set<String>,\n      actor_claims?: Claims,\n      subject_claims?: Claims,\n    },\n  };\n}\n",
         "created_at": "2026-03-16T10:00:00Z"
       }
     ]
   }
   ```

3. **Create a policy**

   - [Python](#tab-panel-99)
   - [HTTP](#tab-panel-100)
   - [Postman](#tab-panel-101)
   - [Console](#tab-panel-102)

   Create a named policy container. You’ll add the Cedar content as a version in the next step.

   ```
   policy = requests.post(
       f"{base}/zones/{zone_id}/policies",
       headers=headers,
       json={
           "name": "require-token-credentials",
           "description": "Require token credential type for all application access",
       },
   ).json()


   policy_id = policy["id"]
   ```

   Example response

   ```
   {
     "id": "01JKXYZ456DEF789ABC123GH",
     "zone_id": "01JKXYZ789ABC456DEF123GH",
     "name": "require-token-credentials",
     "description": "Require token credential type for all application access",
     "owner_type": "customer",
     "created_at": "2026-03-03T10:00:00Z",
     "updated_at": "2026-03-03T10:00:00Z",
     "archived_at": null
   }
   ```

   Save the `id` from the response. You’ll need it for the next step.

   Create a named policy container. You’ll add the Cedar content as a version in the next step.

   ```
   curl -X POST https://api.keycard.ai/zones/$ZONE_ID/policies \
     -H "Authorization: Bearer $ACCESS_TOKEN" \
     -H "Content-Type: application/json" \
     -d '{
       "name": "require-token-credentials",
       "description": "Require token credential type for all application access"
     }'
   ```

   Example response

   ```
   {
     "id": "01JKXYZ456DEF789ABC123GH",
     "zone_id": "01JKXYZ789ABC456DEF123GH",
     "name": "require-token-credentials",
     "description": "Require token credential type for all application access",
     "owner_type": "customer",
     "created_at": "2026-03-03T10:00:00Z",
     "updated_at": "2026-03-03T10:00:00Z",
     "archived_at": null
   }
   ```

   Save the `id` from the response. You’ll need it for the next step.

   Create a named policy container. You’ll add the Cedar content as a version in the next step.

   ```
   curl -X POST https://api.keycard.ai/zones/{{zone_id}}/policies \
     -H "Authorization: Bearer {{token}}" \
     -H "Content-Type: application/json" \
     -d '{
       "name": "require-token-credentials",
       "description": "Require token credential type for all application access"
     }'
   ```

   Example response

   ```
   {
     "id": "01JKXYZ456DEF789ABC123GH",
     "zone_id": "01JKXYZ789ABC456DEF123GH",
     "name": "require-token-credentials",
     "description": "Require token credential type for all application access",
     "owner_type": "customer",
     "created_at": "2026-03-03T10:00:00Z",
     "updated_at": "2026-03-03T10:00:00Z",
     "archived_at": null
   }
   ```

   Save the `id` from the response. You’ll need it for the next step.

   Navigate to **Policies** in the sidebar and click **Create policy**.

   ![New policy form with name and description inputs](/images/light/policies/create-policy.png)![New policy form with name and description inputs](/images/dark/policies/create-policy.png)

   Enter a **name** (e.g., `require-token-credentials`) and an optional **description**.

   Note

   In the Console, creating a policy and authoring its first version happen in a single form. Continue to the next step to configure the policy rules.

4. **Author the policy version**

   Policy

   ```
   @id("require-token-credentials")
   forbid (
     principal is Keycard::Application,
     action,
     resource
   ) unless {
     principal has credential_type && principal.credential_type == Keycard::CredentialType::"token"
   };
   ```

   - [Python](#tab-panel-107)
   - [HTTP](#tab-panel-108)
   - [Postman](#tab-panel-109)
   - [Console](#tab-panel-110)

   Create an immutable policy version with Cedar content and a schema version. Submit the policy as `cedar_raw` (human readable Cedar syntax). The API also accepts `cedar_json` (JSON AST). You must provide exactly one.

   ```
   cedar_policy = """\
   @id("require-token-credentials")
   forbid (
     principal is Keycard::Application,
     action,
     resource
   ) unless {
     principal has credential_type && principal.credential_type == Keycard::CredentialType::"token"
   };"""


   version = requests.post(
       f"{base}/zones/{zone_id}/policies/{policy_id}/versions",
       headers=headers,
       json={
           "cedar_raw": cedar_policy,
           "schema_version": "2026-03-16",
       },
   ).json()


   version_id = version["id"]
   ```

   Example response

   ```
   {
     "id": "01JKXYZ789ABC456DEF123GH",
     "policy_id": "01JKXYZ456DEF789ABC123GH",
     "version": 1,
     "schema_version": "2026-03-16",
     "cedar_raw": "@id(\"require-token-credentials\")\nforbid (\n  principal is Keycard::Application,\n  action,\n  resource\n) unless {\n  principal has credential_type && principal.credential_type == \"token\"\n};",
     "content_sha256": "a3f5d8c9e2b1a4c7d6e5f8a9b2c1d4e7",
     "created_at": "2026-03-03T10:01:00Z",
     "archived_at": null
   }
   ```

   Note

   Keycard validates the Cedar policy against the schema before storing the version. If validation fails, the API returns a 400 error with details about what went wrong.

   Save the `id` from the response. This is the `policy_version_id` you’ll reference in the policy set manifest.

   Create an immutable policy version with Cedar content and a schema version. Submit the policy as `cedar_raw` (human readable Cedar syntax). The API also accepts `cedar_json` (JSON AST). You must provide exactly one.

   ```
   curl -X POST https://api.keycard.ai/zones/$ZONE_ID/policies/$POLICY_ID/versions \
     -H "Authorization: Bearer $ACCESS_TOKEN" \
     -H "Content-Type: application/json" \
     -d '{
       "cedar_raw": "@id(\"require-token-credentials\")\nforbid (\n  principal is Keycard::Application,\n  action,\n  resource\n) unless {\n  principal has credential_type && principal.credential_type == Keycard::CredentialType::\"token\"\n};",
       "schema_version": "2026-03-16"
     }'
   ```

   Example response

   ```
   {
     "id": "01JKXYZ789ABC456DEF123GH",
     "policy_id": "01JKXYZ456DEF789ABC123GH",
     "version": 1,
     "schema_version": "2026-03-16",
     "cedar_raw": "@id(\"require-token-credentials\")\nforbid (\n  principal is Keycard::Application,\n  action,\n  resource\n) unless {\n  principal has credential_type && principal.credential_type == \"token\"\n};",
     "content_sha256": "a3f5d8c9e2b1a4c7d6e5f8a9b2c1d4e7",
     "created_at": "2026-03-03T10:01:00Z",
     "archived_at": null
   }
   ```

   Note

   Keycard validates the Cedar policy against the schema before storing the version. If validation fails, the API returns a 400 error with details about what went wrong.

   Save the `id` from the response. This is the `policy_version_id` you’ll reference in the policy set manifest.

   Create an immutable policy version with Cedar content and a schema version. Submit the policy as `cedar_raw` (human readable Cedar syntax). The API also accepts `cedar_json` (JSON AST). You must provide exactly one.

   ```
   curl -X POST https://api.keycard.ai/zones/{{zone_id}}/policies/{{policy_id}}/versions \
     -H "Authorization: Bearer {{token}}" \
     -H "Content-Type: application/json" \
     -d '{
       "cedar_raw": "@id(\"require-token-credentials\")\nforbid (\n  principal is Keycard::Application,\n  action,\n  resource\n) unless {\n  principal has credential_type && principal.credential_type == Keycard::CredentialType::\"token\"\n};",
       "schema_version": "2026-03-16"
     }'
   ```

   Example response

   ```
   {
     "id": "01JKXYZ789ABC456DEF123GH",
     "policy_id": "01JKXYZ456DEF789ABC123GH",
     "version": 1,
     "schema_version": "2026-03-16",
     "cedar_raw": "@id(\"require-token-credentials\")\nforbid (\n  principal is Keycard::Application,\n  action,\n  resource\n) unless {\n  principal has credential_type && principal.credential_type == \"token\"\n};",
     "content_sha256": "a3f5d8c9e2b1a4c7d6e5f8a9b2c1d4e7",
     "created_at": "2026-03-03T10:01:00Z",
     "archived_at": null
   }
   ```

   Note

   Keycard validates the Cedar policy against the schema before storing the version. If validation fails, the API returns a 400 error with details about what went wrong.

   Save the `id` from the response. This is the `policy_version_id` you’ll reference in the policy set manifest.

   Use the rule builder to configure the policy effect, principal, and conditions. The **Cedar preview** sidebar on the right shows the generated Cedar in real time.

   ![Policy form with rule builder and Cedar preview sidebar](/images/light/policies/author-policy-version.png)![Policy form with rule builder and Cedar preview sidebar](/images/dark/policies/author-policy-version.png)

   1. Set the **effect** to `forbid`
   2. Set the **principal** to `Keycard::Application`
   3. Add an **unless** condition: `principal has credential_type && principal.credential_type == "token"`
   4. Click **Create policy** to save the policy and its first version

   Note

   The Console validates the Cedar against the schema before saving. If validation fails, you’ll see inline errors in the rule builder.

   Example response

   ```
   {
     "id": "01JKXYZ789ABC456DEF123GH",
     "policy_id": "01JKXYZ456DEF789ABC123GH",
     "version": 1,
     "schema_version": "2026-03-16",
     "cedar_raw": "@id(\"require-token-credentials\")\nforbid (\n  principal is Keycard::Application,\n  action,\n  resource\n) unless {\n  principal has credential_type && principal.credential_type == Keycard::CredentialType::\"token\"\n};",
     "content_sha256": "a3f5d8c9e2b1a4c7d6e5f8a9b2c1d4e7",
     "created_at": "2026-03-03T10:01:00Z",
     "archived_at": null
   }
   ```

   Note

   Keycard validates the Cedar policy against the schema before storing the version. If validation fails, the API returns a 400 error with details about what went wrong.

   Save the `id` from the response. This is the `policy_version_id` you’ll reference in the policy set manifest.

5. **Create a policy set**

   - [Python](#tab-panel-83)
   - [HTTP](#tab-panel-84)
   - [Postman](#tab-panel-85)
   - [Console](#tab-panel-86)

   Create a policy set that will serve as the deployment unit for your policies.

   ```
   policy_set = requests.post(
       f"{base}/zones/{zone_id}/policy-sets",
       headers=headers,
       json={
           "name": "custom-zone-policies",
           "scope_type": "zone",
       },
   ).json()


   policy_set_id = policy_set["id"]
   ```

   Example response

   ```
   {
     "id": "01JKXYZ123ABC789DEF456GH",
     "zone_id": "01JKXYZ789ABC456DEF123GH",
     "name": "custom-zone-policies",
     "scope_type": "zone",
     "owner_type": "customer",
     "created_at": "2026-03-03T10:02:00Z",
     "updated_at": "2026-03-03T10:02:00Z",
     "archived_at": null
   }
   ```

   Save the `id` from the response.

   Create a policy set that will serve as the deployment unit for your policies.

   ```
   curl -X POST https://api.keycard.ai/zones/$ZONE_ID/policy-sets \
     -H "Authorization: Bearer $ACCESS_TOKEN" \
     -H "Content-Type: application/json" \
     -d '{
       "name": "custom-zone-policies",
       "scope_type": "zone"
     }'
   ```

   Example response

   ```
   {
     "id": "01JKXYZ123ABC789DEF456GH",
     "zone_id": "01JKXYZ789ABC456DEF123GH",
     "name": "custom-zone-policies",
     "scope_type": "zone",
     "owner_type": "customer",
     "created_at": "2026-03-03T10:02:00Z",
     "updated_at": "2026-03-03T10:02:00Z",
     "archived_at": null
   }
   ```

   Save the `id` from the response.

   Create a policy set that will serve as the deployment unit for your policies.

   ```
   curl -X POST https://api.keycard.ai/zones/{{zone_id}}/policy-sets \
     -H "Authorization: Bearer {{token}}" \
     -H "Content-Type: application/json" \
     -d '{
       "name": "custom-zone-policies",
       "scope_type": "zone"
     }'
   ```

   Example response

   ```
   {
     "id": "01JKXYZ123ABC789DEF456GH",
     "zone_id": "01JKXYZ789ABC456DEF123GH",
     "name": "custom-zone-policies",
     "scope_type": "zone",
     "owner_type": "customer",
     "created_at": "2026-03-03T10:02:00Z",
     "updated_at": "2026-03-03T10:02:00Z",
     "archived_at": null
   }
   ```

   Save the `id` from the response.

   Navigate to the **Policy Sets** tab and click **New Policy Set**.

   ![New policy set page with name input and policy selection](/images/light/policies/create-policy-set.png)![New policy set page with name input and policy selection](/images/dark/policies/create-policy-set.png)

   Enter a **name** (e.g., `custom-zone-policies`), then use the **Add policy** combobox to select the policies you want to include.

6. **Create a policy set version**

   - [Python](#tab-panel-103)
   - [HTTP](#tab-panel-104)
   - [Postman](#tab-panel-105)
   - [Console](#tab-panel-106)

   Create an immutable policy set version with a manifest that references your custom policy version.

   Caution

   Your policy set manifest should include the platform-managed default policies to avoid blocking legitimate traffic.

   ```
   ps_version = requests.post(
       f"{base}/zones/{zone_id}/policy-sets/{policy_set_id}/versions",
       headers=headers,
       json={
           "manifest": {
               "entries": [
                   {
                       "policy_id": policy_id,
                       "policy_version_id": version_id,
                   }
               ]
           },
           "schema_version": "2026-03-16",
       },
   ).json()


   ps_version_id = ps_version["id"]
   ```

   Example response

   ```
   {
     "id": "01JKXYZ456DEF123ABC789GH",
     "policy_set_id": "01JKXYZ123ABC789DEF456GH",
     "version": 1,
     "schema_version": "2026-03-16",
     "manifest": {
       "entries": [
         {
           "policy_id": "01JKXYZ456DEF789ABC123GH",
           "policy_version_id": "01JKXYZ789ABC456DEF123GH"
         }
       ]
     },
     "manifest_sha256": "b7e9d2a5c8f1d4e7a9b2c5d8e1f4a7b9",
     "created_at": "2026-03-03T10:03:00Z",
     "archived_at": null
   }
   ```

   Save the `id` from the response.

   Create an immutable policy set version with a manifest that references your custom policy version.

   Caution

   Your policy set manifest should include the platform-managed default policies to avoid blocking legitimate traffic.

   ```
   curl -X POST https://api.keycard.ai/zones/$ZONE_ID/policy-sets/$POLICY_SET_ID/versions \
     -H "Authorization: Bearer $ACCESS_TOKEN" \
     -H "Content-Type: application/json" \
     -d '{
       "manifest": {
         "entries": [
           {
             "policy_id": "<your-policy-id>",
             "policy_version_id": "<your-policy-version-id>"
           }
         ]
       },
       "schema_version": "2026-03-16"
     }'
   ```

   Example response

   ```
   {
     "id": "01JKXYZ456DEF123ABC789GH",
     "policy_set_id": "01JKXYZ123ABC789DEF456GH",
     "version": 1,
     "schema_version": "2026-03-16",
     "manifest": {
       "entries": [
         {
           "policy_id": "01JKXYZ456DEF789ABC123GH",
           "policy_version_id": "01JKXYZ789ABC456DEF123GH"
         }
       ]
     },
     "manifest_sha256": "b7e9d2a5c8f1d4e7a9b2c5d8e1f4a7b9",
     "created_at": "2026-03-03T10:03:00Z",
     "archived_at": null
   }
   ```

   Save the `id` from the response.

   Create an immutable policy set version with a manifest that references your custom policy version.

   Caution

   Your policy set manifest should include the platform-managed default policies to avoid blocking legitimate traffic. The [Postman collection](/access-policies.postman_collection.json) automatically handles this by merging your custom policy with the managed policy entries.

   ```
   curl -X POST https://api.keycard.ai/zones/{{zone_id}}/policy-sets/{{policy_set_id}}/versions \
     -H "Authorization: Bearer {{token}}" \
     -H "Content-Type: application/json" \
     -d '{{ps_version_body}}'
   ```

   The pre-request script in the Postman collection automatically builds the request body by merging your custom policy entry with the managed policy entries.

   Example response

   ```
   {
     "id": "01JKXYZ456DEF123ABC789GH",
     "policy_set_id": "01JKXYZ123ABC789DEF456GH",
     "version": 1,
     "schema_version": "2026-03-16",
     "manifest": {
       "entries": [
         {
           "policy_id": "01JKXYZ456DEF789ABC123GH",
           "policy_version_id": "01JKXYZ789ABC456DEF123GH"
         }
       ]
     },
     "manifest_sha256": "b7e9d2a5c8f1d4e7a9b2c5d8e1f4a7b9",
     "created_at": "2026-03-03T10:03:00Z",
     "archived_at": null
   }
   ```

   Save the `id` from the response.

   Select the desired policy versions using the version dropdowns for each policy in the set. Then click **Publish as Candidate**.

   ![Publish confirmation dialog for a new policy set version](/images/light/policies/publish-candidate.png)![Publish confirmation dialog for a new policy set version](/images/dark/policies/publish-candidate.png)

   Confirm the publish in the dialog. This creates an immutable policy set version with the selected policy versions pinned in its manifest.

   Example response

   ```
   {
     "id": "01JKXYZ456DEF123ABC789GH",
     "policy_set_id": "01JKXYZ123ABC789DEF456GH",
     "version": 1,
     "schema_version": "2026-03-16",
     "manifest": {
       "entries": [
         {
           "policy_id": "01JKXYZ456DEF789ABC123GH",
           "policy_version_id": "01JKXYZ789ABC456DEF123GH"
         }
       ]
     },
     "manifest_sha256": "b7e9d2a5c8f1d4e7a9b2c5d8e1f4a7b9",
     "created_at": "2026-03-03T10:03:00Z",
     "archived_at": null
   }
   ```

   Save the `id` from the response.

7. **Activate the policy set**

   - [Python](#tab-panel-87)
   - [HTTP](#tab-panel-88)
   - [Postman](#tab-panel-89)
   - [Console](#tab-panel-90)

   Bind the policy set version as the active policy set for your zone.

   ```
   response = requests.patch(
       f"{base}/zones/{zone_id}/policy-sets/{policy_set_id}/versions/{ps_version_id}",
       headers=headers,
       json={"active": True},
   ).json()
   ```

   Example response

   ```
   {
     "id": "01JKXYZ456DEF123ABC789GH",
     "policy_set_id": "01JKXYZ123ABC789DEF456GH",
     "version": 1,
     "schema_version": "2026-03-16",
     "manifest": {
       "entries": [
         {
           "policy_id": "01JKXYZ456DEF789ABC123GH",
           "policy_version_id": "01JKXYZ789ABC456DEF123GH"
         }
       ]
     },
     "manifest_sha256": "b7e9d2a5c8f1d4e7a9b2c5d8e1f4a7b9",
     "active": true,
     "created_at": "2026-03-03T10:03:00Z",
     "archived_at": null
   }
   ```

   Bind the policy set version as the active policy set for your zone.

   ```
   curl -X PATCH https://api.keycard.ai/zones/$ZONE_ID/policy-sets/$POLICY_SET_ID/versions/$VERSION_ID \
     -H "Authorization: Bearer $ACCESS_TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"active": true}'
   ```

   Example response

   ```
   {
     "id": "01JKXYZ456DEF123ABC789GH",
     "policy_set_id": "01JKXYZ123ABC789DEF456GH",
     "version": 1,
     "schema_version": "2026-03-16",
     "manifest": {
       "entries": [
         {
           "policy_id": "01JKXYZ456DEF789ABC123GH",
           "policy_version_id": "01JKXYZ789ABC456DEF123GH"
         }
       ]
     },
     "manifest_sha256": "b7e9d2a5c8f1d4e7a9b2c5d8e1f4a7b9",
     "active": true,
     "created_at": "2026-03-03T10:03:00Z",
     "archived_at": null
   }
   ```

   Bind the policy set version as the active policy set for your zone.

   ```
   curl -X PATCH https://api.keycard.ai/zones/{{zone_id}}/policy-sets/{{policy_set_id}}/versions/{{version_id}} \
     -H "Authorization: Bearer {{token}}" \
     -H "Content-Type: application/json" \
     -d '{"active": true}'
   ```

   Example response

   ```
   {
     "id": "01JKXYZ456DEF123ABC789GH",
     "policy_set_id": "01JKXYZ123ABC789DEF456GH",
     "version": 1,
     "schema_version": "2026-03-16",
     "manifest": {
       "entries": [
         {
           "policy_id": "01JKXYZ456DEF789ABC123GH",
           "policy_version_id": "01JKXYZ789ABC456DEF123GH"
         }
       ]
     },
     "manifest_sha256": "b7e9d2a5c8f1d4e7a9b2c5d8e1f4a7b9",
     "active": true,
     "created_at": "2026-03-03T10:03:00Z",
     "archived_at": null
   }
   ```

   From the **Policy Sets** list, click the candidate policy set to open its detail page. Click the **Activate** button.

   ![Policy set detail page with Activate button and confirmation dialog](/images/light/policies/activate-policy-set.png)![Policy set detail page with Activate button and confirmation dialog](/images/dark/policies/activate-policy-set.png)

   Confirm the activation in the dialog. This deploys the policy set zone-wide and atomically replaces the currently active policy set.

   Example response

   ```
   {
     "id": "01JKXYZ456DEF123ABC789GH",
     "policy_set_id": "01JKXYZ123ABC789DEF456GH",
     "version": 1,
     "schema_version": "2026-03-16",
     "manifest": {
       "entries": [
         {
           "policy_id": "01JKXYZ456DEF789ABC123GH",
           "policy_version_id": "01JKXYZ789ABC456DEF123GH"
         }
       ]
     },
     "manifest_sha256": "b7e9d2a5c8f1d4e7a9b2c5d8e1f4a7b9",
     "active": true,
     "created_at": "2026-03-03T10:03:00Z",
     "archived_at": null
   }
   ```

8. **Verify**

   - [Python](#tab-panel-91)
   - [HTTP](#tab-panel-92)
   - [Postman](#tab-panel-93)
   - [Console](#tab-panel-94)

   Confirm the policy set is active for your zone.

   ```
   policy_sets = requests.get(
       f"{base}/zones/{zone_id}/policy-sets",
       headers=headers,
   ).json()


   for ps in policy_sets["items"]:
       print(ps["name"], ps.get("active"), ps.get("mode"))
   ```

   Example response

   ```
   {
     "items": [
       {
         "id": "01JKXYZ123ABC789DEF456GH",
         "zone_id": "01JKXYZ789ABC456DEF123GH",
         "name": "custom-zone-policies",
         "scope_type": "zone",
         "owner_type": "customer",
         "active": true,
         "mode": "active",
         "created_at": "2026-03-03T10:02:00Z",
         "updated_at": "2026-03-03T10:03:00Z"
       }
     ]
   }
   ```

   You should see your policy set with `"active": true` and `"mode": "active"`.

   Confirm the policy set is active for your zone.

   ```
   curl https://api.keycard.ai/zones/$ZONE_ID/policy-sets \
     -H "Authorization: Bearer $ACCESS_TOKEN"
   ```

   Example response

   ```
   {
     "items": [
       {
         "id": "01JKXYZ123ABC789DEF456GH",
         "zone_id": "01JKXYZ789ABC456DEF123GH",
         "name": "custom-zone-policies",
         "scope_type": "zone",
         "owner_type": "customer",
         "active": true,
         "mode": "active",
         "created_at": "2026-03-03T10:02:00Z",
         "updated_at": "2026-03-03T10:03:00Z"
       }
     ]
   }
   ```

   You should see your policy set with `"active": true` and `"mode": "active"`.

   Confirm the policy set is active for your zone.

   ```
   curl https://api.keycard.ai/zones/{{zone_id}}/policy-sets \
     -H "Authorization: Bearer {{token}}"
   ```

   Example response

   ```
   {
     "items": [
       {
         "id": "01JKXYZ123ABC789DEF456GH",
         "zone_id": "01JKXYZ789ABC456DEF123GH",
         "name": "custom-zone-policies",
         "scope_type": "zone",
         "owner_type": "customer",
         "active": true,
         "mode": "active",
         "created_at": "2026-03-03T10:02:00Z",
         "updated_at": "2026-03-03T10:03:00Z"
       }
     ]
   }
   ```

   You should see your policy set with `"active": true` and `"mode": "active"`.

   Navigate to the **Policy Sets** tab. Your active policy set is indicated by a green accent bar and an **Active Policy Set** badge.

   ![Policy sets list showing the active policy set with green accent](/images/light/policies/verify-active.png)![Policy sets list showing the active policy set with green accent](/images/dark/policies/verify-active.png)

   Verify that your custom policy set shows as active with the correct policies listed.

9. **Rollback**

   - [Python](#tab-panel-63)
   - [HTTP](#tab-panel-64)
   - [Postman](#tab-panel-65)
   - [Console](#tab-panel-66)

   To revert to the previous managed policy set, re-activate the platform-owned policy set version that was active before your custom set replaced it.

   ```
   response = requests.patch(
       f"{base}/zones/{zone_id}/policy-sets/{managed_ps_id}/versions/{managed_ps_version_id}",
       headers=headers,
       json={"active": True},
   ).json()
   ```

   Example response

   ```
   {
     "id": "01JKXYZ999AAA888BBB777CC",
     "policy_set_id": "01JKXYZ789DEF456ABC123GH",
     "version": 2,
     "schema_version": "2026-03-16",
     "active": true,
     "created_at": "2026-03-02T21:41:29Z",
     "archived_at": null
   }
   ```

   The managed policy set is now active again, restoring the previous authorization baseline.

   To revert to the previous managed policy set, re-activate the platform-owned policy set version that was active before your custom set replaced it.

   ```
   curl -X PATCH https://api.keycard.ai/zones/$ZONE_ID/policy-sets/$MANAGED_PS_ID/versions/$MANAGED_PS_VERSION_ID \
     -H "Authorization: Bearer $ACCESS_TOKEN" \
     -H "Content-Type: application/json" \
     -d '{"active": true}'
   ```

   Example response

   ```
   {
     "id": "01JKXYZ999AAA888BBB777CC",
     "policy_set_id": "01JKXYZ789DEF456ABC123GH",
     "version": 2,
     "schema_version": "2026-03-16",
     "active": true,
     "created_at": "2026-03-02T21:41:29Z",
     "archived_at": null
   }
   ```

   The managed policy set is now active again, restoring the previous authorization baseline.

   To revert to the previous managed policy set, re-activate the platform-owned policy set version that was active before your custom set replaced it.

   ```
   curl -X PATCH https://api.keycard.ai/zones/{{zone_id}}/policy-sets/{{managed_ps_id}}/versions/{{managed_ps_version_id}} \
     -H "Authorization: Bearer {{token}}" \
     -H "Content-Type: application/json" \
     -d '{"active": true}'
   ```

   Example response

   ```
   {
     "id": "01JKXYZ999AAA888BBB777CC",
     "policy_set_id": "01JKXYZ789DEF456ABC123GH",
     "version": 2,
     "schema_version": "2026-03-16",
     "active": true,
     "created_at": "2026-03-02T21:41:29Z",
     "archived_at": null
   }
   ```

   The managed policy set is now active again, restoring the previous authorization baseline.

   Open the active policy set and use the **version selector** to switch to the previous version. Click **Activate** on that version to restore it.

   The previous version becomes active immediately. Keycard atomically replaces the current policy set, restoring the previous authorization baseline.

   Example response

   ```
   {
     "id": "01JKXYZ999AAA888BBB777CC",
     "policy_set_id": "01JKXYZ789DEF456ABC123GH",
     "version": 2,
     "schema_version": "2026-03-16",
     "active": true,
     "created_at": "2026-03-02T21:41:29Z",
     "archived_at": null
   }
   ```

   The managed policy set is now active again, restoring the previous authorization baseline.

## Examples

These examples show common policy patterns for real-world deployments. Each includes the Cedar policy, an explanation of when to use it, and API calls to create it.

**Prevent shadow server access by requiring token-based credentials**

This policy blocks applications that don’t use token-based credentials from accessing resources. It prevents shadow servers (services using static secrets instead of short-lived tokens) from obtaining upstream access through Keycard.

Applications configured with workload identity federation receive a `credential_type` of `"token"`, meaning they authenticate with short-lived, verifiable tokens rather than long-lived secrets. This `forbid` policy denies access to any application that doesn’t meet that requirement.

The `credential_type` attribute is a `Keycard::CredentialType` enum entity. Other possible values are `Keycard::CredentialType::"password"` (client ID & secret), `Keycard::CredentialType::"public-key"` (key-based assertion), `Keycard::CredentialType::"url"` (URL-based identity), and `Keycard::CredentialType::"public"` (no secret). The attribute is optional and may be absent if the application’s credential type is not yet determined. See the [entity properties](#entity-properties) reference for the full list.

Policy

```
@id("require-workload-identity")
forbid (
  principal is Keycard::Application,
  action,
  resource
) unless {
  principal has credential_type && principal.credential_type == Keycard::CredentialType::"token"
};
```

- [Python](#tab-panel-67)
- [HTTP](#tab-panel-68)
- [Postman](#tab-panel-69)

```
import requests


base = "https://api.keycard.ai"
headers = {"Authorization": f"Bearer {ACCESS_TOKEN}"}


policy = requests.post(
    f"{base}/zones/{zone_id}/policies",
    headers=headers,
    json={
        "name": "require-workload-identity",
        "description": "Block applications without token-based credentials to prevent shadow server access",
    },
).json()


cedar_policy = """\
@id("require-workload-identity")
forbid (
  principal is Keycard::Application,
  action,
  resource
) unless {
  principal has credential_type && principal.credential_type == Keycard::CredentialType::"token"
};"""


version = requests.post(
    f"{base}/zones/{zone_id}/policies/{policy['id']}/versions",
    headers=headers,
    json={
        "cedar_raw": cedar_policy,
        "schema_version": "2026-03-16",
    },
).json()
```

```
curl -X POST https://api.keycard.ai/zones/$ZONE_ID/policies \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "require-workload-identity",
    "description": "Block applications without token-based credentials to prevent shadow server access"
  }'


curl -X POST https://api.keycard.ai/zones/$ZONE_ID/policies/$POLICY_ID/versions \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "cedar_raw": "@id(\"require-workload-identity\")\nforbid (\n  principal is Keycard::Application,\n  action,\n  resource\n) unless {\n  principal has credential_type && principal.credential_type == Keycard::CredentialType::\"token\"\n};",
    "schema_version": "2026-03-16"
  }'
```

**Create the policy:**

```
curl -X POST https://api.keycard.ai/zones/{{zone_id}}/policies \
  -H "Authorization: Bearer {{token}}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "require-workload-identity",
    "description": "Block applications without token-based credentials to prevent shadow server access"
  }'
```

**Create the policy version:**

```
curl -X POST https://api.keycard.ai/zones/{{zone_id}}/policies/{{policy_id}}/versions \
  -H "Authorization: Bearer {{token}}" \
  -H "Content-Type: application/json" \
  -d '{
    "cedar_raw": "@id(\"require-workload-identity\")\nforbid (\n  principal is Keycard::Application,\n  action,\n  resource\n) unless {\n  principal has credential_type && principal.credential_type == Keycard::CredentialType::\"token\"\n};",
    "schema_version": "2026-03-16"
  }'
```

Example response

```
{
  "id": "01JKXYZ000III111JJJ222KK",
  "policy_id": "01JKXYZ999HHH000III111JJ",
  "version": 1,
  "schema_version": "2026-03-16",
  "cedar_raw": "@id(\"require-workload-identity\")\nforbid (\n  principal is Keycard::Application,\n  action,\n  resource\n) unless {\n  principal has credential_type && principal.credential_type == Keycard::CredentialType::\"token\"\n};",
  "content_sha256": "e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1",
  "created_at": "2026-03-03T10:12:00Z",
  "archived_at": null
}
```

**Grant access based on upstream IdP groups**

This policy maps IdP group membership to resource access in Keycard. You manage who gets access from your existing identity provider (Okta, Entra ID, Google Workspace) rather than duplicating access rules.

When a user authenticates through an upstream IdP, Keycard captures their group claims. This `permit` policy checks those claims at authorization time, granting access to users who belong to a specific IdP group. In this example, members of the “Engineering” group in Okta receive access to all resources.

Policy

```
@id("permit-idp-engineering-group")
permit (
  principal is Keycard::User,
  action,
  resource
) when {
  context has subject_claims &&
  context.subject_claims has groups &&
  context.subject_claims.groups.contains("Engineering")
};
```

- [Python](#tab-panel-70)
- [HTTP](#tab-panel-71)
- [Postman](#tab-panel-72)

```
import requests


base = "https://api.keycard.ai"
headers = {"Authorization": f"Bearer {ACCESS_TOKEN}"}


policy = requests.post(
    f"{base}/zones/{zone_id}/policies",
    headers=headers,
    json={
        "name": "permit-idp-engineering-group",
        "description": "Grant access to users in the Engineering group from the upstream IdP",
    },
).json()


cedar_policy = """\
@id("permit-idp-engineering-group")
permit (
  principal is Keycard::User,
  action,
  resource
) when {
  context has subject_claims &&
  context.subject_claims has groups &&
  context.subject_claims.groups.contains("Engineering")
};"""


version = requests.post(
    f"{base}/zones/{zone_id}/policies/{policy['id']}/versions",
    headers=headers,
    json={
        "cedar_raw": cedar_policy,
        "schema_version": "2026-03-16",
    },
).json()
```

```
curl -X POST https://api.keycard.ai/zones/$ZONE_ID/policies \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "permit-idp-engineering-group",
    "description": "Grant access to users in the Engineering group from the upstream IdP"
  }'


curl -X POST https://api.keycard.ai/zones/$ZONE_ID/policies/$POLICY_ID/versions \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "cedar_raw": "@id(\"permit-idp-engineering-group\")\npermit (\n  principal is Keycard::User,\n  action,\n  resource\n) when {\n  context has subject_claims &&\n  context.subject_claims has groups &&\n  context.subject_claims.groups.contains(\"Engineering\")\n};",
    "schema_version": "2026-03-16"
  }'
```

**Create the policy:**

```
curl -X POST https://api.keycard.ai/zones/{{zone_id}}/policies \
  -H "Authorization: Bearer {{token}}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "permit-idp-engineering-group",
    "description": "Grant access to users in the Engineering group from the upstream IdP"
  }'
```

**Create the policy version:**

```
curl -X POST https://api.keycard.ai/zones/{{zone_id}}/policies/{{policy_id}}/versions \
  -H "Authorization: Bearer {{token}}" \
  -H "Content-Type: application/json" \
  -d '{
    "cedar_raw": "@id(\"permit-idp-engineering-group\")\npermit (\n  principal is Keycard::User,\n  action,\n  resource\n) when {\n  context has subject_claims &&\n  context.subject_claims has groups &&\n  context.subject_claims.groups.contains(\"Engineering\")\n};",
    "schema_version": "2026-03-16"
  }'
```

Example response

```
{
  "id": "01JKXYZ111JJJ222KKK333LL",
  "policy_id": "01JKXYZ000III111JJJ222KK",
  "version": 1,
  "schema_version": "2026-03-16",
  "cedar_raw": "@id(\"permit-idp-engineering-group\")\npermit (\n  principal is Keycard::User,\n  action,\n  resource\n) when {\n  context has subject_claims &&\n  context.subject_claims has groups &&\n  context.subject_claims.groups.contains(\"Engineering\")\n};",
  "content_sha256": "f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2",
  "created_at": "2026-03-03T10:13:00Z",
  "archived_at": null
}
```

## Audit Log Integration

Every policy mutation and authorization evaluation emits a structured audit event. These events give you a complete audit trail for compliance and debugging.

### Policy management events

| Event Action                  | Trigger                           |
| ----------------------------- | --------------------------------- |
| policy:create                 | New policy created                |
| policy:update                 | Policy metadata updated           |
| policy:archive                | Policy soft-deleted               |
| policy\_version:create        | New immutable version created     |
| policy\_set:create            | New policy set created            |
| policy\_set:update            | Policy set metadata updated       |
| policy\_set\_version:create   | New policy set version created    |
| policy\_set\_version:activate | Enable the policy set version     |
| policy\_version:archive       | Policy version soft-deleted       |
| policy\_set:archive           | Policy set soft-deleted           |
| policy\_set\_version:archive  | Policy set version soft-deleted   |
| policy\_set\_version:check    | Authorization decision evaluated  |
| policy\_schema:set\_default   | Default schema changed for a zone |

### Authorization evaluation events

Keycard logs each authorization decision with:

- **request\_id** correlates the decision to the originating request
- **decision** is the authorization outcome (allow or deny)
- **determining\_policies** lists the policy IDs that determined the outcome
- **policy\_set\_id** identifies the active policy set that Keycard evaluated
- **policy\_set\_version\_id** identifies the specific version of the policy set that was evaluated
- **evaluation\_status** indicates whether all policies evaluated successfully (complete or partial)
- **diagnostics** contains `{policy_id, message}` entries for evaluation errors or warnings
- **evaluated\_at** is the timestamp of when the evaluation occurred
- **manifest\_sha** is the SHA-256 hash of the policy set manifest for integrity verification

### Privacy

Audit events preserve privacy while maintaining a verifiable audit trail:

- Keycard never logs Cedar source, only SHA-256 hashes of policy content
- Entity attributes (email, name, scopes) never appear in plaintext
- Keycard never logs JWT claims
- Audit emission failures never block authorization decisions

## Diagnosing policy-blocked actions

When a policy blocks a resource during token exchange, Keycard responds in one of two ways depending on whether the blocked resource is the **primary** resource or a **dependency**.

### Two types of policy denial

|                     | Hard denial                                              | Soft denial (dependency)                                                          |
| ------------------- | -------------------------------------------------------- | --------------------------------------------------------------------------------- |
| **What is blocked** | The primary resource in the token request                | A dependency resource (e.g., a secondary API the primary resource depends on)     |
| **HTTP response**   | access\_denied error, no token issued                    | HTTP 200, token issued successfully                                               |
| **Signal**          | error and error\_description fields in the response body | The denied resource is silently dropped from the target claim in the issued token |
| **Visibility**      | Immediately obvious from the error response              | Requires comparing requested resources against the token’s target claim           |

Caution

Soft denials are silent. The token endpoint returns HTTP 200 and the token is valid, but it contains fewer resources than you requested. If your agent assumes all requested resources will be present in the token, it will fail at the downstream API call, not at the token exchange.

### What you see in the error response (hard denial)

A hard policy denial from the token endpoint looks like this:

```
{
  "error": "access_denied",
  "error_description": "Access denied by policy. Policy set: ps_abc123. Policy set version: psv_def456. Determining policies: policy-forbid-external-calendar.",
  "requestId": "req_a1b2c3"
}
```

A **soft denial** returns a normal success response. The only clue is the missing resource in the token:

```
{
  "access_token": "eyJhbGciOi...",
  "token_type": "Bearer",
  "expires_in": 3600
}
```

Decode the `access_token` and inspect the `target` claim. If a resource you requested is missing, it was denied by policy.

### Detecting soft dependency denials

When your agent requests multiple resources, compare what you asked for against what you received:

- [Python](#tab-panel-73)
- [TypeScript](#tab-panel-74)
- [Go](#tab-panel-75)

```
import jwt


token = jwt.decode(access_token, options={"verify_signature": False})
requested = ["https://api.google.com/calendar", "https://api.github.com"]
granted = token.get("target", [])
missing = [r for r in requested if r not in granted]
if missing:
    print(f"Resources denied by policy: {missing}")
    # Use the requestId from the token response to find
    # policy_set_version:check audit events with denial details
```

```
import { decodeJwt } from "jose";


const claims = decodeJwt(accessToken);
const requested = ["https://api.google.com/calendar", "https://api.github.com"];
const granted = (claims.target as string[]) ?? [];
const missing = requested.filter((r) => !granted.includes(r));
if (missing.length > 0) {
  console.log(`Resources denied by policy: ${missing.join(", ")}`);
  // Use the requestId from the token response to find
  // policy_set_version:check audit events with denial details
}
```

```
// After decoding the access token claims:
requested := []string{"https://api.google.com/calendar", "https://api.github.com"}
granted := claims.Target // []string from the "target" claim
missing := []string{}
for _, r := range requested {
    found := false
    for _, g := range granted {
        if r == g { found = true; break }
    }
    if !found { missing = append(missing, r) }
}
if len(missing) > 0 {
    log.Printf("Resources denied by policy: %v", missing)
    // Use the requestId from the token response to find
    // policy_set_version:check audit events with denial details
}
```

Note

For custom-scheme apps (desktop/CLI), the HTML success page includes a yellow policy warning banner with the policy set ID when a dependency is denied. This is a UI-only signal and is not present in the token response itself.

### MCP SDK: handling both hard and soft denials

If you are using the MCP SDK, hard policy denials surface through `AccessContext` errors. For soft denials, check which resources were actually granted:

- [Python](#tab-panel-76)
- [TypeScript](#tab-panel-77)
- [Go](#tab-panel-78)

```
# Hard denial — access_denied error
if access_context.has_errors():
    errors = access_context.get_errors()
    # errors contains the policy denial details


# Soft denial — check granted vs failed resources
successful = access_context.get_successful_resources()
failed = access_context.get_failed_resources()
if failed:
    print(f"Resources denied by policy: {failed}")
```

```
// Hard denial — access_denied error
if (accessContext.hasErrors()) {
  const errors = accessContext.getErrors();
  // errors contains the policy denial details
}


// Soft denial — decode the JWT target claim to check granted resources
// (TS SDK does not yet have resource enumeration methods)
import { decodeJwt } from "jose";


const claims = decodeJwt(accessToken);
const granted = (claims.target as string[]) ?? [];
if (!granted.includes("https://api.github.com")) {
  // This resource was silently denied by policy
}
```

```
// Hard denial — access_denied error
if ac.HasErrors() {
    // ac contains the policy denial details
}


// Soft denial — decode the JWT target claim to check granted resources
// (Go SDK does not yet have resource enumeration methods)
// After decoding the access token claims:
granted := claims.Target // []string from the "target" claim
// Check if your expected resource is in the granted list
```

Tip

The Python MCP SDK provides `get_successful_resources()` and `get_failed_resources()` on `AccessContext`. TypeScript and Go SDK support for resource enumeration is coming soon. In the meantime, decode the JWT `target` claim to check which resources were granted.

See the [MCP SDK documentation](/sdk/mcp/index.md) and [Authorization](/platform/authorization/index.md) for full details on handling access errors.

### Key fields in a denial

| Field              | What it tells you                                                                                      |
| ------------------ | ------------------------------------------------------------------------------------------------------ |
| error              | Always access\_denied for policy denials                                                               |
| error\_description | Human-readable message containing the policy set ID, policy set version ID, and determining policy IDs |
| requestId          | Correlates this denial with policy\_set\_version:check audit events for the full evaluation context    |

Note

The token endpoint returns policy details as a human-readable `error_description` string. For the full machine-readable evaluation (including `evaluation_status`, `diagnostics`, and `evaluated_at`), query the audit log using the `requestId`.

### Common deny scenarios

| Scenario                          | What you see                                                           | Resolution                                                                                                      |
| --------------------------------- | ---------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| No matching permit (default deny) | access\_denied with no determining policies listed                     | Add a permit policy that covers the actor, action, and resource.                                                |
| Explicit forbid matched           | access\_denied with determining policy IDs in the description          | Inspect the listed forbid policies in Console. A forbid always overrides a permit.                              |
| Partial evaluation                | Audit log shows evaluation\_status: “partial” with diagnostics entries | Check diagnostic messages for schema mismatches or malformed entity references. Fix the flagged policies.       |
| No active policy set version      | HTTP 422 from the management API                                       | No policy set version is active in the zone. Activate a policy set version first.                               |
| Entity not found                  | HTTP 400 from the token endpoint                                       | The actor or resource in the request does not exist in the zone. Verify entity IDs.                             |
| Dependency denied by policy       | Token issued but resource missing from target claim                    | Check audit log with requestId for the dependency denial. Add a permit policy covering the dependency resource. |

### Step-by-step debugging workflow

1. **Check the error response**

   Look at the `error_description` from the token endpoint. If it lists determining policy IDs, those policies explicitly blocked the action. If no determining policies are listed, this is a default deny because no permit policy matched.

2. **If the token was issued but a resource is missing**

   This is a soft dependency denial. The primary resource was allowed, but a dependency was denied by policy. Decode the access token and compare the `target` claim against the resources you requested. Use the `requestId` from the token response to find the specific `policy_set_version:check` audit event showing which dependency was denied and why.

3. **If no determining policies, review your permit policies**

   No policy explicitly matched the request. Verify that a permit policy exists for the correct combination of actor, action, and resource. Check that the entity types and IDs match what the policy references.

4. **If determining policies are listed, inspect them in Console**

   Those are forbid policies that blocked the action. Open them in Console to review their conditions. Remember that forbid always takes precedence over permit.

5. **Check the audit log for full evaluation details**

   Filter audit logs by the `requestId` from the error response. The `policy_set_version:check` event contains the complete evaluation context:

   - `evaluation_status` shows whether all policies evaluated (`complete`) or some failed (`partial`)
   - `diagnostics` contains `{policy_id, message}` entries for any evaluation errors (schema mismatches, etc.)
   - `policy_set_version_id` is the exact version that was evaluated
   - `manifest_sha` is the integrity hash of the policy set manifest

6. **If evaluation was partial, fix the flagged policies**

   When `evaluation_status` is `partial`, the `diagnostics` array explains what went wrong. Common causes include schema mismatches, missing entity attributes, or malformed policy syntax. Fix the flagged policies and re-evaluate.

Tip

In Console, filter audit logs by `requestId` to see the full context of any authorization decision. This is the fastest way to get the machine-readable evaluation details that the token endpoint does not include directly.

[Audit Log Export ](/console/audit-log-export/index.md)Export audit logs to your S3 bucket in OCSF format
