---
title: "Create a new policy set"
method: POST
path: "/zones/{zone_id}/policy-sets"
tags: ["PolicySets"]
---

# Create a new policy set

`POST /zones/{zone_id}/policy-sets`

## Request

- Base URL: `https://docs.keycard.ai` (the document declares no server; this is the origin it was published from)
- URL: `https://docs.keycard.ai/zones/{zone_id}/policy-sets`
- Auth: one of:
  - HTTP bearer
  - OAuth 2
    - Client credentials
    - Token URL: `/service-account-token`

Creates a policy set. Supply `manifest` to create its first
version and any new policies in the same transaction. A failure rolls
back every write. Without `manifest`, creates a versionless set and
preserves the existing response body.

Entries use manifest apply semantics with no predecessor: bare pins
use each policy's latest version; supplied content reuses that version
when its SHA and schema match. Omitted `schema_version` uses the zone
default. This operation supports neither `dry_run` nor `If-Match`.
Set `manifest.activate: true` to bind v1 to the zone's active slot in
the same transaction. Requires `target_type: zone` (the default) and
the `activate` permission on `policy_set_bindings`, in addition to
the route's `create` permission. Omitted or false leaves the set unbound.

The `ETag` header is the set revision, as on `GET`. The manifest digest
is `policy_set_version.manifest_sha` and the `ETag` of `GET .../manifest`.

Domain error codes: `policy_set_name_conflict`, `policy_name_conflict`,
`policy_not_found`, `policy_archived`, `policy_version_not_found`,
`version_archived`, `schema_version_mismatch`,
`manifest_duplicate_policy`, `missing_cedar_content`, `invalid_cedar`,
`schema_version_unsupported`, `activate_requires_zone_target`.

## Path parameters

- `zone_id` string, required

## Headers

- `X-API-Version` string
- `X-Client-Request-ID` string, uuid

## Request body

- PdpCreatePolicySetRequest: At most one of `target_type` or the deprecated `scope_type` may be provided; supplying both with different values returns 400. Defaults to `zone`.
  - `name` string, required
  - `scope_type` 'zone': **Deprecated.** Use `target_type` instead. Only `zone` is accepted; use `target_type` for `user` targets.
  - `target_type` 'zone' | 'user': What this policy set targets: - `"zone"` — applies to all requests in the zone. - `"user"` — can be bound to a specific user.
  - `manifest` PdpCreatePolicySetManifest: Content for the first version, created atomically with the set.
    - `activate` boolean: Bind the first version to the zone's active slot in the same transaction. Requires a zone-targeted set and the activate permission on policy_set_bindings.
    - `entries` PdpApplyManifestEntry[], required: Initial manifest entries, in request order.
      - union
        - PdpExistingPolicyEntry: Reference to an existing (non-archived) policy in the zone — not limited to policies already in this set. With `cedar_raw`/`cedar_json` (mutually exclusive): the server diffs by content SHA; unchanged content under the resolved schema reuses the pinned policy version, changed content mints a new one. Without content ("pin as-is"): reuses the version pinned in the latest manifest, or the policy's latest version when the policy is newly added to this set. Bare pins are re-versioned when the resolved schema differs from the pinned version's schema. With `policy_version_id`: pins exactly that existing version and mints nothing. Mutually exclusive with `cedar_raw`/`cedar_json` (a version is content; 400 when both are supplied). The version must belong to `policy_id`, must not be archived (`version_archived`), and must have been validated against the resolved schema (`schema_version_mismatch`; no re-versioning). Reported as `repinned` when the set already pins a different version of the policy, otherwise `reused`. Platform-owned policies accept bare pins and `policy_version_id` (customers cannot mint versions of those).
          - `policy_id` string, required: Public ID of an existing policy in the zone.
          - `policy_version_id` string: Public ID of an existing version of `policy_id` to pin. Mutually exclusive with cedar_raw and cedar_json.
          - `cedar_raw` string: Cedar policy text. Mutually exclusive with cedar_json.
          - `cedar_json` object: Cedar policy JSON. Mutually exclusive with cedar_raw.
        - PdpNewPolicyEntry: Mints a new customer-owned policy with the requested name (409 `policy_name_conflict` on collision) plus its first version from the supplied content. Exactly one of `cedar_raw`/`cedar_json` is required.
          - `new_policy` object, required
            - `name` string, required
            - `description` string
          - `cedar_raw` string: Cedar policy text. Mutually exclusive with cedar_json.
          - `cedar_json` object: Cedar policy JSON. Mutually exclusive with cedar_raw.
    - `schema_version` string: Schema to validate and pin v1 against. Defaults to the zone default.

## Response `201`

The newly created policy set

