Reject an approver change
Rejects an approver change.
# Sign the step up authorization on your own machine first:
# npx tsx scripts/sign-policy-change-step-up.ts --private-key-file step-up.private.pem \
# --key-id checker-step-up-1 --pending-policy-change-id ba7c5827-5844-4069-94fc-9b438ef08f78 --action reject > step-up.json
curl -X POST https://parmana-api-real.vercel.app/approval-issuers/changes/ba7c5827-5844-4069-94fc-9b438ef08f78/reject \
-H "Authorization: Bearer $PARMANA_API_KEY" \
-H "Content-Type: application/json" \
--data "{\"rejectionReason\": \"Priya stays on refunds until 31 October.\", \"stepUpAuthorization\": $(cat step-up.json)}"import { ParmanaClient, signPolicyChangeStepUp } from "@parmana/sdk";
import { readFileSync } from "node:fs";
const client = new ParmanaClient({
endpoint: "https://parmana-api-real.vercel.app",
apiKey: process.env.PARMANA_API_KEY!,
});
const stepUp = signPolicyChangeStepUp({
pendingPolicyChangeId: "ba7c5827-5844-4069-94fc-9b438ef08f78",
action: "reject",
privateKeyPem: readFileSync("step-up.private.pem", "utf8"),
keyId: "checker-step-up-1",
});
const change = await client.rejectApproverChange(
"ba7c5827-5844-4069-94fc-9b438ef08f78",
"Priya stays on refunds until 31 October.",
stepUp,
);
console.log(change.status);import os
from pathlib import Path
from parmana import ParmanaClient
from parmana.crypto import sign_policy_change_step_up
client = ParmanaClient(
endpoint="https://parmana-api-real.vercel.app",
api_key=os.environ["PARMANA_API_KEY"],
)
step_up = sign_policy_change_step_up(
pending_policy_change_id="ba7c5827-5844-4069-94fc-9b438ef08f78",
action="reject",
private_key_pem=Path("step-up.private.pem").read_text(),
key_id="checker-step-up-1",
)
change = client.reject_approver_change(
"ba7c5827-5844-4069-94fc-9b438ef08f78",
"Priya stays on refunds until 31 October.",
step_up,
)
print(change.status)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
rejectionReason: 'Does not meet the bar for this fixture',
stepUpAuthorization: {
payload: {
version: 1,
nonce: '4807fe45-0379-415b-9455-b8cb84eb5554',
pendingPolicyChangeId: '5617ba3c-fb07-437d-9131-232b84bbb863',
action: 'reject',
authorizedAt: '2026-09-15T03:48:16.755Z',
expiresAt: '2026-09-15T03:50:16.755Z'
},
signature: 'Wu5/oibGCjvkTklGQPiDMDemqxCVbSyuoVT7WHW63rGq8Av5KFTW6vMkUCP03pANv8bDu60bZVuVcTDVmLXpDA==',
keyId: 'human-checker-step-up-key',
algorithm: 'ed25519'
}
})
};
fetch('https://parmana-sandbox.vercel.app/approval-issuers/changes/{id}/reject', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://parmana-sandbox.vercel.app/approval-issuers/changes/{id}/reject",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'rejectionReason' => 'Does not meet the bar for this fixture',
'stepUpAuthorization' => [
'payload' => [
'version' => 1,
'nonce' => '4807fe45-0379-415b-9455-b8cb84eb5554',
'pendingPolicyChangeId' => '5617ba3c-fb07-437d-9131-232b84bbb863',
'action' => 'reject',
'authorizedAt' => '2026-09-15T03:48:16.755Z',
'expiresAt' => '2026-09-15T03:50:16.755Z'
],
'signature' => 'Wu5/oibGCjvkTklGQPiDMDemqxCVbSyuoVT7WHW63rGq8Av5KFTW6vMkUCP03pANv8bDu60bZVuVcTDVmLXpDA==',
'keyId' => 'human-checker-step-up-key',
'algorithm' => 'ed25519'
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://parmana-sandbox.vercel.app/approval-issuers/changes/{id}/reject"
payload := strings.NewReader("{\n \"rejectionReason\": \"Does not meet the bar for this fixture\",\n \"stepUpAuthorization\": {\n \"payload\": {\n \"version\": 1,\n \"nonce\": \"4807fe45-0379-415b-9455-b8cb84eb5554\",\n \"pendingPolicyChangeId\": \"5617ba3c-fb07-437d-9131-232b84bbb863\",\n \"action\": \"reject\",\n \"authorizedAt\": \"2026-09-15T03:48:16.755Z\",\n \"expiresAt\": \"2026-09-15T03:50:16.755Z\"\n },\n \"signature\": \"Wu5/oibGCjvkTklGQPiDMDemqxCVbSyuoVT7WHW63rGq8Av5KFTW6vMkUCP03pANv8bDu60bZVuVcTDVmLXpDA==\",\n \"keyId\": \"human-checker-step-up-key\",\n \"algorithm\": \"ed25519\"\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://parmana-sandbox.vercel.app/approval-issuers/changes/{id}/reject")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"rejectionReason\": \"Does not meet the bar for this fixture\",\n \"stepUpAuthorization\": {\n \"payload\": {\n \"version\": 1,\n \"nonce\": \"4807fe45-0379-415b-9455-b8cb84eb5554\",\n \"pendingPolicyChangeId\": \"5617ba3c-fb07-437d-9131-232b84bbb863\",\n \"action\": \"reject\",\n \"authorizedAt\": \"2026-09-15T03:48:16.755Z\",\n \"expiresAt\": \"2026-09-15T03:50:16.755Z\"\n },\n \"signature\": \"Wu5/oibGCjvkTklGQPiDMDemqxCVbSyuoVT7WHW63rGq8Av5KFTW6vMkUCP03pANv8bDu60bZVuVcTDVmLXpDA==\",\n \"keyId\": \"human-checker-step-up-key\",\n \"algorithm\": \"ed25519\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://parmana-sandbox.vercel.app/approval-issuers/changes/{id}/reject")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"rejectionReason\": \"Does not meet the bar for this fixture\",\n \"stepUpAuthorization\": {\n \"payload\": {\n \"version\": 1,\n \"nonce\": \"4807fe45-0379-415b-9455-b8cb84eb5554\",\n \"pendingPolicyChangeId\": \"5617ba3c-fb07-437d-9131-232b84bbb863\",\n \"action\": \"reject\",\n \"authorizedAt\": \"2026-09-15T03:48:16.755Z\",\n \"expiresAt\": \"2026-09-15T03:50:16.755Z\"\n },\n \"signature\": \"Wu5/oibGCjvkTklGQPiDMDemqxCVbSyuoVT7WHW63rGq8Av5KFTW6vMkUCP03pANv8bDu60bZVuVcTDVmLXpDA==\",\n \"keyId\": \"human-checker-step-up-key\",\n \"algorithm\": \"ed25519\"\n }\n}"
response = http.request(request)
puts response.read_body{
"changeId": "348e9b4c-61dc-4bef-8815-6d4488750111",
"action": "revoke",
"approverId": "manager-priya",
"keyId": "manager-priya-key-1",
"reason": "Priya moved to another team.",
"proposedBy": "human-maker",
"proposedAt": "2026-09-28T19:05:59.750Z",
"status": "REJECTED",
"resolvedBy": "human-checker",
"resolvedAt": "2026-09-28T19:05:59.753Z",
"rejectionReason": "Priya stays on refunds until 31 October."
}{
"error": "rejectionReason is required, at most 2000 characters."
}{
"error": "authentication required"
}{
"error": "Approver change 'e0292192-c75b-45eb-89e2-c14bae0f8751' was proposed by this same caller — the proposer (maker) may not also approve or reject it (checker).",
"code": "SAME_ACTOR_CANNOT_APPROVE_OWN_CHANGE"
}{
"error": "Approver change '7d5f8a52-0000-4000-8000-000000000000' not found.",
"code": "APPROVAL_ISSUER_CHANGE_NOT_FOUND"
}{
"error": "Approver change 'e0292192-c75b-45eb-89e2-c14bae0f8751' is already APPROVED.",
"code": "CONFLICT"
}Authorizations
Caller API key issued by scripts/generate-api-key.ts. Sent as Authorization: Bearer . Verified against a stored SHA-256 hash in constant time by packages/api/src/auth/StaticKeyAuthenticator.ts. Required on every route not listed as exempt in this document's top-level description. See /api-reference/authentication.
Path Parameters
The changeId from the proposal.
Body
Request body for POST /policies/pending-changes/{id}/reject.
Free-text reason. Required, non-empty.
Signed envelope proving a checker's explicit, fresh intent to approve or reject one specific Pending Policy Change (Policy Governance, Layer 4). Produced by PolicyChangeStepUpAuthorizationSigner (@parmana/crypto) using the checker's own step-up private key, never the bearer API key. Verified server-side against: the checker's registered stepUpPublicKey, payload.pendingPolicyChangeId matching the URL's {id}, payload.action matching the endpoint (approve vs reject), payload.expiresAt not yet passed, and payload.nonce not previously seen (single-use, replay-rejected on a second attempt with the same envelope).
Show child attributes
Show child attributes
{
"payload": {
"version": 1,
"nonce": "343ee454-92c6-4ef9-9cfe-e98329e51f53",
"pendingPolicyChangeId": "6f4e4020-adf5-4449-938d-27aab58f66b7",
"action": "approve",
"authorizedAt": "2026-09-15T03:47:51.939Z",
"expiresAt": "2026-09-15T03:49:51.939Z"
},
"signature": "keOXnzUk8KcGrQTkpXC4Ki8ps3GTvMANwqq96b+toxba07gjkcmpyL/vaPZ7+pPZ1HAOJOOy3rj5XfhcwOTnAw==",
"keyId": "human-checker-step-up-key",
"algorithm": "ed25519"
}
Response
Rejected. No key changed.
Response of POST /approval-issuers/changes (201) and of approve and reject (200). A bare Approver Change, no wrapper.
Unique id of the change, a UUID. The step up authorization for approve or reject names it in payload.pendingPolicyChangeId.
add trusts a new key; revoke stops trusting a key added this way, and every approval it ever signed.
add, revoke The approver, as named in the payload.issuer.approverId of the approvals they sign.
^[A-Za-z0-9._-]{1,128}$The approver's key, as named in payload.issuer.keyId. A key id is used once: a revoked key id cannot be added again.
^[A-Za-z0-9._-]{1,128}$Why, from the proposer.
2000The proposer's caller id. Always a human credential.
PENDING_APPROVAL until a second person approves or rejects it; then APPROVED or REJECTED, once.
PENDING_APPROVAL, APPROVED, REJECTED The approver's Ed25519 public key, PEM (SPKI), as the server stored it. Present on add, absent on revoke.
Who approved or rejected it. Never the proposer.
Present when REJECTED.
# Sign the step up authorization on your own machine first:
# npx tsx scripts/sign-policy-change-step-up.ts --private-key-file step-up.private.pem \
# --key-id checker-step-up-1 --pending-policy-change-id ba7c5827-5844-4069-94fc-9b438ef08f78 --action reject > step-up.json
curl -X POST https://parmana-api-real.vercel.app/approval-issuers/changes/ba7c5827-5844-4069-94fc-9b438ef08f78/reject \
-H "Authorization: Bearer $PARMANA_API_KEY" \
-H "Content-Type: application/json" \
--data "{\"rejectionReason\": \"Priya stays on refunds until 31 October.\", \"stepUpAuthorization\": $(cat step-up.json)}"import { ParmanaClient, signPolicyChangeStepUp } from "@parmana/sdk";
import { readFileSync } from "node:fs";
const client = new ParmanaClient({
endpoint: "https://parmana-api-real.vercel.app",
apiKey: process.env.PARMANA_API_KEY!,
});
const stepUp = signPolicyChangeStepUp({
pendingPolicyChangeId: "ba7c5827-5844-4069-94fc-9b438ef08f78",
action: "reject",
privateKeyPem: readFileSync("step-up.private.pem", "utf8"),
keyId: "checker-step-up-1",
});
const change = await client.rejectApproverChange(
"ba7c5827-5844-4069-94fc-9b438ef08f78",
"Priya stays on refunds until 31 October.",
stepUp,
);
console.log(change.status);import os
from pathlib import Path
from parmana import ParmanaClient
from parmana.crypto import sign_policy_change_step_up
client = ParmanaClient(
endpoint="https://parmana-api-real.vercel.app",
api_key=os.environ["PARMANA_API_KEY"],
)
step_up = sign_policy_change_step_up(
pending_policy_change_id="ba7c5827-5844-4069-94fc-9b438ef08f78",
action="reject",
private_key_pem=Path("step-up.private.pem").read_text(),
key_id="checker-step-up-1",
)
change = client.reject_approver_change(
"ba7c5827-5844-4069-94fc-9b438ef08f78",
"Priya stays on refunds until 31 October.",
step_up,
)
print(change.status)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
rejectionReason: 'Does not meet the bar for this fixture',
stepUpAuthorization: {
payload: {
version: 1,
nonce: '4807fe45-0379-415b-9455-b8cb84eb5554',
pendingPolicyChangeId: '5617ba3c-fb07-437d-9131-232b84bbb863',
action: 'reject',
authorizedAt: '2026-09-15T03:48:16.755Z',
expiresAt: '2026-09-15T03:50:16.755Z'
},
signature: 'Wu5/oibGCjvkTklGQPiDMDemqxCVbSyuoVT7WHW63rGq8Av5KFTW6vMkUCP03pANv8bDu60bZVuVcTDVmLXpDA==',
keyId: 'human-checker-step-up-key',
algorithm: 'ed25519'
}
})
};
fetch('https://parmana-sandbox.vercel.app/approval-issuers/changes/{id}/reject', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://parmana-sandbox.vercel.app/approval-issuers/changes/{id}/reject",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'rejectionReason' => 'Does not meet the bar for this fixture',
'stepUpAuthorization' => [
'payload' => [
'version' => 1,
'nonce' => '4807fe45-0379-415b-9455-b8cb84eb5554',
'pendingPolicyChangeId' => '5617ba3c-fb07-437d-9131-232b84bbb863',
'action' => 'reject',
'authorizedAt' => '2026-09-15T03:48:16.755Z',
'expiresAt' => '2026-09-15T03:50:16.755Z'
],
'signature' => 'Wu5/oibGCjvkTklGQPiDMDemqxCVbSyuoVT7WHW63rGq8Av5KFTW6vMkUCP03pANv8bDu60bZVuVcTDVmLXpDA==',
'keyId' => 'human-checker-step-up-key',
'algorithm' => 'ed25519'
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://parmana-sandbox.vercel.app/approval-issuers/changes/{id}/reject"
payload := strings.NewReader("{\n \"rejectionReason\": \"Does not meet the bar for this fixture\",\n \"stepUpAuthorization\": {\n \"payload\": {\n \"version\": 1,\n \"nonce\": \"4807fe45-0379-415b-9455-b8cb84eb5554\",\n \"pendingPolicyChangeId\": \"5617ba3c-fb07-437d-9131-232b84bbb863\",\n \"action\": \"reject\",\n \"authorizedAt\": \"2026-09-15T03:48:16.755Z\",\n \"expiresAt\": \"2026-09-15T03:50:16.755Z\"\n },\n \"signature\": \"Wu5/oibGCjvkTklGQPiDMDemqxCVbSyuoVT7WHW63rGq8Av5KFTW6vMkUCP03pANv8bDu60bZVuVcTDVmLXpDA==\",\n \"keyId\": \"human-checker-step-up-key\",\n \"algorithm\": \"ed25519\"\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://parmana-sandbox.vercel.app/approval-issuers/changes/{id}/reject")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"rejectionReason\": \"Does not meet the bar for this fixture\",\n \"stepUpAuthorization\": {\n \"payload\": {\n \"version\": 1,\n \"nonce\": \"4807fe45-0379-415b-9455-b8cb84eb5554\",\n \"pendingPolicyChangeId\": \"5617ba3c-fb07-437d-9131-232b84bbb863\",\n \"action\": \"reject\",\n \"authorizedAt\": \"2026-09-15T03:48:16.755Z\",\n \"expiresAt\": \"2026-09-15T03:50:16.755Z\"\n },\n \"signature\": \"Wu5/oibGCjvkTklGQPiDMDemqxCVbSyuoVT7WHW63rGq8Av5KFTW6vMkUCP03pANv8bDu60bZVuVcTDVmLXpDA==\",\n \"keyId\": \"human-checker-step-up-key\",\n \"algorithm\": \"ed25519\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://parmana-sandbox.vercel.app/approval-issuers/changes/{id}/reject")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"rejectionReason\": \"Does not meet the bar for this fixture\",\n \"stepUpAuthorization\": {\n \"payload\": {\n \"version\": 1,\n \"nonce\": \"4807fe45-0379-415b-9455-b8cb84eb5554\",\n \"pendingPolicyChangeId\": \"5617ba3c-fb07-437d-9131-232b84bbb863\",\n \"action\": \"reject\",\n \"authorizedAt\": \"2026-09-15T03:48:16.755Z\",\n \"expiresAt\": \"2026-09-15T03:50:16.755Z\"\n },\n \"signature\": \"Wu5/oibGCjvkTklGQPiDMDemqxCVbSyuoVT7WHW63rGq8Av5KFTW6vMkUCP03pANv8bDu60bZVuVcTDVmLXpDA==\",\n \"keyId\": \"human-checker-step-up-key\",\n \"algorithm\": \"ed25519\"\n }\n}"
response = http.request(request)
puts response.read_body{
"changeId": "348e9b4c-61dc-4bef-8815-6d4488750111",
"action": "revoke",
"approverId": "manager-priya",
"keyId": "manager-priya-key-1",
"reason": "Priya moved to another team.",
"proposedBy": "human-maker",
"proposedAt": "2026-09-28T19:05:59.750Z",
"status": "REJECTED",
"resolvedBy": "human-checker",
"resolvedAt": "2026-09-28T19:05:59.753Z",
"rejectionReason": "Priya stays on refunds until 31 October."
}{
"error": "rejectionReason is required, at most 2000 characters."
}{
"error": "authentication required"
}{
"error": "Approver change 'e0292192-c75b-45eb-89e2-c14bae0f8751' was proposed by this same caller — the proposer (maker) may not also approve or reject it (checker).",
"code": "SAME_ACTOR_CANNOT_APPROVE_OWN_CHANGE"
}{
"error": "Approver change '7d5f8a52-0000-4000-8000-000000000000' not found.",
"code": "APPROVAL_ISSUER_CHANGE_NOT_FOUND"
}{
"error": "Approver change 'e0292192-c75b-45eb-89e2-c14bae0f8751' is already APPROVED.",
"code": "CONFLICT"
}