Skip to main content

Draft and publish

Prisma Browser policy is versioned configuration. Your changes do not go live the moment you make them. Instead, every change lands in a draft, and you make the draft live by publishing it, which creates a new active version. This is the single most important concept for writing automation safely.

If you have used "staging then deploy", or git "working copy then commit", this will feel familiar:

Prisma Browsergit analogy
draftworking copy (uncommitted edits)
publishcommit
active versionthe committed, live revision

On this page: the two versions, writes always target the draft, publishing, check pending changes, typical write flow, example, verify changes before publishing, common pitfalls.


The two versions​

Every configuration entity (policy rules, sections, controls) has two states you work with day to day, selected with the configurationVersion query parameter on GET requests:

configurationVersionReads
draft (default)Your pending, unpublished edits
activeThe live configuration enforced on devices
# Read pending edits (default)
curl -sS "$PB_API_BASE/policy/security/rules/$RULE_ID" -H "Authorization: Bearer $PB_TOKEN"

# Read what is actually live
curl -sS "$PB_API_BASE/policy/security/rules/$RULE_ID?configurationVersion=active" \
-H "Authorization: Bearer $PB_TOKEN"

Writes always target the draft​

All mutations (POST, PATCH, PUT, DELETE) act on the draft only. They do not accept a configurationVersion; if you send one, it is ignored. There is no way to write directly to the active configuration. This means you can stage as many changes as you like, review them, and only then make them live in one atomic publish.


Publishing​

Two separate endpoints publish the draft:

POST /seb-api/v1/configuration-management/draft/publish
POST /seb-api/v1/configuration-management/draft/partial-publish

Full publish promotes every change currently in the draft at once. It takes a description and nothing else; there is no way to narrow its scope.

Partial publish promotes only the objects you name in entityIds. The rest of the draft stays pending.

caution

Partial publish is a preview feature. Send x-prisma-browser-preview: partial-publish on every call; without it the request returns 400. See Partial publish draft in the API reference.

note

Non-policy objects only. Partial publish supports user groups, application groups, applications, device groups, and application tags. Naming a rule or a section returns 501 PARTIAL_PUBLISH_POLICY_UNSUPPORTED; publish policy changes with a full publish.

Full publish​

curl -sS -X POST "$PB_API_BASE/configuration-management/draft/publish" \
-H "Authorization: Bearer $PB_TOKEN" \
-H "Content-Type: application/json" \
-d '{"description": "CHG-00123: block developer tools on extensions"}'

A 201 means a new active version was created and everything staged in the draft is now live.

Partial publish​

curl -sS -X POST "$PB_API_BASE/configuration-management/draft/partial-publish" \
-H "Authorization: Bearer $PB_TOKEN" \
-H "x-prisma-browser-preview: partial-publish" \
-H "Content-Type: application/json" \
-d '{
"entityIds": [ "0UG01PILOTXXXXXXXXXXXXXXXXXXX" ],
"description": "CHG-00124: add pilot user group"
}'

Response (201):

{
"configurationVersion": { "id": "0CV01EXAMPLEXXXXXXXXXXXXXXXXX", "number": 74 },
"publishedEntityIds": [ "0UG01PILOTXXXXXXXXXXXXXXXXXXX" ]
}

entityIds takes 1 to 1000 unique IDs, each a 29-character prefixed ID whose prefix determines the object type, so no type parameter is needed. A body that carries only description is rejected with 400. The publish is atomic: if any named ID is rejected, nothing is published and the draft is left unchanged.

note

Always include a description that references your internal ticket or change-request ID (for example "CHG-00124: add pilot user group"). This populates the version history with a searchable label and provides an audit trail for compliance reviews. Where no ticket reference exists, use a stable identifier for the automation and its run, for example "Identity sync job 2026-08-06T02:00Z".

Full publish responses​

HTTPMeaningWhat to do
201A new active version was createdDone. Your changes are live.
400The request is invalid (for example a description over 255 characters), or the draft would leave more than 1000 rules in one policy typeCorrect the request, or delete rules from the draft before you publish
403Service account lacks write permissionUse a Super User account
409CONFLICT: the draft has no changesNothing to publish; usually safe to ignore
409PUBLISH_IN_PROGRESS: another publish or a draft reset holds the draft lock. Nothing was publishedWait, then retry
429The tenant's request rate limit was exceededRetry after the current window; see Errors

