{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://travel.agenthaven.dev/contracts/schemas/mandate.schema.json",
  "title": "Mandate (payload of a mandate+jwt)",
  "description": "Issued by the authority after human approval, as a compact JWS with header {alg: ES256, typ: mandate+jwt, kid: <thumbprint of the authority issuing key, published at <issuer>/.well-known/jwks.json on the authority domain, TLSA-pinned>}. Three stages in AP2 terms: intent (issued at approval, carries a ceiling), cart (bound to a quote hash), payment (references the PaymentIntent). The seller's checks 4 to 10 read exactly these fields (architecture v0.2 §5.5, §5.6): it accepts issuers on its MANDATE_ISSUERS allow-list only, requires stage cart or payment at checkout, amount.value EQUAL to the quote total, cnf.jkt equal to the kid of the request envelope that presents the mandate, act.sub equal to the envelope's iss, refuses act.act (R-12), claims jti once, and asks the authority's status endpoint (schemas/authority-status.schema.json) before any side effect. Status: IMPLEMENTED by the seller (checks 4 to 10, since 2026-09-07; test/mandate.test.mjs, test/authority-vector.test.mjs) and by agent-authority, issuing at https://sealedby.dev since 2026-09-11 (vectors/positive/mandate-authority*.json); the first cart mandate was accepted live on 2026-09-11. The self-issued authorization of the demo scripts keeps its own claim set on a separate path. Field names follow RFC 7519/8693/7800 where a registered claim fits.",
  "type": "object",
  "required": ["iss", "sub", "act", "aud", "merchant_id", "action", "amount", "nbf", "exp", "jti", "cnf", "policy_version", "stage"],
  "properties": {
    "iss": { "type": "string", "format": "uri", "description": "The authority's issuer URL. The seller accepts issuers on its allow-list, discovered by domain (JWKS on a DNSSEC + DANE anchored name); and, where it takes guests (guest-buyer.md), https://<the buyer's own trust domain>, whose keys are the TXT record set at _agent-keys.<that domain>." },
    "sub": { "type": "string", "description": "The pseudonymous person: an opaque id the authority holds; never a phone number, never a name." },
    "act": { "type": "object", "required": ["sub"], "properties": { "sub": { "type": "string", "pattern": "^spiffe://", "description": "The buyer instance acting for the person (RFC 8693 actor claim)." } } },
    "aud": { "type": "string", "format": "uri", "description": "The seller resource: its public base URL." },
    "merchant_id": { "type": "string" },
    "action": { "type": "string", "enum": ["purchase"], "description": "What the mandate permits. Search needs no mandate." },
    "amount": { "type": "object", "required": ["value", "currency"], "properties": { "value": { "type": "integer", "minimum": 1, "description": "Minor units. The ceiling at the intent stage; exactly the quote total at the cart stage." }, "currency": { "type": "string", "pattern": "^[A-Z]{3}$" } } },
    "quote_hash": { "type": "string", "pattern": "^[0-9a-f]{64}$", "description": "Required from the cart stage on: SHA-256 over the payload octets of the seller's quote+jwt, recomputed by the authority from the JWS the buyer forwarded after verifying the seller's signature." },
    "payment_intent_id": { "type": "string", "description": "Payment stage only." },
    "nbf": { "type": "integer" },
    "exp": { "type": "integer", "description": "Epoch seconds, like nbf; the window from nbf to exp is minutes long (freeze-manifest.json). Clock skew tolerated everywhere: 60 seconds. (Description corrected 2026-09-10: it said 'minutes after nbf' while the schema, the seller and the positive vector all use epoch seconds.)" },
    "jti": { "type": "string", "minLength": 16, "description": "Single use. The seller inserts it atomically in its one named Durable Object before any side effect (check 9)." },
    "cnf": { "type": "object", "required": ["jkt"], "properties": { "jkt": { "type": "string", "description": "RFC 7638 thumbprint of the buyer's stable signing key (RFC 7800 confirmation). Must equal the kid of the request envelope that presents the mandate (check 9, key binding). A mandate with no cnf is refused at that same check as key_binding_mismatch, not at check 4 as mandate_invalid: check 4 verifies the signature, the issuer and the naming claims, check 9 is the one that reads cnf (R-05, settled 2026-09-12)." } } },
    "policy_version": { "type": "string" },
    "stage": { "type": "string", "enum": ["intent", "cart", "payment"] }
  },
  "allOf": [
    { "if": { "properties": { "stage": { "enum": ["cart", "payment"] } } }, "then": { "required": ["quote_hash"] } },
    { "if": { "properties": { "stage": { "const": "payment" } } }, "then": { "required": ["payment_intent_id"] } }
  ]
}
