Skip to main content

Verifying Actions

POST /verify answers whether an agent may perform an action on a resource. The answer holds for any request that matches its action and resource, and the acting is left to the caller: the agent, or the service in front of it, then acts with its own credentials. POST /verify is Stable, and this page describes it in full.

Where Mandaitor acts instead

Mandaitor's own model goes further. With executions, the agent asks Mandaitor to perform the operation: Mandaitor reads the record, decides on what it read, writes with a credential the agent never holds, and signs a receipt of the outcome. When your own system must perform the operation, request-bound decisions give its enforcement point an authorization for that one request, redeemed once before it acts. Both are in Preview.

POST /verifyRequest-bound decisionsExecutions
Who actsThe callerYour systemMandaitor
Decided onThe action, resource and context the caller sendsThe facts your system readThe facts Mandaitor read from the record
The answer coversAny request that matchesOne request, redeemed onceOne request, which Mandaitor performs
EvidenceA decision, and optionally a Proof-of-MandateA decision and its redemptionA Proof-of-Mandate and a receipt

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: "construction:agent:validation-v3",
action: "construction.validation.approve",
resource: "construction: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: "construction:agent:validation-v3",
action: "construction.validation.approve",
resource: "construction: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: "construction: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.