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)

Playground Verification

Dashboard and tenant applications can use the Cognito-protected playground endpoint for dry-run verification without exposing raw API keys in the browser:

POST /tenants/{id}/playground/verify
Authorization: Bearer <cognito-jwt>
Content-Type: application/json

The request body follows the same VerifyRequest shape as POST /verify. The endpoint is intended for interactive testing and onboarding screens; production workloads should continue to call POST /verify from a trusted server or SDK integration.

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
}
}

Verifiable User Instruction Retrieval

Mandates can store an encrypted Verifiable User Instruction (VUI) that explains the original user intent, confirmation method, channel, and evidence pointers. Authorized clients can retrieve the decrypted instruction when they need to render an audit review or produce an evidence bundle:

const instruction = await client.getMandateInstruction("mdt_abc123");
console.log(instruction.instruction.content_hash);

The corresponding API endpoint is GET /mandates/{id}/instruction. Access should remain limited to trusted review surfaces because instruction content may include sensitive user intent.

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

Tenant-Wide Event Retrieval and SIEM Export

Mandate-specific events are available through GET /mandates/{id}/events. Tenant operators can also query the consolidated audit stream with GET /tenants/{id}/events, which is what dashboard and compliance views should use when they need cross-mandate review. For downstream governance, risk, compliance, and SIEM tooling, authorized tenant auditors can export a tenant-scoped JSON audit page from GET /tenants/{id}/audit-export.

The export endpoint requires the events:export permission and is constrained to the authenticated tenant. Each successful export emits a TENANT_AUDIT_EXPORTED audit event, which records the export window, returned event count, and generated export identifier without recursively including prior export events in the payload.

Use caseEndpointNotes
Mandate timelineGET /mandates/{id}/eventsShows events scoped to one mandate.
Tenant audit streamGET /tenants/{id}/eventsLists events across the tenant for dashboard and compliance review workflows.
Tenant audit/SIEM exportGET /tenants/{id}/audit-exportReturns an attachment-style JSON page for SIEM ingestion; supports limit, cursor, event_type, from_timestamp, and to_timestamp.
Single event detailGET /events/{eventId}Retrieves one audit event by ID.
curl -H "Authorization: Bearer $MANDAITOR_TOKEN" \
"https://api.mandaitor.io/v1/tenants/tnt_demo/audit-export?from_timestamp=2026-04-01T00:00:00.000Z&to_timestamp=2026-04-30T23:59:59.999Z&limit=500" \
-o mandaitor-audit-export.json
Query parameterDefaultDescription
limit100Number of events to return, capped at 500 for export workloads.
cursornoneOpaque pagination cursor returned as next_cursor when another export page is available.
event_typenoneOptional audit-event filter, for example VERIFICATION_DENIED or MANDATE_REVOKED.
from_timestampnoneInclusive ISO 8601 lower bound for the event timestamp.
to_timestampnoneInclusive ISO 8601 upper bound for the event timestamp.

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.