Partial publish responses​

A 201 returns the new version and the IDs that went live, as shown above. Errors use the format below. Where present, details.errors[] identifies the offending items: field names the request field, index its position, value the offending ID, and error (when present) the reason:

{
"error": {
"code": "NOT_FOUND",
"message": "No pending change found for one or more of the specified entities.",
"timestamp": "2026-08-06T09:15:00Z",
"details": {
"errors": [
{ "index": 0, "field": "entityIds", "error": "no pending change in the current draft", "value": "0UG01PILOTXXXXXXXXXXXXXXXXXXX" }
]
}
}
}
HTTPcodeConditionWhat to do
400VALIDATION_ERRORAn ID is malformed or duplicated, the list is empty or exceeds 1000 items, the body carries only description, or the x-prisma-browser-preview header is missingCorrect the request. This is a defect in the caller; do not retry unchanged
403FORBIDDENThe service account cannot publish configuration changesUse a Super User account
404NOT_FOUNDA valid, supported ID has no pending change in the draft. details.errors[] lists every affected IDUsually the write made no material change, or the object was already published. Do not retry unchanged
409CONFLICTA named object depends on an unpublished change you did not include. details.errors[].ids lists the blockersInclude the blocking IDs and retry, or escalate. Reports only the first conflict found
409PUBLISH_IN_PROGRESSAnother publish or a draft reset holds the draft lock. Nothing was publishedWait, then retry
429(gateway)The tenant's request rate limit was exceededRetry after the current window; see Errors
500INTERNAL_ERRORUnexpected server-side failureRetry with bounded exponential backoff. Nothing was published
501PARTIAL_PUBLISH_POLICY_UNSUPPORTEDThe request named a rule or section. details.errors[] lists every offenderPublish policy objects with a full publish
501PARTIAL_PUBLISH_TYPE_UNSUPPORTEDThe request named an entity that is not supported in draft mode. details.errors[] lists every offenderUse a full publish

Two of these deserve explicit handling in automation:

  • 404 after an apparently successful write. A PUT that changes nothing, such as adding a user who is already a member, stages no pending change for the publish to promote. Decide whether your automation treats this as success or as an exception.
  • 409 from an unpublished dependency. When an administrator has an unpublished rule that references the object you are publishing, both must be published together. This most often appears when deleting an object a rule still uses. Escalation is usually more appropriate than an automated retry.

Check pending changes​

The draft is shared per tenant. Before publishing, confirm exactly what changes are staged so nothing unexpected goes live.

GET /seb-api/v1/configuration-management/draft/pending-changes
curl -sS "$PB_API_BASE/configuration-management/draft/pending-changes" \
-H "Authorization: Bearer $PB_TOKEN"

Response (200):

{
"pageInfo": { "hasNextPage": false, "cursor": "", "totalCount": 1 },
"data": [
{
"entityId": "0UG01PILOTXXXXXXXXXXXXXXXXXXX",
"entityType": "userGroup",
"name": "Pilot - DevTools block",
"operation": "created",
"lastUpdatedBy": "0UR01EXAMPLEADMINXXXXXXXXXXXX"
}
// ... more pending changes
]
}

Each item in the data array describes one pending change:

FieldTypeDescription
entityIdstringID of the changed entity
entityTypestringType of the entity: securityRule, userGroup, applicationGroup, customApplication, and so on
namestringName of the entity (for deleted entities, the name it had before deletion)
operationstringcreated, updated, or deleted
lastUpdatedBystringWho last modified this entity in the draft: an administrator's email or a service-account identifier

An empty draft returns 200 with an empty data array, not 404. Paginate with limit (1-1000, default 100) and cursor.

Publish patterns​

Pattern 1: You know what you changed. Your script just modified a specific object and you have its ID. Partial publish it directly, naming only that object:

curl -sS -X POST "$PB_API_BASE/configuration-management/draft/partial-publish" \
-H "Authorization: Bearer $PB_TOKEN" \
-H "x-prisma-browser-preview: partial-publish" \
-H "Content-Type: application/json" \
-d '{"entityIds": ["0UG01PILOTXXXXXXXXXXXXXXXXXXX"], "description": "CHG-00124: add pilot user group"}'

Where one run modifies several objects, collect the IDs and submit them in a single partial publish rather than one call per object.

Pattern 2: You are not sure what is in the draft. List pending changes first, review them, then publish everything or a targeted subset:

