Documentation
Sign up to get an API keyVerifying signed decisions
Every AI Agent Guardrails authorization decision is logged and, when signing keys are configured, signed with ML-DSA-65 (NIST FIPS 204). The signature is attestation only — it is produced after the deterministic decision and never influences it. If signing is unavailable the decision is still made and logged, with the signature fields left null.
Canonical payload
What gets signed is an exact JSON string with a fixed key order — stored verbatim as signed_payload. Always verify against that stored string; never rebuild it from the individual columns.
{
"schema": "guard.decision.v1",
"user_id": "...",
"agent": "payments-agent",
"action": "create_payout",
"resource": "acct:1234",
"context": { "amount_usd": 2500 },
"decision": "deny",
"reason": "No policy grants this action (default-deny).",
"obligations": [],
"matched_policy_id": null,
"decided_at": "2026-01-01T00:00:00.000Z"
}Fetch the public verification key
curl https://akrum.io/api/public/v1/guard/verification-keyReturns { suite: "mldsa65-v1", public_key_b64 }. The public key is safe to publish; the signing key never leaves AKRUM.
Export the decision log
curl "https://akrum.io/api/public/v1/guard/decisions?limit=-n">100" \
-H "Authorization: Bearer $AKRUM_KEY"
-c"># verify a single decision server-side
curl -X POST https://akrum.io/api/public/v1/guard/decisions/<decision_id>/verify \
-H "Authorization: Bearer $AKRUM_KEY"Both endpoints require the guard scope and only ever return your own rows. Each exported decision carries its signed_payload, signature, signature_suite, message_sha256 and signed_at, so the export is independently verifiable without calling AKRUM again.
Offline verification
import { ml_dsa65 } from "@noble/post-quantum/ml-dsa.js";
const b64 = (s: string) => Uint8Array.from(Buffer.from(s, "base64"));
class="tk-c">// publicKeyB64 from /v1/guard/verification-key
class="tk-c">// decision from /v1/guard/decisions
const valid = ml_dsa65.verify(
b64(decision.signature),
new TextEncoder().encode(decision.signed_payload), class="tk-c">// the stored string, byte for byte
b64(publicKeyB64),
);
console.log(valid ? "authentic" : "tampered or unsigned");Change a single character of signed_payload and verification fails — that is what makes the log tamper-evident. An unsigned row (no signature) verifies as { valid: false, reason: "unsigned" }.