- PdpPolicySetWithBinding
  - `id` string, required
  - `zone_id` string, required
  - `name` string, required
  - `scope_type` 'zone' | 'resource' | 'user' | 'session', required: **Deprecated.** Use `target_type` instead. Carries the same value.
  - `target_type` 'zone' | 'user', required: What this policy set targets: - `"zone"` — applies to all requests in the zone. - `"user"` — scoped to a specific user. `resource` and `session` are reserved; legacy sets with those scopes carry them in the deprecated `scope_type` field.
  - `latest_version_id` string, nullable
  - `latest_version` integer, nullable: Human-readable version number of the latest version (e.g., 1, 2, 3)
  - `archived_at` string, date-time, nullable
  - `created_at` string, date-time, required
  - `updated_at` string, date-time, required
  - `created_by` string, required
  - `created_by_user` PdpUser: The organization user behind a `created_by`, `updated_by` or `archived_by` value. Returned only when `expand[]=user` is requested.
    - `id` string, required: Public ID of the user in the organization's platform zone. This is not the same value as the `*_by` field it expands; use it to link to `/zones/{zone_id}/users/{id}`.
    - `zone_id` string, required: Public ID of the organization's platform zone the user belongs to.
    - `email` string, nullable, required: The user's email address, or null when not known.
  - `updated_by` string, nullable
  - `updated_by_user` PdpUser: The organization user behind a `created_by`, `updated_by` or `archived_by` value. Returned only when `expand[]=user` is requested.
    - `id` string, required: Public ID of the user in the organization's platform zone. This is not the same value as the `*_by` field it expands; use it to link to `/zones/{zone_id}/users/{id}`.
    - `zone_id` string, required: Public ID of the organization's platform zone the user belongs to.
    - `email` string, nullable, required: The user's email address, or null when not known.
  - `owner_type` 'platform' | 'customer', required: Who manages this policy set: - `"platform"` — managed by the Keycard platform (system policies). - `"customer"` — managed by the tenant (custom policies).
  - `scope_target_id` string, nullable: **Deprecated.** Use `target_id` instead. Carries the active binding's target; null when unbound.
  - `target_id` string, nullable: Target entity ID. Equals `zone_id` for zone-targeted sets; the principal identifier for principal-scoped sets. Null only for legacy non-zone sets that predate target tracking.
  - `mode` 'active' | 'shadow', nullable
  - `active` boolean: Whether this policy set is currently bound to a scope
  - `active_version_id` string, nullable: Public ID of the currently active (bound) version
  - `active_version` integer, nullable: Human-readable version number of the active version (e.g., 1, 2, 3)
  - `shadow_version_id` string, nullable: Public ID of the shadow (observed) version, if any
  - `shadow_version` integer, nullable: Human-readable version number of the shadow version
  - `policy_set_version` PdpPolicySetVersion
    - `id` string, required
    - `policy_set_id` string, required
    - `version` integer, required
    - `schema_version` string, required: Schema version pinned to this policy set version. Determines the Cedar schema used for evaluation when activated.
    - `manifest` PdpPolicySetManifest, required
      - `entries` PdpPolicySetManifestEntry[], required
        - `policy_id` string, required
        - `policy_version_id` string, required
        - `sha` string: SHA-256 of the policy version content, populated by the server
    - `manifest_sha` string, required: Hex-encoded SHA-256 of the canonicalized manifest
    - `attestation` PdpAttestationStatement: Decoded content of an Attestation JWS payload. Describes the exact policy set version composition at attestation time. This schema defines what consumers see after base64url-decoding the Attestation.payload field.
      - `v` 1, required: Statement schema version
      - `type` 'policy_set_attestation', required: Statement type discriminator
      - `zone_id` string, required
      - `policy_set_id` string, required
      - `policy_set_version` integer, required
      - `manifest_sha` string, required: SHA-256 of the policy set version manifest. Verifiers MUST check this matches the policy_set_version.manifest_sha to detect attestation/version mismatches.
      - `status` 'created' | 're_signed', required: Event that produced this attestation. "created" is the initial attestation at version creation; "re_signed" is a re-attestation after key rotation (same content, new signature).
      - `attested_at` string, date-time, required
      - `attested_by` string, required
      - `key_id` string, required: Key ID of the signing key used to produce the attestation signature. Matches the "kid" in the JWS protected header.
    - `active` boolean: Whether this policy set version is currently bound with mode='active'. Always populated in responses; clients must treat absence as unknown rather than inferring 'not bound'.
    - `archived_at` string, date-time, nullable: Timestamp when the version was archived. Non-null only for archived versions; null or absent means not archived.
    - `archived_by` string, nullable: Identifier of the actor that archived the version. Null or absent means not archived.
    - `archived_by_user` PdpUser: The organization user behind a `created_by`, `updated_by` or `archived_by` value. Returned only when `expand[]=user` is requested.
      - `id` string, required: Public ID of the user in the organization's platform zone. This is not the same value as the `*_by` field it expands; use it to link to `/zones/{zone_id}/users/{id}`.
      - `zone_id` string, required: Public ID of the organization's platform zone the user belongs to.
      - `email` string, nullable, required: The user's email address, or null when not known.
    - `created_at` string, date-time, required
    - `created_by` string, required
    - `created_by_user` PdpUser: The organization user behind a `created_by`, `updated_by` or `archived_by` value. Returned only when `expand[]=user` is requested.
      - `id` string, required: Public ID of the user in the organization's platform zone. This is not the same value as the `*_by` field it expands; use it to link to `/zones/{zone_id}/users/{id}`.
      - `zone_id` string, required: Public ID of the organization's platform zone the user belongs to.
      - `email` string, nullable, required: The user's email address, or null when not known.
    - `owner_type` 'platform' | 'customer', required: Who manages this policy set version: - `"platform"` — managed by the Keycard platform (system policy set versions). - `"customer"` — managed by the tenant (custom policy set versions).
  - `changes` PdpApplyChange[]: Per-policy outcomes, present only when created with a manifest.
    - `policy_id` string, required
    - `name` string, required: The policy's name. Lets a caller correlate a `created_policy` row with its `new_policy` request entry without a re-list.
    - `action` 'created_policy' | 'created_version' | 'reused' | 'repinned' | 'dropped', required: `repinned`: an explicit `policy_version_id` replaced a different version the set already pinned for that policy; no version minted.
    - `policy_version_id` string: Absent when action is dropped.
  - `warnings` PdpApplyWarning[]: Non-fatal findings, present only when non-empty on create.
    - `code` string, required: Machine-readable warning code, e.g. unknown_actions.
    - `message` string, required
  - `binding` PdpPolicySetBinding
    - `id` string, required: Binding identifier (stable per slot)
    - `scope_type` 'zone', required: **Deprecated.** Use `target_type` instead. Carries the same value.
    - `scope_target_id` string, required: **Deprecated.** Use `target_id` instead. Carries the same value.
    - `target_type` 'zone' | 'user', required: What this binding targets
    - `target_id` string, required: Target entity ID. Equals zone_id for zone-targeted bindings.
    - `mode` 'active' | 'shadow', required: Binding mode
    - `policy_set_id` string, required: Public ID of the bound policy set
    - `policy_set_version_id` string, required: Public ID of the bound policy set version
    - `created_at` string, date-time, required

