TypeScript SDK
@mnd8t/sdk — a typed client for the mnd8t authority API. No runtime
dependencies; receipt and artifact verification run on WebCrypto, so they
work in Node, Deno, Bun, edge runtimes and the browser.
For the integration narrative, read the developer guide. This page is the method-by-method reference.
Install
pnpm add @mnd8t/sdk
import { MandateClient } from "@mnd8t/sdk";
const mandate = new MandateClient({
apiKey: process.env.MANDATE_API_KEY!,
});
Client options
| Option | Type | Default | Notes |
|---|---|---|---|
apiKey |
string |
— | Required. Throws if empty. |
baseUrl |
string |
http://localhost:4000 |
Your API host. Trailing slash is trimmed. |
timeoutMs |
number |
15000 |
Per-request timeout. |
maxRetries |
number |
2 |
Applies to safe requests only — see retries. |
fetch |
typeof fetch |
global | Inject your own for tests or proxies. |
Never put a live key in browser code. The SDK will happily run there for receipt verification, but an API key in a bundle is a key you have given away.
Decisions
The endpoint that matters. Call it before touching a wallet or provider.
decisions.authorize(request)
const decision = await mandate.decisions.authorize({
agent_external_id: "procurement_agent_12",
external_reference: "INV-2026-0042",
action_type: "PAYMENT",
amount: { asset: "USDC", asset_amount_minor: 84_000_000 },
valuation: {
policy_amount_minor: 50_000,
policy_currency: "GBP",
source: "CUSTOMER_SUPPLIED",
rate_reference: "provider_quote_123",
},
counterparty_external_id: "supplier_acme_cloud",
destination: "0x1111111111111111111111111111111111111111",
purpose: "cloud_infrastructure",
});
Returns AuthorizeResponse:
{
id: string;
mode: "SHADOW" | "ENFORCE";
decision: "APPROVE" | "REJECT" | "ESCALATE" | "OBSERVE";
effective_decision: same;
would_decide: "APPROVE" | "REJECT" | "ESCALATE";
reason_codes: string[];
mandate_id: string | null;
mandate_version: number | null;
approval: { id, status, expires_at, approval_url } | null;
authorisation_artifact: { id, status, expires_at, download_url } | null;
evidence_receipt_id: string | null;
authority_record?: {
receipt_id: string;
receipt_version: string;
action_hash: string; // sha256: digest of the proposed action
key_id: string;
url: string; // GET the signed record
verify_url: string; // POST a presented record to verify it
};
}
An idempotency key is generated per call. Override it with
idempotencyKey to make a retry collide deliberately — derive it from
something stable in your domain:
await mandate.decisions.authorize({
...payload,
idempotencyKey: `invoice-${invoice.id}`,
});
Amounts are integer minor units. 50_000 is £500.00. The flat form
(amount_minor, policy_amount_minor, policy_currency, asset) is still
accepted but deprecated; prefer amount + valuation.
decisions.confirm(intentId, body)
Reports an execution that you performed. mnd8t does not execute.
await mandate.decisions.confirm(decision.id, {
provider: "MY_PROVIDER",
external_reference: txRef,
authorisation_artifact_id: artifact.id, // required in enforce mode
reported_by: "CUSTOMER_EXECUTOR",
verification: { method: "PROVIDER_LOOKUP", verified: true },
});
Consumes the budget reservation once. Repeating with the same
external_reference is idempotent and returns duplicate: true.
decisions.cancel(intentId, reason?)
Releases the reservation, expires any pending approval, cancels live artifacts.
decisions.projectAp2(intentId, { cnf, ttl_seconds? })
POST /v1/decision-intents/{id}/ap2-open-payment-mandate, on deployments
with ENABLE_AP2_MAPPING. It turns an approved decision into AP2 v0.2 Open
Payment Mandate content for your AP2 Trusted Surface or Trusted Agent
Provider to sign; mnd8t does not sign it.
cnf binds the mandate to the agent's public key: an RFC 7800 claim
carrying jwk, jwe, jku or kid. The result is typed Ap2Projection:
mandate_content, content_digest, authorisation_artifact_id and
source.
The call spends the approval. It claims the decision's authorisation
artifact, so it succeeds once per decision, and the SDK never retries it.
Report the AP2 payment's outcome with decisions.confirm (passing
authorisation_artifact_id) or artifacts.fail. If the call fails after
the claim, for example on a timeout, the content cannot be fetched again.
Call artifacts.fail on the claimed artifact (decisions.get shows it) and
request a new decision. See
api.md for what it refuses.
decisions.get(intentId) · decisions.list(params?)
get returns the decision with its rule-by-rule evaluations trace,
approvals, executions, artifacts and receipts.
list accepts { status?, decision?, limit? }.
Authorisation artifacts
The signed, single-use token that lets your system execute an approved action.
artifacts.get(artifactId)
Returns the envelope, including the signed artifact object.
artifacts.verify(artifact, options?)
Verifies the Ed25519 signature locally. Do this before acting on an artifact — it is the entire point of the signature.
const envelope = await mandate.artifacts.get(artifact.id);
const { valid, reason } = await mandate.artifacts.verify(envelope.artifact);
if (!valid) throw new Error(`refusing to execute: ${reason}`);
The public key is fetched from the gateway's key discovery document and
matched on signature.key_id. Pin it instead with
{ publicKeyPem } if you would rather not fetch.
artifacts.claim(artifactId, executorReference)
Atomic. If two executors race, exactly one succeeds and the other throws a 409. Claim before executing.
await mandate.artifacts.claim(artifact.id, "executor-instance-1");
artifacts.fail(artifactId, reasonCode)
Marks the artifact FAILED after a provider rejection.
artifacts.reissue(intentId)
Issues a fresh artifact after failure or expiry. Re-checks live mandate state — a revoked or expired mandate refuses, as does a released reservation.
Mandates
mandates.create(request)
const m = await mandate.mandates.create({
agent_id: agent.id,
name: "Cloud procurement mandate",
mode: "SHADOW",
policy: {
currency: "GBP",
allowed_assets: ["USDC"],
per_transaction: { autonomous_limit: 75_000, absolute_limit: 250_000 },
budgets: [{ period: "MONTH", limit: 500_000 }],
approval: { new_counterparty_requires_approval: true },
allowed_purposes: ["cloud_infrastructure"],
effective_at: new Date().toISOString(),
expires_at: new Date(Date.now() + 30 * 86_400_000).toISOString(),
},
});
Full field semantics: policy model.
mandates.publish(mandateId, options)
Freezes the draft. Requires a delegator attestation — whoever publishes asserts they hold the authority being delegated:
await mandate.mandates.publish(m.id, {
delegation: {
delegatorRole: "Finance Director",
authoritySource: "CORPORATE_POLICY",
sourceReference: "DoA-2026-v4-section-8",
},
changeNote: "Initial cloud procurement authority",
});
mnd8t records the assertion in the evidence chain. It does not verify that the delegator legally held the authority.
mandates.simulate(mandateId, body)
Dry run — nothing persisted, no budget moved.
const { would_decide, reason_codes } = await mandate.mandates.simulate(m.id, {
amount_minor: 120_000,
asset: "USDC",
counterparty_external_id: "supplier_acme_cloud",
purpose: "software",
at: "2026-08-04T14:00:00Z", // test schedule rules
});
mandates.revoke(mandateId)
Immediate kill switch: new decisions reject, pending approvals expire, unconsumed reservations release, live artifacts are revoked.
mandates.get(id) · mandates.list()
Agents and counterparties
const agent = await mandate.agents.create({
name: "Procurement Agent",
external_id: "procurement_agent_12", // what you pass on every decision
purpose: "Purchase approved software",
principal_type: "ORGANISATION",
principal_reference: "acme_ltd",
});
await mandate.counterparties.create({
external_id: "supplier_acme_cloud",
display_name: "Acme Cloud",
authority_status: "CUSTOMER_APPROVED", // your assertion, not a screening result
destination: "0x1111111111111111111111111111111111111111",
});
Also: agents.list(), agents.get(id), counterparties.list().
external_id must be unique within your organisation and matches
[a-zA-Z0-9_.-]+.
Receipts
receipts.get(receiptId)
receipts.verify(receipt, options?)
Local verification — the one that matters, because it does not require
trusting the API that issued the receipt. Checks the Ed25519 signature
against the published key the record names (retired keys included), that
the record's action_hash binds the action it records, and, with
expectedAction, that it is the action you are about to execute.
const envelope = await mandate.receipts.get(receiptId);
const result = await mandate.receipts.verify(envelope.receipt, {
expectedAction: {
action_type: "PAYMENT",
financial_account_id: null,
asset: "GBP",
amount_minor: 500_000,
counterparty: "supplier_a",
destination: null,
purpose: "invoice_payment",
external_reference: "INV-123",
},
});
// { valid, signature_valid, record_integrity, action_match, key_id, key_status, action_hash, reason }
publicKeyPem pins a key instead of fetching /.well-known/mandate-keys.json.
actionDigest(fields) · verifyAuthorityRecord(receipt, keys, options?)
Standalone exports. actionDigest computes the sha256: action hash an
Authority Record carries, from your own instruction; see
evidence-receipts.md for the
specification. verifyAuthorityRecord is what receipts.verify runs, for
callers who already hold the key set.
ap2DecisionRequest(input) · verifyAp2PaymentAgainstRecord(input)
AP2 adapter v0.1, written against AP2 v0.2.0. This is a versioned mapping, not certified AP2 conformance. See api.md for the gateway side.
ap2DecisionRequest({ checkoutReference, payee, amount }) returns the
decisions.authorize fields that tie an mnd8t decision to an AP2 payment:
action_type: "PAYMENT";amountfrom the AP2 amount;- the merchant id as
counterparty_external_id; - the open Checkout Mandate digest as
external_reference.
Spread it into the rest of your request.
verifyAp2PaymentAgainstRecord({ openMandate, closedMandate, record, keys })
is for whoever holds an AP2 payment and the Authority Record it claims: a
credential provider, a merchant or an executor. Run AP2's own checks first.
This adds the mnd8t side:
- the record's signature and integrity;
- an enforce-mode decision that authorised the payment and was not revoked;
- payee, currency, amount and checkout reference, field by field;
- immediate execution only.
It returns { valid, record, mismatches }, where mismatches names each
difference. It runs locally with the published keys (receipts.keys()), and
answers what was decided when the record was signed, not whether the
authority still stands.
receipts.verifyPresented(receipt, expectedAction?)
POST /v1/evidence-receipts/verify: the server-side check of a record you
hold. Also returns current_authority, the mandate's state now, kept apart
from validity: a revoked mandate's APPROVE record is still a valid record.
receipts.verifyRemote(receiptId)
Server-side check of a stored receipt by id. Convenience only — it asks the issuer to vouch for itself.
receipts.keys()
The published verification keys, with status ACTIVE or RETIRED.
Errors
Every non-2xx response throws a typed subclass of MandateError, so you can
branch on the class rather than parsing strings:
import {
MandateError,
MandateAuthenticationError,
MandateValidationError,
MandateIdempotencyConflictError,
MandateRateLimitError,
MandateNotFoundError,
} from "@mnd8t/sdk";
try {
await mandate.decisions.authorize(payload);
} catch (err) {
if (err instanceof MandateRateLimitError) return backOffAndRetry();
if (err instanceof MandateValidationError) return report(err.details);
if (err instanceof MandateError) log(err.status, err.code, err.message);
throw err;
}
| Class | Status |
|---|---|
MandateAuthenticationError |
401 |
MandateNotFoundError |
404 |
MandateIdempotencyConflictError |
409 (IDEMPOTENCY_CONFLICT) |
MandateValidationError |
422 |
MandateRateLimitError |
429 |
MandateError |
anything else |
Every instance carries status, code, message and details.
Retries and idempotency
GETs retry automatically. Writes retry only when an idempotency key
makes the replay safe — which decisions.authorize always sets, so it is
safe by default. Retries fire on network failures and 5xx, up to
maxRetries, and never on 4xx.
If you supply your own idempotencyKey, the guarantees are the API's:
identical payload replays the original response; a different payload with
the same key throws MandateIdempotencyConflictError. Keys are retained 7
days.
A complete flow
import { MandateClient } from "@mnd8t/sdk";
const mandate = new MandateClient({ apiKey: process.env.MANDATE_API_KEY! });
const decision = await mandate.decisions.authorize({
agent_external_id: "procurement_agent_12",
action_type: "PAYMENT",
amount: { asset: "USDC", asset_amount_minor: 84_000_000 },
valuation: { policy_amount_minor: 50_000, policy_currency: "GBP" },
counterparty_external_id: "supplier_acme_cloud",
purpose: "cloud_infrastructure",
});
if (decision.effective_decision === "ESCALATE") {
await notifyApprover(decision.approval!.approval_url);
return;
}
if (decision.effective_decision !== "APPROVE") return;
const artifact = decision.authorisation_artifact!;
const envelope = await mandate.artifacts.get(artifact.id);
const { valid } = await mandate.artifacts.verify(envelope.artifact);
if (!valid) throw new Error("artifact failed verification");
await mandate.artifacts.claim(artifact.id, "executor-1");
const txRef = await myWallet.pay(envelope.artifact); // your credentials
await mandate.decisions.confirm(decision.id, {
provider: "MY_PROVIDER",
external_reference: txRef,
authorisation_artifact_id: artifact.id,
reported_by: "CUSTOMER_EXECUTOR",
verification: { method: "PROVIDER_LOOKUP", verified: true },
});