Skip to content

Signed mandates

A signed mandate is the request an AI agent sends to act on your store: place an order, request a refund. The agent signs it with its own ed25519 key, so Sill can prove which agent asked, for whom, for what, and up to how much. Every Transactional order starts as one; see the overview.

This page is for developers building an agent that buys from Sill-protected stores, and for merchants who want to know what the agent actually committed to.

POST https://edge.sill.so/v1/m/{site_key}/mandate
Content-Type: application/json

{site_key} is the store’s site key. The body is the mandate below, at most 8 KB. The agent’s key must be registered with Sill; see Identifying agents.

A mandate has two parts: signed (everything the signature covers) and envelope (the signature itself).

{
"signed": {
"mandate_id": "mnd_01K6HJ2PPKF9SECPQ49FWC3W8H",
"site_id": "01JDKM5V0XQZ8T3H6N9WPRB2CE",
"agent_id": "agent_acme_shopper",
"issued_at": "2026-10-01T14:51:10.000Z",
"expires_at": "2026-10-01T14:51:40.000Z",
"nonce": "F3RkP2x9Tn5sQwAaC1bD7g",
"replay_window_seconds": 30,
"principal": { "type": "human", "verification": "delegated" },
"intent": {
"action": "place_order",
"merchant": "northwind-coffee.example",
"sku": "NW-GRINDER-01",
"quantity": 1,
"max_amount": 129.00,
"quoted_total": 129.00,
"currency": "USD"
},
"protocol_context": { "protocol": "a2a", "version": "1.2" }
},
"envelope": {
"algorithm": "ed25519",
"key_id": "acme-2026-10",
"signature": "<86-character base64url ed25519 signature>"
}
}

Identifiers and the signature above are illustrative.

FieldFormatMeaning
mandate_idmnd_ + 26-character ULIDUnique id for this request.
site_idULIDThe store’s Sill site. Must match the site of {site_key}.
agent_idagent_ + 1–64 lowercase letters, digits or _The signing agent’s registered id.
issued_atISO-8601 UTCWhen the agent created the mandate. Up to 60 seconds in the future is tolerated.
expires_atISO-8601 UTCAfter this the mandate is refused. Must be more than 1 second after issued_at.
nonce22-character base64url (16 random bytes)Makes each mandate single-use.
replay_window_secondsinteger, 1–600How long the nonce is remembered. Must cover expires_at minus issued_at.
principalobjectWho the agent acts for: type is human, organization or none; verification is delegated, direct or none. Optional user_ref and org_ref identify the customer for per-customer and per-organization spend caps.
intentobjectWhat the agent wants to do; see below.
protocol_contextobjectThe protocol the request arrives over (a2a, ap2 or mcp) and its version.
delegation_chainarray, optionalUp to 4 signed hand-offs when one agent acts on behalf of another.

intent.action decides which fields are allowed.

  • Orders (place_order): merchant, currency (three uppercase letters), max_amount (the most the agent may spend, in major units: 129.00 is $129.00), and either sku (with optional quantity, 1–100) or line_items (1–20 entries of { "sku", "quantity" }). Optional: quoted_total (the price the agent agreed; required with line_items), buyer (the shopper’s contact and shipping details, passed to the merchant’s payment account for the receipt and delivery), shipping, shipping_method, discount, attributes.
  • Refunds (request_refund): merchant, currency, max_amount, original_mandate_id, reason_code and scope, and no other field. See Refunds.

The amount charged comes from these signed values, never from anything sent later. See Payments.

The agent signs these bytes with its ed25519 private key and puts the base64url signature (no padding) in envelope.signature:

utf8("sill-mandate-v1") || 0x00 || utf8(JCS(signed))

JCS is the RFC 8785 canonical JSON form of the signed object. The sill-mandate-v1 prefix means a mandate signature can never be reused as a signature on any other kind of Sill document. envelope.key_id names which of the agent’s registered keys signed it.

Anyone can check the signature the same way with the agent’s public key; the general recipe is on Verify a signature.

HTTPBodyMeaning
200{"outcome":"approved","mandate_id","record_id","evaluated_at","rules_evaluated"}Your rules approved it. The charge runs next.
202{"outcome":"escalated","mandate_id","escalation_id","evaluated_at","evaluated_rule_id"}Held for review. See Human in the loop.
403{"outcome":"rejected","reason":"<rule id>","mandate_id","record_id",…}A rule rejected it.
4xx{"outcome":"rejected","reason":"…"}The mandate itself was refused; see the next table.
429{"outcome":"rate_limited","retry_after_seconds":60}Too many requests. Retry after the Retry-After header.
503{"outcome":"unavailable","request_id"}Sill could not decide right now, or the site has no published policy. Nothing was approved. Retry later with a new mandate.
HTTPreasonWhat to fix
400malformedA field is missing, has the wrong format, or is not allowed for this action.
400agent_id_malformedagent_id is not agent_ followed by lowercase letters, digits or _.
400mandate_id_malformedmandate_id is not mnd_ plus a ULID.
400algorithm_unsupportedenvelope.algorithm must be ed25519.
400replay_window_too_shortreplay_window_seconds does not cover the validity window.
413oversizeThe body is over 8 KB.
415unsupported_media_typeSend Content-Type: application/json.
404not_foundThe site key is unknown or the site is not ready for orders.
401identity_check_failedThe agent or key is not registered or was revoked, the protocol is not accepted, or site_id does not match the site key. Sill does not say which.
401signature_invalidThe signature does not match the signed body. Check canonicalisation and the prefix.
401delegation_chain_invalidA link in delegation_chain is broken, expired, widens the scope or is badly signed.
403expiredexpires_at has passed.
403issued_in_futureissued_at is more than 60 seconds ahead of Sill’s clock.
403replayThis nonce was already used. Create a new mandate.
403merchant_overrideThe store has blocked or held this agent.
429agent_temporarily_lockedToo many failed attempts from your network address. Wait before retrying.

Refusals for a known site are also recorded in the merchant’s audit log with the precise reason, so the merchant can see who tried and why it failed.

When one agent acts on behalf of another, the mandate can carry a delegation_chain: up to 4 hand-offs, each signed by the delegating agent (with the separate prefix sill-delegation-v1) and each with up to 16 scope entries. Each link can only narrow what the next agent may do, never widen it. Mandates without a chain are unaffected.

A verified mandate goes to the site’s policy engine. Each mandate ends in one signed audit record:

DecisionMeaning
approvedYour rules approved it; the charge follows.
rejectedA rule rejected it.
escalated_approvedHeld, then approved by a person.
escalated_rejectedHeld, then rejected by a person or expired after 24 hours.
verification_rejectedRefused before guardrails ran (signature, replay, unknown agent and so on).

When the order is charged, a separate signed settlement record is added and linked to the decision. See Audit envelope.

Approved signed order from the Acme agent: verified signature, 1 × NW-GRINDER-01, protocol AP2, every rule passed An approved order as the merchant sees it: the verified agent, what it asked for, and the rules it passed.

Does Sill sign the mandate? No. The agent signs the mandate. Sill verifies it, then signs its own audit record of the decision.

Does the mandate carry card details? No. It carries the amount ceiling, currency and items. Payment happens on the merchant’s Stripe account; see Payments.

Which protocols can a mandate arrive over? A2A, AP2 and MCP. With protocol set to ap2, protocol_context can also carry cart_total and cart_currency, which the guardrails check against the intent. See Protocols.

Can I resend a mandate that timed out on the network? Not the same one: its nonce is used, so it returns replay. Create a new mandate with a new mandate_id and nonce.