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/sdkpackage 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
| Field | Type | Description |
|---|---|---|
decision | "ALLOW" | "DENY" | The authorization decision |
mandate_id | string? | The mandate that matched (present for ALLOW and some DENY reasons) |
event_id | string | Unique audit event ID for this verification |
reason_codes | string[]? | Why the request was denied (only on DENY) |
constraints_remaining | object? | Remaining constraint details (e.g., escalation target) |
Deny Reason Codes
| Code | Meaning |
|---|---|
NO_MATCHING_MANDATE | No active mandate matches the delegate/action/resource combination |
APPROVAL_REQUIRED | A matching mandate exists but is pending approval |
MFA_REQUIRED | Mandate requires multi-factor authentication proof in the context |
ESCALATION_REQUIRED | Action 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
expclaim 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) | Canonical | Notes |
|---|---|---|
agent_id | delegate_subject_id | Server accepts both. If both are sent, delegate_subject_id takes precedence. |
Response Fields
| Legacy (demo) | Canonical | Notes |
|---|---|---|
proof_token | proof_of_mandate.compact | Response includes both when PoM is requested. proof_token is the raw SD-JWT string. |
Migration Steps
- No immediate action required — existing integrations using
agent_idor readingproof_tokenwill continue to work. - Update request payloads at your convenience: replace
agent_idwithdelegate_subject_id. - Update response handling: switch from
proof_tokentoproof_of_mandate.compact(which also gives you access toproof_of_mandate.payloadwith decoded claims). - Legacy aliases will be removed in v2.0.0.