## Other responses

- `400`: bad request error response when caller supplied invalid input data
- `401`: unauthorized error response when caller session is not authenticated
- `403`: forbidden error response when caller does not have permissions to a resource
- `404`: not found error response when caller does not have permission to see a resource or the resource does not exist
- `409`: A policy or policy set name conflicts, or a referenced policy is archived
- `429`: rate limit exceeded error response when caller has exhausted api limits for the given time period
- `500`: internal server error response when server encountered error of its own creation
- `503`: service unavailable error when server you're attempting to reach is not available
- `default`: internal server error response when server encountered error of its own creation

## Changes

- **2026-10-16** `0cfaff865771`: 16 breaking, 8 warning, 88 info
  - the response property `details/items/field` became optional for the status `400`
  - the response property `details/items/field` became optional for the status `401`
  - the response property `details/items/field` became optional for the status `403`
  - the response property `details/items/field` became optional for the status `409`
  - …108 more
- **2026-06-17** `1a0dd4d857ee`: 3 breaking, 6 info
  - removed the enum value `resource` of the request property `scope_type`
  - removed the enum value `session` of the request property `scope_type`
  - removed the enum value `user` of the request property `scope_type`
  - added the new optional request property `target_type`
  - …5 more
- **2026-04-20** `932699a5ea16`: 2 info
  - added the optional property `allOf[subschema #2]/shadow_version` to the response with the `201` status
  - added the optional property `allOf[subschema #2]/shadow_version_id` to the response with the `201` status
- **2026-04-02** `aee6e66de0d3`: 1 info
  - the endpoint scheme security `OAuth2` was added to the API
- **2026-03-16** `d3d69f45bfed`: 1 info
  - endpoint added

[Operation history](https://skmtc.dev/keycardai/apis/keycard-ai-keycard-api/changes/zones/:zone_id/policy-sets/post.md)

---

[API](https://skmtc.dev/keycardai/apis/keycard-ai-keycard-api.md) - [All operations](https://skmtc.dev/keycardai/apis/keycard-ai-keycard-api/llms.txt) - [OpenAPI document](https://skmtc.dev/keycardai/apis/keycard-ai-keycard-api/revisions/0dee67e0a73a?raw)
