Skip to main content

Verifying Actions

The verification endpoint is the performance-critical core of Mandaitor. It allows any service to check in real-time whether an AI agent is authorized to perform a specific action.

Prerequisites

  • A Mandaitor tenant account with an API key
  • The @mandaitor/sdk package installed
  • At least one active mandate created for the delegate you're verifying

Basic Verification

import { MandaitorClient } from "@mandaitor/sdk";

const client = new MandaitorClient({
apiKey: process.env.MANDAITOR_API_KEY!,
tenantId: "tnt_your_tenant_id",
});

const result = await client.verify({
delegate_subject_id: "monco:agent:validation-v3",
action: "construction.validation.approve",
resource: "monco:project:proj_12345/zone:A",
});

if (result.decision === "ALLOW") {
// Proceed with the action
console.log("Authorized via mandate:", result.mandate_id);
} else {
// Action denied
console.log("Denied:", result.reason_codes);
}

Understanding the Response

FieldTypeDescription
decision"ALLOW" | "DENY"The authorization decision
mandate_idstring?The mandate that matched (present for ALLOW and some DENY reasons)
event_idstringUnique audit event ID for this verification
reason_codesstring[]?Why the request was denied (only on DENY)
constraints_remainingobject?Remaining constraint details (e.g., escalation target)

Deny Reason Codes

CodeMeaning
NO_MATCHING_MANDATENo active mandate matches the delegate/action/resource combination
APPROVAL_REQUIREDA matching mandate exists but is pending approval
MFA_REQUIREDMandate requires multi-factor authentication proof in the context
ESCALATION_REQUIREDAction triggered an escalation rule (e.g., cost deviation above threshold)

Verification with Proof-of-Mandate

To receive a cryptographic proof alongside the verification result, request a Proof-of-Mandate (PoM) Verifiable Credential:

const result = await client.verifyWithPoM(
{
delegate_subject_id: "monco:agent:validation-v3",
action: "construction.validation.approve",
resource: "monco:project:proj_12345/zone:A",
},
{ pom: "sd-jwt-vc" },
);

if (result.proof_of_mandate) {
// SD-JWT compact string — can be stored, forwarded, or verified independently
console.log(result.proof_of_mandate.compact);

// Also available as a convenience alias
console.log(result.proof_token);
}

The PoM is an SD-JWT VC signed by the Mandaitor issuer DID. It can be independently verified without calling the Mandaitor API, which is useful for audit trails and cross-system trust chains.

Verifying a Proof-of-Mandate

Use the SDK's built-in verifier to validate a PoM token:

import { verifyProofOfMandate } from "@mandaitor/sdk";

const result = await verifyProofOfMandate(pomCompactString, {
audience: "https://your-service.example.com", // optional
});

if (result.valid) {
console.log("PoM is valid:", result.claims);
console.log("Disclosures:", result.disclosures);
} else {
console.log("Invalid PoM:", result.errors);
}

The verifier performs:

  • DID resolution (did:web → HTTPS .well-known/did.json)
  • SD-JWT signature verification (ES256, ES384, RS256)
  • Claim validation (issuer, expiry, issued-at, audience)
  • Selective disclosure extraction

Security Considerations

  • PoM tokens have an expiry — verify the exp claim before trusting
  • Always verify the issuer DID matches did:web:api.mandaitor.io
  • Use skipSignatureVerification: false (the default) in production
  • Store PoM tokens in your audit log for court-ready evidence

MFA-Protected Mandates

Mandates with constraints.require_mfa = true require multi-factor authentication proof in the verification context. Without it, the verify endpoint returns DENY with reason MFA_REQUIRED.

// Passes MFA proof via the context object
const result = await client.verify({
delegate_subject_id: "agent:compliance-bot",
action: "healthcare.prescription.issue",
resource: "clinic:org_1/patient:pat_123/prescription:*",
context: {
amr: ["pwd", "mfa"], // Authentication Methods Reference
loa: "HIGH", // Level of Assurance
},
});

Accepted MFA proof formats in context:

  • amr: string[] — Must include "mfa" (standard OIDC claim)
  • loa: string — Must be "SUBSTANTIAL" or "HIGH" (eIDAS LoA)
  • mfa_timestamp: string — ISO 8601 timestamp of last MFA verification

Approval Workflow Integration

When a mandate requires approval before activation, the verify endpoint distinguishes between "no mandate exists" and "mandate exists but is pending approval":

const result = await client.verify({
delegate_subject_id: "agent:procurement-bot",
action: "construction.procurement.create_order",
resource: "monco:project:proj_789/*",
});

if (result.decision === "DENY") {
if (result.reason_codes?.includes("APPROVAL_REQUIRED")) {
// A matching mandate exists but hasn't been approved yet
console.log("Pending mandate:", result.mandate_id);
// Show user: "This action requires approval from a manager"
} else if (result.reason_codes?.includes("NO_MATCHING_MANDATE")) {
// No mandate at all — delegate needs to request one
}
}

Evidence Pack Export

For audit and compliance, export a court-ready evidence pack containing the mandate snapshot, hash-chained event log, and proof tokens:

const evidencePack = await client.getEvidencePack(
"mdt_01HXYZ...", // mandate ID
"evt_01HABC...", // optional: scope to a specific event
);

console.log(evidencePack.schema_version); // "1.0.0"
console.log(evidencePack.case_log_hash); // SHA-256 of the event chain
console.log(evidencePack.issuer_did); // DID for independent verification
console.log(evidencePack.mandate_snapshot); // Full mandate at time of export
console.log(evidencePack.event_chain.length); // Hash-chained audit events

Error Handling

import { MandaitorClient, MandaitorApiError } from "@mandaitor/sdk";

try {
await client.verify({
/* ... */
});
} catch (err) {
if (err instanceof MandaitorApiError) {
switch (err.status) {
case 400:
console.error("Invalid request:", err.message);
break;
case 401:
console.error("Invalid API key");
break;
case 429:
console.error("Rate limited, retry after:", err.retryAfterMs, "ms");
break;
}
}
}

Migrating from Demo/Legacy Field Names

If you integrated with Mandaitor using demo code or early prototypes, you may be using legacy field names. The API now supports both canonical and legacy names for backwards compatibility.

Request Fields

Legacy (demo)CanonicalNotes
agent_iddelegate_subject_idServer accepts both. If both are sent, delegate_subject_id takes precedence.

Response Fields

Legacy (demo)CanonicalNotes
proof_tokenproof_of_mandate.compactResponse includes both when PoM is requested. proof_token is the raw SD-JWT string.

Migration Steps

  1. No immediate action required — existing integrations using agent_id or reading proof_token will continue to work.
  2. Update request payloads at your convenience: replace agent_id with delegate_subject_id.
  3. Update response handling: switch from proof_token to proof_of_mandate.compact (which also gives you access to proof_of_mandate.payload with decoded claims).
  4. Legacy aliases will be removed in v2.0.0.