Skip to main content

AP2 Mandate Verification

Endpoint: POST /agentic_commerce/ap2/verify Auth: None at this endpoint — verification is of the mandate itself, not the caller (see Trust model below) Protocol: Agent Payments Protocol (AP2) (Google), mandate format per IETF SD-JWT (RFC 9901)

Live & Public
curl -X POST https://ainative-browser-builder.up.railway.app/agentic_commerce/ap2/verify \
-H "Content-Type: application/json" \
-d '{"mandate": "<sd-jwt-string>"}'

What it's for​

AP2 is Google's open protocol for letting an AI agent carry a cryptographically verifiable mandate — a signed credential proving the agent's human principal actually authorized an intent, a cart, or a payment — instead of the agent acting on bare, unverifiable say-so. AINative's /agentic_commerce/ap2/verify endpoint does the real, server-side work of checking one of these mandates before anything downstream is allowed to treat it as authorized: signature validity, expiry, and selective-disclosure integrity are all actually enforced, not assumed.

This endpoint is discoverable as the ap2 capability in the platform's UCP discovery document (GET /.well-known/ucp).

The mandate format: SD-JWT​

AP2 mandates are Selective Disclosure JWTs (SD-JWT) — a JWT whose payload hides individual claims behind salted hash digests, plus a separately-presented list of disclosures that reveal only the claims the presenter chooses to share. The wire format is:

<signed-jwt>~<disclosure1>~<disclosure2>~...
  • The first segment (before the first ~) is a normal signed JWT. Its payload carries an _sd array — base64url-encoded SHA-256 digests of every hideable claim — plus _sd_alg (currently always "sha-256").
  • Every segment after a ~ is one disclosure: a base64url-encoded JSON array of exactly three elements, [salt, claim_name, claim_value].
  • A disclosure is only trusted if hashing it reproduces one of the digests in the JWT's own _sd array. A disclosure whose hash doesn't match anything in _sd is treated as a forged or tampered claim and rejected outright — it is never silently folded into the result.

What verification actually checks​