# Step 1: see what is staged
curl -sS "$PB_API_BASE/configuration-management/draft/pending-changes" \
-H "Authorization: Bearer $PB_TOKEN"

# Step 2a: publish the entire draft if all changes look correct
curl -sS -X POST "$PB_API_BASE/configuration-management/draft/publish" \
-H "Authorization: Bearer $PB_TOKEN" \
-H "Content-Type: application/json" \
-d '{"description": "CHG-00124: weekly policy sync"}'

# Step 2b: or publish only the specific objects you want
curl -sS -X POST "$PB_API_BASE/configuration-management/draft/partial-publish" \
-H "Authorization: Bearer $PB_TOKEN" \
-H "x-prisma-browser-preview: partial-publish" \
-H "Content-Type: application/json" \
-d '{"entityIds": ["0UG01PILOTXXXXXXXXXXXXXXXXXXX"], "description": "CHG-00124: add pilot user group"}'

Typical write flow​

  1. Read the current state (draft or active) to know what you are changing.
  2. Write one or more edits (they accumulate in the draft).
  3. Check pending changes with GET /configuration-management/draft/pending-changes to confirm what will go live.
  4. Publish with a description referencing your change ticket.

Example: create, verify, and publish​

A compact version of the full loop. Getting started runs the same loop end to end with cleanup; this is the concept in three calls.

Create a rule (lands on the draft):

RULE_ID="$(curl -sS -X POST "$PB_API_BASE/policy/security/rules" \
-H "Authorization: Bearer $PB_TOKEN" -H "Content-Type: application/json" \
-d '{"name":"Block developer tools","mode":"active","controls":{"developerToolsForExtensions":{"action":"block"}}}' \
| python3 -c 'import sys,json; print(json.load(sys.stdin)["id"])')"

Verify it exists on the draft but not yet on active (the second read returns 404 until you publish):

curl -sS "$PB_API_BASE/policy/security/rules/$RULE_ID" -H "Authorization: Bearer $PB_TOKEN"
curl -sS "$PB_API_BASE/policy/security/rules/$RULE_ID?configurationVersion=active" -H "Authorization: Bearer $PB_TOKEN"

Publish to promote the whole draft to a new active version:

curl -sS -X POST "$PB_API_BASE/configuration-management/draft/publish" \
-H "Authorization: Bearer $PB_TOKEN" -H "Content-Type: application/json" \
-d '{"description": "CHG-00123: block developer tools on extensions"}'

Verify changes before publishing​

Because the draft is shared per tenant, it pays to confirm exactly what is about to go live before promoting it. GET /configuration-management/draft/pending-changes gives you the complete list. For a more targeted check on a specific resource, compare the draft and active versions directly:

  1. Stage your change on the draft (create, update, or delete).
  2. Compare draft and active by reading the same resource twice: once with ?configurationVersion=draft and once with ?configurationVersion=active. This shows exactly what will change for that object, including anything else staged in the shared draft.
  3. Decide. If the changes match your intent, publish. If the result contains unexpected edits, coordinate before publishing.
  4. Publish with a description referencing your change ticket.
import os, requests

base = os.environ["PB_API_BASE"]
headers = {"Authorization": f"Bearer {os.environ['PB_TOKEN']}"}

def get_rule(rule_id, version):
return requests.get(f"{base}/policy/security/rules/{rule_id}",
headers=headers, params={"configurationVersion": version}, timeout=30).json()

draft = get_rule(os.environ["RULE_ID"], "draft")
active = get_rule(os.environ["RULE_ID"], "active")
print("changed:", draft != active) # inspect the difference before publishing
note

To stage a change without enforcing it immediately, create rules with "mode": "disabled", then enable them in a later publish once you have validated them.


Common pitfalls​

  • "I created a rule but it isn't enforced." You created it on the draft. Publish to make it active.
  • "My GET doesn't show my change." You probably read ?configurationVersion=active. Remove it (or use draft) to see pending edits.
  • "Publish returned 409." Check error.code. CONFLICT means the draft is identical to active and there was nothing to publish. PUBLISH_IN_PROGRESS means another publish was running; retry after it finishes.
  • Concurrent editors. The draft is shared per tenant. If multiple automations (or a human in Strata Cloud Manager) edit at once, a publish promotes everyone's staged changes together. Use GET /configuration-management/draft/pending-changes to check what is staged before publishing.