Skip to main content
In production a policy decides nothing until it has been approved through policy governance. A request under a policy that was never approved is refused with 403 POLICY_DENIED and a reason that ends in “has no PolicyChangeApprovalRecord — it has never completed the Policy Governance approval flow.” This applies to the policies shipped with Parmana too. The seed service copies them into your database but does not approve them, because an approval is a decision your own people make. Approval takes two different people: The server refuses an approval from the proposer, so one person can never make a policy take effect alone. How governance works in general: Policy governance.

1. Each approver makes a step up key

On the approver’s own machine:
The approver keeps step-up.private.pem and sends only step-up.public.pem to the operator. Anyone holding the private key can approve policies as that person.

2. The operator adds the proposer and the approver

On the machine that runs Parmana, in the repository root, with the approver’s public key saved as bob-step-up.public.pem:
Each add prints the new key once. Give each person their own. Details: Manage API keys.

3. The proposer proposes the policy

The request body holds the full policy content and a reason:
For a policy that ships with Parmana, build the body from its file:
Send it. The name and version in the URL must match policyId and policyVersion in the content:
Check: it prints the change ID. The response has status 201 and "status":"PENDING_APPROVAL". Only one proposal per policy name and version can be open at a time. A second one is refused with CONFLICT until the first is approved or rejected.

4. The approver reviews the change

The approver lists open changes. The response is {"changes":[...]}. Each entry holds pendingPolicyChangeId, proposedBy, reason, the proposed content and, under diff, the content in effect now next to the proposed one:
status may be PENDING_APPROVAL, APPROVED or REJECTED, or left out to list all. Only verified humans may list; any other key gets 403 NON_HUMAN_CALLER_DENIED.

5. The approver signs and sends the decision

To approve, on the approver’s machine, from the repository clone:
Check: status 200, and the body contains "status":"APPROVED" and "resolvedBy":"bob". From now on requests under this policy are decided by its rules. To reject, sign with --action reject and send a reason:
Check: the body contains "status":"REJECTED". A signed step up authorization:
  • is valid for 120 seconds (change it with --ttl-seconds);
  • is valid for one change ID and one action only;
  • can be used once.

Or approve from code, with an SDK

SDK 1.3.0 can do steps 3 to 5 from application code, and signs on the approver’s machine without a clone of the repository: signPolicyChangeStepUp() and approvePolicyChange() in TypeScript, parmana.crypto.sign_policy_change_step_up() and approve_policy_change() in Python. Examples are on the TypeScript SDK and Python SDK pages.
SDK 1.3.0 is published on npm and PyPI, so an approver can npm install @parmana/sdk or pip install "parmana[verify]" and sign without the repository.
A signature made by either SDK is accepted by the server exactly as one made by the script: this is tested for both SDKs against the server’s own verifier, and was run against a live self hosted deployment on 2026-09-25.

Errors

The full list of codes is in the error catalog.