Reading the real implementation (ap2_mandate_service.py's verify_sd_jwt()), every one of these is a real, enforced check — none of them are stubbed:

  1. JWT signature — verified against the configured issuer secret (HMAC). An invalid or missing-~ string is rejected before signature checking even runs.
  2. Expiry (exp) — enforced via verify_exp: True. This matters because the sd-jwt reference PyPI library does not check expiry itself (its own source has a literal # TODO: Check exp/nbf/iat); AINative's implementation is hand-rolled on top of PyJWT specifically to not inherit that gap.
  3. Disclosure integrity — every disclosure's SHA-256 digest must appear in the JWT's _sd array, or the whole mandate is rejected.
  4. Disclosure shape — each disclosure must decode to valid base64url JSON and be exactly a 3-element [salt, claim_name, claim_value] array.

Only claims with a matching, well-formed disclosure are reconstructed into the returned claims object. A claim hidden behind _sd with no corresponding disclosure presented simply never appears in the result — it stays undisclosed, which is the whole point of SD-JWT.

Request​

POST /agentic_commerce/ap2/verify
Content-Type: application/json
FieldTypeRequiredDescription
mandatestringYesThe full SD-JWT presentation string: <signed-jwt>~<disclosure1>~<disclosure2>~...

Response — success​

200 OK:

{
"result": "success",
"reference": "k3F2k9...base64url-sha256-of-the-mandate",
"claims": {
"transaction_id": "txn_real_1",
"payment_amount": { "amount": 5000, "currency": "usd" },
"payee": { "id": "ainative", "name": "AINative Studio" }
}
}
FieldTypeDescription
resultstringAlways "success" on a 200 response.
referencestringA deterministic receipt reference: base64url(SHA-256(mandate)). The same mandate string always produces the same reference — it is not a random id.
claimsobjectThe reconstructed claims from every disclosure that verified against _sd. Shape depends entirely on what the issuer disclosed — there is no fixed schema beyond what the issuer chose to include (commonly transaction_id, payment_amount, payee, etc., per AP2's Payment Mandate shape).

Response — error​

Any verification failure returns 400 Bad Request with the same structured shape:

{
"result": "error",
"error": "invalid_credential",
"message": "Malformed SD-JWT: missing '~' disclosure separator."
}
FieldTypeDescription
resultstringAlways "error".
errorstringCurrently always "invalid_credential" — every verification failure (bad signature, expired, malformed, tampered disclosure) maps to this one error code today.
messagestringHuman-readable reason. Real examples produced by the live endpoint: "Malformed SD-JWT: missing '~' disclosure separator.", "Mandate has expired.", "Invalid signature.", "Unmatched disclosure: hash does not appear in the mandate's _sd array (forged or tampered claim)."
Verified against the live endpoint

The malformed-mandate example above was reconfirmed by posting a deliberately malformed mandate to the live production endpoint on 2026-10-27 — it is not quoted from an older report. Note that the platform's structured-error middleware wraps this body inside an outer detail field in the actual HTTP response (plus error_code/next_action metadata); the result/error/message shape above is the payload the service layer itself produces.

A request missing the mandate field entirely gets a standard FastAPI/Pydantic 422 validation error instead (no mandate string was even present to verify).

If the server has no issuer secret configured at all, the endpoint returns 500 with {"detail": "AP2 verification not configured"} — this is an operational misconfiguration, not a mandate-verification outcome.

Trust model today​

Be precise about what this endpoint actually trusts: verification is currently against a single shared secret, AP2_ISSUER_SECRET, configured once on the server (HMAC, HS256). Any mandate signed with that same secret verifies — there is no per-issuer public-key registry yet, so the endpoint cannot yet distinguish "issuer A's mandate" from "issuer B's mandate" by key identity; it can only tell a correctly-signed mandate from an incorrectly-signed one.

This is intentional for the current increment, not an oversight: the acceptance criteria behind this endpoint called for real verification mechanics — signature, expiry, and disclosure integrity — ahead of a real mandate-issuing counterparty actually existing to integrate against. A full per-issuer public-key trust store (resolving iss to a specific registered public key, supporting asymmetric algorithms like ES256/RS256) is tracked as a separate, later increment and is not part of what this endpoint does today. Treat any claim that this endpoint supports multi-issuer trust as inaccurate until that work actually ships.

Mandate types​

The platform's UCP discovery document advertises three mandate types this endpoint is prepared to verify:

TypeDescription
intentAn intent mandate — the user authorized the agent to act toward a goal, without fixing exact terms yet.
cartA cart mandate — the user authorized a specific, itemized cart.
paymentA payment mandate — the user authorized an exact payment amount, payee, and instrument.

The verifier itself (verify_sd_jwt()) does not branch on mandate type — the same signature/expiry/disclosure checks apply uniformly. Type-specific business rules (e.g. rejecting a payment mandate that requests more than its own declared payment_amount constraint) belong to whatever calls this endpoint and inspects the returned claims, not to the verifier itself.

curl examples​

Malformed mandate (real 400):

curl -s -X POST https://ainative-browser-builder.up.railway.app/agentic_commerce/ap2/verify \
-H "Content-Type: application/json" \
-d '{"mandate": "garbage-not-a-jwt"}'

Missing field (real 422):

curl -s -X POST https://ainative-browser-builder.up.railway.app/agentic_commerce/ap2/verify \
-H "Content-Type: application/json" \
-d '{}'
DocumentPurpose
UCP Discovery DocumentLists AP2 as one of the platform's advertised payment capabilities
Agent402 APIAgent-side x402 wallets — a different, HTTP-402-based payment mechanism from AP2 mandates

See also: AP2 protocol spec (Google) and RFC 9901 (SD-JWT) for the underlying standards this endpoint implements.