Skip to Content
AgentFlow
AI agents

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.

FieldMeaning
outcomeDecision outcome: approve | reject (decision verbs, not request statuses)
proofStatus"signed" for delivered receipts
proofIdProof ID; id is rcp_<proofId>
verificationRuleAlways the literal "local-ed25519"
referenceIdAUTH-<externalRequestId> — ties the receipt to your externalRequestId
requestHashThe contentHash you submitted — confirms the signed request is the content you sent
decisionHashSHA-256 hex of the signed payload (see below)
signatureEd25519 signature, base64url
keyIdWhich 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"}'
  • outcome inside the payload is the decision verb (approve, reject), matching the receipt's outcome field.
  • decisionHash = SHA-256(payload) as a bare hex string (no sha256: prefix).
  • requestHash = the request's contentHash, 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 — reject or expiry — is not, even though the receipt itself still verifies (it proves that outcome, no more).
  • verificationRule is always "local-ed25519"; if a receipt claims anything else, reject it.
  • Always re-check getStatus if 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.

On this page