Developers
From zero to your first verification in under ten minutes.
Three calls, no account: issue a sandbox mandate, create a presentation, verify. Every answer carries a signed receipt and exactly one primary code — deterministic and machine-readable.
Playground — real calls
The whole lifecycle in five clicks.
Antworten erscheinen hier — echte Calls gegen /api/v1/*.
Quickstart · cURL
# 1 · Issue a sandbox mandate (no sign-up)
curl -X POST https://mandact.com/api/v1/sandbox/mandates \
-H "content-type: application/json" \
-d '{"scope":["purchase.goods"],"amount_limit_per_action":500}'
# 2 · Create a presentation (sandbox convenience; in production the agent wallet does this)
curl -X POST https://mandact.com/api/v1/sandbox/presentations \
-H "content-type: application/json" \
-d '{"mandate_id":"mnd_sbx_…","agent_secret":"…"}'
# 3 · Verify
curl -X POST https://mandact.com/api/v1/verify \
-H "content-type: application/json" \
-d '{"presentation":{…},"action":{"type":"purchase.goods","amount":{"value":120,"currency":"CHF"}}}'Quickstart · TypeScript SDK
import { Mandact } from "@mandact/sdk";
const mandact = new Mandact(process.env.MANDACT_KEY);
// Sandbox round trip
const mandate = await mandact.sandbox.issueMandate({
scope: ["purchase.goods"],
amountLimitPerAction: 500,
});
const decision = await mandact.verify({
presentation,
action: { type: "purchase.goods", amount: { value: 120, currency: "CHF" } },
});
// Exactly one primary code per answer — deterministic, machine-readable.
if (decision.result !== "allow") console.log(decision.primary_code);Recommended · wrap your tools
The check you cannot forget.
With a direct call you have to place the check in the right spot and build the intent hash yourself. Both get forgotten — and that is exactly where the gaps appear. guard() wraps your function: the hash comes from the arguments it runs with anyway, and the real function never starts before the answer is in.
import { guard, MandactDenied, MandactEscalated } from "@mandact/sdk";
// The real function never starts before the answer is in.
const orderGoods = guard(mandact, "purchase.goods", async (cart) => {
return await supplier.placeOrder(cart);
});
try {
await orderGoods(cart);
} catch (e) {
if (e instanceof MandactEscalated) {
// Not forbidden — it is waiting for a human. Retry later.
await createTicket(e.answer.escalation_id);
} else {
throw e; // MandactDenied: the action was not covered.
}
}MandactDenied and MandactEscalated are deliberately different errors: «drop it» and «wait for a human» call for different reactions. Treating both the same way builds either an infinite loop or gives up too early.
Denial and escalation codes
Stable, versioned, one primary code per answer.
| Group | Area | Codes |
|---|---|---|
| MD-1xx | Signature / format | 101 invalid_signature · 102 malformed_presentation · 104 stale_pop (PoP older than 60 s) |
| MD-2xx | Status | 201 revoked · 202 suspended · 203 expired · 204 consumed · 205 not_yet_valid |
| MD-3xx | Scope / constraints | 301 action_out_of_scope · 302 amount_exceeds_limit · 303 period_limit_exhausted · 304 counterparty_denied |
| MD-4xx | Escalation | 401 escalation_required — step-up to the principal, timeout 600 s |
| MD-5xx | Verifier policy | 502 attestation_required |
The check order is fixed: signature → status (live, no-store) → time window → proof-of-possession freshness → scope → limits (reserved atomically) → counterparty → verifier policy. Escalations are raised after all checks.