Receipts
Fetch the signed approval receipt with getProof and verify its Ed25519 signature independently.
Every finalized decision produces an Ed25519-signed authority proof.
The agent-facing view of it is the receipt returned by getProof —
verify it before treating an approved decision as authorization.
getProof
curl -s https://af-api.ivalt.com/api/v1/ai-agent/getProof \
-H 'x-ai-agent-key: <agent-key>' \
-H 'content-type: application/json' \
-d '{"json": {"requestId": "apr_1234_v1"}}'{
"json": {
"id": "rcp_prf_9d3a",
"createdAt": "2026-11-10T14:23:11.041Z",
"updatedAt": "2026-11-10T14:23:11.041Z",
"deletedAt": null,
"meta": {},
"requestId": "apr_1234_v1",
"requestVersion": 1,
"decisionId": "di_0021",
"outcome": "approve",
"proofStatus": "signed",
"proofId": "prf_9d3a",
"verificationRule": "local-ed25519",
"issuedAt": "2026-11-10T14:23:11.041Z",
"referenceId": "AUTH-REQ-1001",
"requestHash": "sha256:2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
"decisionHash": "4b227777d4dd1fc61c6f884f48641d02b4d121d3fd328cb08b5531fcacdabf8a",
"signature": "3yZe7d…base64url…",
"keyId": "authority-key-local"
}
}Errors: not_found when the request is unknown or no proof exists
yet (i.e. the decision isn't final). The receiptId returned by
getStatus and the approval.completed webhook event is the bare
proofId.
| Field | Meaning |
|---|---|
outcome | Decision outcome: approve | reject (decision verbs, not request statuses) |
proofStatus | "signed" for delivered receipts |
proofId | Proof ID; id is rcp_<proofId> |
verificationRule | Always the literal "local-ed25519" |
referenceId | AUTH-<externalRequestId> — ties the receipt to your externalRequestId |
requestHash | The contentHash you submitted — confirms the signed request is the content you sent |
decisionHash | SHA-256 hex of the signed payload (see below) |
signature | Ed25519 signature, base64url |
keyId | Which platform signing key produced the proof |
What is signed
The signature covers exactly this JSON-serialized payload, produced at finalization:
const payload = JSON.stringify({
requestId: 'apr_1234_v1',
decisionId: 'di_0021',
outcome: 'approve',
});
// '{"requestId":"apr_1234_v1","decisionId":"di_0021","outcome":"approve"}'outcomeinside the payload is the decision verb (approve,reject), matching the receipt'soutcomefield.decisionHash=SHA-256(payload)as a bare hex string (nosha256:prefix).requestHash= the request'scontentHash, verbatim — if you hashed canonical action content at submit time, you can confirm the receipt covers that same content.
Verification recipe
A third party needs: the receipt, the request ID, the decision ID,
the outcome, and the public key for keyId. Public keys are
distributed out-of-band by the AgentFlow team — there is no public
key endpoint. Ask your operator contact for the verification key for
your environment.
import {
createHash,
createPublicKey,
verify,
} from 'node:crypto';
// Rebuild the exact signed payload — property order matters.
const payload = JSON.stringify({
requestId: receipt.requestId,
decisionId: receipt.decisionId,
outcome: receipt.outcome,
});
// 1. decisionHash binds the receipt to that payload.
const decisionHashOk =
receipt.decisionHash ===
createHash('sha256').update(payload).digest('hex');
// 2. Ed25519 signature over the payload.
const signatureOk = verify(
null,
Buffer.from(payload),
createPublicKey(operatorSuppliedPublicKeyPem),
Buffer.from(receipt.signature, 'base64url'),
);
const verified =
decisionHashOk &&
signatureOk &&
receipt.referenceId === `AUTH-${yourExternalRequestId}` &&
receipt.requestHash === yourSubmittedContentHash;Interpretation rules
outcome: "approve"+ a valid signature is authorization to perform the protected action. Any other final outcome —rejector expiry — is not, even though the receipt itself still verifies (it proves that outcome, no more).verificationRuleis always"local-ed25519"; if a receipt claims anything else, reject it.- Always re-check
getStatusif the receipt was delivered via webhook — webhook delivery is at-least-once and a newer version of the request may exist since the event was queued.