{"openapi":"3.1.0","info":{"title":"OID4Pay Authorization Server","description":"OID4AC-compliant Authorization Server issuing JWT-AT (RFC 9068), SD-JWT VC mandates, DPoP-bound (RFC 9449) and DCR-managed (RFC 7591) tokens. Wire shapes pinned in ","contact":{"name":"OID4Pay","url":"https://oid4pay.com/","email":"ops@oid4pay.com"},"license":{"name":"Proprietary","url":"https://oid4pay.com/legal/license"},"version":"0.0.1"},"servers":[{"url":"https://as.oid4pay.com","description":"Production"},{"url":"https://sandbox.oid4pay.com","description":"Sandbox"}],"paths":{"/healthz":{"get":{"tags":["health"],"summary":"Healthz","description":"Liveness probe.","operationId":"healthz_healthz_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":{"type":"string"},"type":"object","title":"Response Healthz Healthz Get"}}}}}}},"/readyz":{"get":{"tags":["health"],"summary":"Readyz","description":"Readiness probe: Postgres SELECT 1 + Redis PING.","operationId":"readyz_readyz_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Readyz Readyz Get"}}}}}}},"/.well-known/openid-federation":{"get":{"tags":["federation"],"summary":"Openid Federation Entity Configuration","description":"OID4Pay Entity Configuration. 503 when the federation server is\ndisabled (the dark default).","operationId":"openid_federation_entity_configuration__well_known_openid_federation_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/federation/fetch":{"get":{"tags":["federation"],"summary":"Federation Fetch","description":"Issue a subordinate Entity Statement. 404 when ``sub`` is not an\nactive registered subordinate; 503 when the federation server is disabled.","operationId":"federation_fetch_federation_fetch_get","parameters":[{"name":"sub","in":"query","required":true,"schema":{"type":"string","title":"Sub"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/federation/list":{"get":{"tags":["federation"],"summary":"Federation List","description":"List the active subordinate entity ids.","operationId":"federation_list_federation_list_get","parameters":[{"name":"entity_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Entity Type"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/federation/trust-mark":{"get":{"tags":["federation"],"summary":"Federation Trust Mark","description":"Issue a Trust Mark for an enrolled + eligible subordinate. 404 when the\nsubject is not an active subordinate or is not eligible for the mark id; 503\nwhen the federation server is disabled. Eligibility is the subordinate row's\n``trust_mark_ids``; suspension / revocation removes eligibility.","operationId":"federation_trust_mark_federation_trust_mark_get","parameters":[{"name":"trust_mark_id","in":"query","required":true,"schema":{"type":"string","title":"Trust Mark Id"}},{"name":"sub","in":"query","required":true,"schema":{"type":"string","title":"Sub"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/federation/trust-mark-status":{"get":{"tags":["federation"],"summary":"Federation Trust Mark Status","description":"Report whether a Trust Mark is currently active (the federation analogue\nof OAuth introspection). Active only while the subject is an active\nsubordinate still eligible for the mark id, and the mark has not expired.\nAccepts either the compact ``trust_mark`` JWT or explicit ``trust_mark_id``\n+ ``sub``. Unknown / undecodable inputs report ``active: false`` (no leak).","operationId":"federation_trust_mark_status_federation_trust_mark_status_get","parameters":[{"name":"trust_mark_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Trust Mark Id"}},{"name":"sub","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sub"}},{"name":"trust_mark","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Trust Mark"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/federation/trust-mark/revoke":{"post":{"tags":["federation"],"summary":"Federation Trust Mark Revoke","description":"Revoke a single trust mark for a subordinate without suspending the\nsubordinate or affecting any other mark. Sets revoked_at on the ledger row;\na subsequent trust-mark-status call returns active: false. 404 when no\nledger row exists for (sub, trust_mark_id). 503 when the federation server\nis disabled.","operationId":"federation_trust_mark_revoke_federation_trust_mark_revoke_post","parameters":[{"name":"trust_mark_id","in":"query","required":true,"schema":{"type":"string","title":"Trust Mark Id"}},{"name":"sub","in":"query","required":true,"schema":{"type":"string","title":"Sub"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/federation/resolve":{"get":{"tags":["federation"],"summary":"Federation Resolve","description":"Resolve a trust chain from ``sub`` up to ``anchor`` and return the\nresolved metadata + chain as a resolve-response JWT, for thin clients that\ncannot resolve chains themselves. 503 when the federation server is disabled.\n\nv1 resolves only against OID4Pay's OWN issuer as the anchor (its keys are the\nout-of-band root of trust); a foreign anchor needs pinned keys and is 400\n(the multi-anchor topology is founder-pending). Resolution reuses the shipped\nconsumer ``resolve_trust_chain`` with a hardened fetcher; any failure is a\nstructured error (the consumer's typed ``TrustChainError.code``), never a\npartial chain. Enrollment is enforced indirectly: an unenrolled ``sub`` has\nno Subordinate Statement at OID4Pay's fetch endpoint, so the chain fails.","operationId":"federation_resolve_federation_resolve_get","parameters":[{"name":"sub","in":"query","required":true,"schema":{"type":"string","title":"Sub"}},{"name":"anchor","in":"query","required":true,"schema":{"type":"string","title":"Anchor"}},{"name":"entity_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Entity Type"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/oauth/par":{"post":{"tags":["oauth"],"summary":"Par","description":"Pushed Authorization Request endpoint (RFC 9126).\n\nBody: ``application/x-www-form-urlencoded``. Returns ``201 Created``\nwith exactly ``{request_uri, expires_in}`` per section\n6A.1, plus ``Cache-Control: no-store`` per RFC 9126 section 2.2.","operationId":"par_oauth_par_post","responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/oauth/par/{request_uri}/status":{"get":{"tags":["oauth"],"summary":"Par Status","description":"Scan-mode pending-authorization status poll.\n\nThe agent polls this with the opaque PAR ``request_uri`` it received from\n/par. Capability-based: knowing the high-entropy request_uri is the\nauthority (the RFC 9126 PAR handle model); the response leaks only the\ndecision status, never principal or mandate data. Gated by\nOID4PAY_CONSENT_SCAN_ENABLED; 404 for an unknown / expired request_uri.","operationId":"par_status_oauth_par__request_uri__status_get","parameters":[{"name":"request_uri","in":"path","required":true,"schema":{"type":"string","title":"Request Uri"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/oauth/authorize":{"get":{"tags":["oauth"],"summary":"Authorize","description":"Authorization endpoint (RFC 6749 section 4.1).\n\nConsumes the PAR record via GETDEL, resolves the consent mode\n(M1 supports ``auto`` only), mints a\nsingle-use authorization code, and returns ``302 Found`` with\n``Location`` carrying ``code``, ``state``, and the RFC 9207\n``iss`` parameter. mandates that the\ncode TTL be 60 s and that a second redemption attempt cascade-\nrevokes the issued token family; this endpoint ships the\ndetection primitives (``oid4pay:code_seen:<code>``,\n``oid4pay:family_of_code:<code>``) the token endpoint consumes\nto perform the revoke.\n\nSecurity headers (RFC 9700 section 4.8 + section 2.2 +\nRFC 6749 section 5.1): ``X-Frame-Options: DENY``, CSP\n``frame-ancestors 'none'``, ``Referrer-Policy: no-referrer``,\n``Cache-Control: no-store``, ``Pragma: no-cache``. These are\nattached by :class:`OAuthSecurityHeadersMiddleware`\n(see ``app/middleware/security_headers.py``) so the success path,\nthe 400 error path, and any future 500 path all carry the same\nset.\n\nValidation failures return ``400 Bad Request`` with a JSON body\nmatching the OAuth error shape so the agent can react without\nHTML scraping.","operationId":"authorize_oauth_authorize_get","parameters":[{"name":"request_uri","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Request Uri"}},{"name":"client_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Client Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/oauth/magic-link/confirm":{"get":{"tags":["oauth"],"summary":"Magic Link Confirm","description":"Magic-link confirm endpoint (6A.10).\n\nThe principal clicks the single-use confirm link the AS dispatched\nfrom ``/oauth/authorize`` when the magic-link gate is on. This\nendpoint consumes the token (atomic GETDEL + replay marker per\nresumes the EXACT post-consent\nauthorization-code issuance (the same code-mint the legacy\nauto-consent path uses; not duplicated), and 302s back to the\nwallet ``redirect_uri`` with ``code`` + ``state`` + the RFC 9207\n``iss`` parameter exactly as the auto-consent path does today.\n\nFailure modes map to the project's OAuth-error response shape\n(mirrors the ``/oauth/authorize`` 400 JSON body): an unknown /\nexpired / malformed token is ``invalid_request``; a replayed\ntoken (second confirm) is ``invalid_request`` with a replay\nerror_description. The token secret is never echoed back.","operationId":"magic_link_confirm_oauth_magic_link_confirm_get","parameters":[{"name":"token","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/oauth/token":{"post":{"tags":["oauth"],"summary":"Token","description":"Token endpoint (6A.7 + 6A.10).\n\nIssues a JWT-AT (RFC 9068, EdDSA, ``typ=at+jwt``) bound to the\nagent's DPoP key via ``cnf.jkt``, an opaque refresh token\n(rotated per), and a placeholder\nmandate JWT (real SD-JWT VC issuance lands at).\nAudience is narrowed to the PAR record's ``resource`` set per\nRFC 8707.\n\nBody is ``application/x-www-form-urlencoded`` per RFC 6749\nsection 4.1.3; the response is ``200 OK`` with\n``Cache-Control: no-store`` per RFC 6749 section 5.1.\nAuthorization-code replay triggers cascade-revoke of the\nissued-token family.\n\nIdempotency: when the request carries an ``Idempotency-Key``\nheader, the response is cached at ``oid4pay:idem:as:token:<k>``\nwith a TTL equal to the configured ``access_token_ttl_seconds``\n(default 300 s). A retry with the same key and the same body\nbytes returns the cached response without invoking the\nhandler (so the issued access token is byte-identical); a retry\nwith the same key but a different body returns 409 Conflict.\nA request without the header runs the handler unconditionally\nso the no-key path matches the pre-idempotency behaviour. The\nform body is fingerprinted byte-wise; the same agent submitting\nthe same form fields in a different order would diverge on the\nfingerprint, but in practice an agent that wants the cached\nresponse must replay the byte-identical body.","operationId":"token_oauth_token_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/oauth/jwks.json":{"get":{"tags":["oauth"],"summary":"Jwks","description":"JWKS endpoint exposing the AS public verification keys.\n\n swap: the M1 in-memory stub is gone; the returned JWKS\nis now the public half of the production / testmode signing\nkey loaded via ``app/signing.py``. Key rotation policy is\ndefined in (key management).","operationId":"jwks_oauth_jwks_json_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Jwks Oauth Jwks Json Get"}}}}}}},"/oauth/register":{"post":{"tags":["dcr"],"summary":"Post Register","description":"RFC 7591 section 3 dynamic client registration.\n\nWire shape pinned by the and the MCP's\n``RegisterResponse`` TypeScript interface. On success the AS:\n\n1. Consumes the setup token (single-use, 24 h TTL) and binds the\n   new client to the carried ``principal_id``.\n2. Validates every claim against and RFC\n   7591 section 2.\n3. Mints a fresh UUID ``client_id`` and a 32-byte base64url\n   ``registration_access_token``.\n4. Persists the new client into ``app.clients`` (M11 swaps the\n   in-process dict for a Postgres-backed registry per the\n   ``app.clients`` module doc).\n5. Returns the RFC 7591 section 3.2.1 response body.","operationId":"post_register_oauth_register_post","responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/oauth/register/{client_id}":{"get":{"tags":["dcr"],"summary":"Get Register","description":"RFC 7592 section 2.1 self-management read.\n\nThe agent presents its ``registration_access_token`` as Bearer;\nthe AS confirms the token maps to the requested ``client_id`` and\nreturns the same metadata shape the POST returned (minus the\n``registration_access_token`` itself, per RFC 7592 section 3 which\nnotes the AS MAY re-issue but is not required to). Following the\nprinciple of least surprise we re-emit the SAME token in the\nresponse body so the agent can re-persist if its local store was\nlost; M11+ may swap to rotation if the threat model demands.","operationId":"get_register_oauth_register__client_id__get","parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/wallet/setup-tokens":{"post":{"tags":["dcr"],"summary":"Post Wallet Setup Token","description":"Mint a one-shot agent-onboarding setup token (Wallet-side helper).\n\nWire shape:\n\n  POST /wallet/setup-tokens\n    {\n        \"principal_id\": \"<sub the new agent binds to>\",\n        \"client_id\": \"<wallet client id>\",\n        \"client_assertion_type\":\n          \"urn:ietf:params:oauth:client-assertion-type:jwt-bearer\",\n        \"client_assertion\": \"<EdDSA-signed private_key_jwt>\"\n    }\n\nResponse (201):\n  {\n    \"setup_token\": \"<32-byte base64url>\",\n    \"expires_in\": 86400,\n    \"qr_payload\": \"oid4ac://register?setup_token=<...>\"\n  }\n\nThe ``qr_payload`` is the URI the Wallet Portal v0 ``/agents/setup``\npage renders into a QR code; the agent scans it and POSTs the\nembedded ``setup_token`` to ``/oauth/register``.\n\nSecurity (oauth-review-2026-05-14 M3):\nthe endpoint is a token-mint endpoint (the issued setup_token IS\na bearer secret with a 24 h TTL).\nmandates DPoP on every token-mint endpoint; the wallet client\nalready presents private_key_jwt + DPoP at /oauth/par for its\nown token flows so a DPoP proof here costs the wallet only a\nsingle extra signature per setup-token mint.","operationId":"post_wallet_setup_token_wallet_setup_tokens_post","responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/oauth/consent/scan":{"post":{"tags":["oauth"],"summary":"Post Oauth Consent Scan","description":"Record a scan-mode consent decision for a pending authorization.\n\nSee module docstring for the wire shape, error envelopes, IDOR\ndiscipline, kill switch, and observability contract. The\nservice-layer state machine is intentionally narrow until the M5\npending-authorization storage lands (see\n:mod:`app.services.consent_scan` for the gap note).","operationId":"post_oauth_consent_scan_oauth_consent_scan_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/oauth/consent/scan/pending":{"get":{"tags":["oauth"],"summary":"List Oauth Consent Scan Pending","description":"List the calling principal's pending scan-consent authorizations.\n\nAuthorization: ``Bearer <jwt-at scope=wallet:read>`` (same scope as the\nsingle-pending GET). IDOR discipline: the principal is the verified JWT-AT\n``sub``; the index is keyed on that principal, never on a query / path /\nbody value, so another principal's pending authorizations are never\nreturned. Kill-switched on ``OID4PAY_CONSENT_SCAN_ENABLED`` (503 while\noff). Returns ``{\"pending\": [...]}`` where each entry is the same display\nprojection the single-pending GET returns (newest first by ``created_at``);\ndecided / expired records are excluded. This replaces the Redis ``SCAN`` the\nWallet / ops previously needed to discover a pending ``authorization_id``.","operationId":"list_oauth_consent_scan_pending_oauth_consent_scan_pending_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/oauth/consent/scan/pending/{authorization_id}":{"get":{"tags":["oauth"],"summary":"Get Oauth Consent Scan Pending","description":"Return a pending authorization's display fields for the Wallet\n``/approve`` screen.\n\nAuthorization: ``Bearer <jwt-at scope=wallet:read>``. IDOR discipline: the\nprincipal is the verified JWT-AT ``sub``; an unknown / expired /\ncross-principal ``authorization_id`` all return 404 (no enumeration oracle,\nmatching the decision endpoint). Kill-switched on\n``OID4PAY_CONSENT_SCAN_ENABLED`` (503 while off). The response leaks only\nwhat the principal must see to decide; the agent's keys / tokens are never\nreturned.","operationId":"get_oauth_consent_scan_pending_oauth_consent_scan_pending__authorization_id__get","parameters":[{"name":"authorization_id","in":"path","required":true,"schema":{"type":"string","title":"Authorization Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/settlement/settle":{"post":{"tags":["settlement"],"summary":"Settle","description":"Settle a verified mandate across the configured rail. 503 when the kill\nswitch is off; 401 without a valid DPoP-bound JWT-AT or a valid, caller-bound\nmandate; 403 without the ``payment:execute`` scope or when a cap is exceeded.\nIdempotent on ``idempotency_key`` (a replay returns the original result).","operationId":"settle_settlement_settle_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SettleRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/oauth/rcs/limit-bump/callback":{"post":{"tags":["rcs"],"summary":"Rcs Limit Bump Callback","description":"limit-bump callback. See the module docstring for the contract.","operationId":"rcs_limit_bump_callback_oauth_rcs_limit_bump_callback_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/oauth/introspect":{"post":{"tags":["oauth"],"summary":"Introspect","description":"RFC 7662 token introspection.\n\nBody (``application/x-www-form-urlencoded``):\n\n- ``token``: REQUIRED, the token string to introspect.\n- ``token_type_hint``: OPTIONAL, one of ``access_token`` or\n  ``refresh_token``. The hint short-circuits the lookup order;\n  an unrecognised hint is ignored per RFC 7662 section 2.1.\n- ``client_id``: REQUIRED, the RS's registered client identifier.\n- ``client_assertion_type``: REQUIRED, fixed to\n  ``urn:ietf:params:oauth:client-assertion-type:jwt-bearer``.\n- ``client_assertion``: REQUIRED, ``private_key_jwt`` signed by\n  the RS's registered key.\n\nResponse: ``200 OK`` with ``Cache-Control: no-store`` on every\noutcome. The body is either the active\nshape or ``{\"active\": false}`` on inactive / unknown.","operationId":"introspect_oauth_introspect_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/oauth/revoke":{"post":{"tags":["oauth"],"summary":"Revoke","description":"RFC 7009 token revocation.\n\nBody: ``application/x-www-form-urlencoded``. The endpoint\nauthenticates the caller via ``private_key_jwt``, then revokes the presented token. The response is\nalways ``200 OK`` with an empty body once the caller is\nauthenticated, even when the token is unknown.\n\nUnknown-token uniform-success defeats token scanning: an attacker\nthat learns a ``client_id`` and steals its keys cannot probe\nother agents' tokens by their revoke response.\n names the contract; RFC 7009 section 2.2 is the\nupstream pin.","operationId":"revoke_oauth_revoke_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/wallet/policy/{merchant_audience}/deny":{"post":{"tags":["wallet"],"summary":"Post Deny","description":"Add an agent to the merchant's deny list.\n\n``merchant_audience`` is the path segment immediately after\n``/wallet/policy/``. The wallet UI percent-encodes the audience\n(``encodeURIComponent``) and then escapes the resulting ``%`` so\na slash ``/`` round-trips as ``%252F``; the handler decodes once\nvia ``urllib.parse.unquote`` to recover the canonical URL form.\nThe double-encoding is necessary because Starlette / Uvicorn\ndecode the path once before routing fires, so a single-encoded\n``%2F`` is interpreted as a path separator and the route does\nnot match (Starlette is RFC 3986 compliant on this point).","operationId":"post_deny_wallet_policy__merchant_audience__deny_post","parameters":[{"name":"merchant_audience","in":"path","required":true,"schema":{"type":"string","title":"Merchant Audience"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/wallet/policy/{merchant_audience}/allow":{"post":{"tags":["wallet"],"summary":"Post Allow","description":"Add an agent to the merchant's allow list and/or flip the gate.\n\nAccepts either ``agent_client_id`` (to add to the allow list) or\n``allow_list_required`` (to flip the gate) or both. At least one\nof the two MUST be present; an empty body is a 400.","operationId":"post_allow_wallet_policy__merchant_audience__allow_post","parameters":[{"name":"merchant_audience","in":"path","required":true,"schema":{"type":"string","title":"Merchant Audience"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/wallet/policy/{merchant_audience}/list":{"get":{"tags":["wallet"],"summary":"Get List","description":"Return the current policy state for the merchant.\n\nThe Wallet UI (M11) consumes this to render the policy editor. The\nGET reads ``client_id`` + ``client_assertion`` from the\n``X-Client-Id`` + ``X-Client-Assertion`` headers because most\nbrowser clients send a GET as a no-body request; the wallet\nSvelteKit client sets the headers on every read so the same auth\ngate fires as on the writes.","operationId":"get_list_wallet_policy__merchant_audience__list_get","parameters":[{"name":"merchant_audience","in":"path","required":true,"schema":{"type":"string","title":"Merchant Audience"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/wallet/agents":{"get":{"tags":["wallet"],"summary":"List Agents","description":"List the calling principal's registered agents.\n\nWire shape:\n\n  GET /wallet/agents\n    Authorization: Bearer <jwt-at scope=wallet:read>\n  ->\n  200 OK\n    {\"agents\": [{id, client_id, label, purpose, ...}, ...]}\n\nIDOR discipline (CLAUDE.md): the principal_id is read from the\nJWT-AT ``sub`` claim, never from a query parameter or URL path.","operationId":"list_agents_wallet_agents_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/wallet/agents/{client_id}":{"get":{"tags":["wallet"],"summary":"Get Agent","description":"Return a single agent's detail.\n\nWire shape:\n\n  GET /wallet/agents/<client_id>\n    Authorization: Bearer <jwt-at scope=wallet:read>\n  ->\n  200 OK\n    {id, client_id, label, purpose, ...}\n\nRefuses to return a client whose ``principal_id`` does not match\nthe JWT-AT ``sub`` (IDOR discipline). The 404 vs 403 distinction\nis deliberate: leaking \"this client exists, you can't see it\"\nwould be an oracle the wallet UI is not expected to differentiate.","operationId":"get_agent_wallet_agents__client_id__get","parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/wallet/agents/{client_id}/revoke":{"post":{"tags":["wallet"],"summary":"Revoke Agent","description":"Revoke an agent and cascade-revoke every family it has issued.\n\nWire shape:\n\n  POST /wallet/agents/<client_id>/revoke\n    Authorization: Bearer <jwt-at scope=wallet:write>\n  ->\n  200 OK\n    {\"ok\": true, \"client_id\": ..., \"revoked_at\": ISO8601,\n     \"families_revoked\": <int>}\n\n cascade-revoke: every token family\nassociated with the agent is swept; each member jti gets a\nrevocation marker so a presented JWT-AT is short-circuited at\nthe introspection layer (M13).\n\nIDOR discipline: the principal_id is read from the JWT-AT\n``sub`` claim. A revoke targeting a client owned by a different\nprincipal returns 404 (not 403; see ``get_agent``).","operationId":"revoke_agent_wallet_agents__client_id__revoke_post","parameters":[{"name":"client_id","in":"path","required":true,"schema":{"type":"string","title":"Client Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/wallet/cards":{"get":{"tags":["wallet"],"summary":"Get Wallet Cards","description":"Principal-scoped wallet saved-cards list (O11).\n\nSee module docstring for the wire shape, error envelopes, IDOR\ndiscipline, kill switch, and observability contract.","operationId":"get_wallet_cards_wallet_cards_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/wallet/setup-intent":{"post":{"tags":["wallet"],"summary":"Post Wallet Setup Intent","description":"Begin card enrolment for the authenticated principal (O11).\n\nSee module docstring for the wire shape, error envelopes, IDOR\ndiscipline, kill switch, and observability contract.","operationId":"post_wallet_setup_intent_wallet_setup_intent_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/wallet/setup-intent/confirm":{"post":{"tags":["wallet"],"summary":"Post Wallet Setup Intent Confirm","description":"Reconcile a confirmed SetupIntent (belt-and-suspenders).\n\nThe wallet calls this from its Stripe return_url handler so a late\nor missing ``setup_intent.succeeded`` webhook does not strand a\nconfirmed card. The handler retrieves the SetupIntent from Stripe\n(expanding the payment method for the card metadata), verifies its\n``metadata.principal_id`` equals the bearer's ``sub`` (IDOR; a\nSetupIntent that does not belong to the caller returns 404 with no\nenumeration oracle), and only when its status is ``succeeded``\nruns the SAME idempotent ``oid4ac.mandate.issued`` emission the\nwebhook runs. The emission is guarded by an existing-row check, so\nthe webhook and this reconcile racing for the same SetupIntent write\nexactly one mandate row.\n\nBody: ``{ \"setup_intent_id\": str }`` (required).\n200: ``{ \"status\": str, \"reconciled\": bool, \"setup_intent_id\": str }``\nwhere ``reconciled`` is true iff a fresh mandate row was written by\nthis call (false on a non-succeeded status or an already-issued\nmandate). Same 401 / 403 / 503 envelopes as the create leg.\n\nThe acceptance is a contractual payment mandate, NOT GDPR consent.","operationId":"post_wallet_setup_intent_confirm_wallet_setup_intent_confirm_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/wallet/payment-history":{"get":{"tags":["wallet"],"summary":"Get Wallet Payment History","description":"Principal-scoped wallet payment history (O12).\n\nSee module docstring for the wire shape, error envelopes, IDOR\ndiscipline, kill switch, and observability contract.","operationId":"get_wallet_payment_history_wallet_payment_history_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/wallet/limits":{"get":{"tags":["wallet"],"summary":"Get Wallet Limits","description":"Return the principal's persisted limits row (or null).\n\nSee module docstring for the wire shape, IDOR discipline, kill\nswitch, and error envelopes.","operationId":"get_wallet_limits_wallet_limits_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"tags":["wallet"],"summary":"Post Wallet Limits","description":"Upsert the principal's limits row.\n\nIdempotent: same payload twice produces the same row but moves\n``updated_at``. Returns the upserted wire shape.","operationId":"post_wallet_limits_wallet_limits_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WalletLimitsRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/wallet/notifications":{"get":{"tags":["wallet"],"summary":"Get Wallet Notifications","description":"Return the principal's persisted notifications row (or null).","operationId":"get_wallet_notifications_wallet_notifications_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"tags":["wallet"],"summary":"Post Wallet Notifications","description":"Upsert the principal's notifications row.\n\nIdempotent: same payload twice produces the same row but moves\n``updated_at``. Returns the upserted wire shape.","operationId":"post_wallet_notifications_wallet_notifications_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WalletNotificationsRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/wallet/limit-bumps":{"get":{"tags":["wallet"],"summary":"List Limit Bumps","description":"List the verified principal's pending limit bumps (wallet:read).","operationId":"list_limit_bumps_wallet_limit_bumps_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/wallet/limit-bumps/{bump_id}/approve":{"post":{"tags":["wallet"],"summary":"Approve Limit Bump","description":"Approve a pending limit bump (wallet:write): apply the bump + record.","operationId":"approve_limit_bump_wallet_limit_bumps__bump_id__approve_post","parameters":[{"name":"bump_id","in":"path","required":true,"schema":{"type":"string","title":"Bump Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/wallet/limit-bumps/{bump_id}/decline":{"post":{"tags":["wallet"],"summary":"Decline Limit Bump","description":"Decline a pending limit bump (wallet:write): record only, no bump.","operationId":"decline_limit_bump_wallet_limit_bumps__bump_id__decline_post","parameters":[{"name":"bump_id","in":"path","required":true,"schema":{"type":"string","title":"Bump Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/audit/{tenant_id}/range":{"get":{"tags":["audit"],"summary":"Get Audit Range","description":"Return the canonical audit-range response.\n\nQuery parameters:\n\n- ``from_seq``: inclusive low end of the range. Defaults to 1\n  if omitted.\n- ``to_seq``: inclusive high end of the range. Defaults to the\n  tenant's current max seq if omitted.\n\nAuthentication: ``Authorization: PrivateKeyJwt <jwt>`` with\n``client_id`` query parameter naming the registered client. The\nclient must have the ``audit:read`` scope and must match the\nrequested tenant per the M19 binding convention\n(``client.client_id == tenant_id``).\n\nRate limit: 10 requests / minute / tenant (section\n17.6(g)); the 11th request in the same wall-clock minute is\nrefused with ``429``.\n\nThe response body shape is what\n:func:`app.services.audit_chain.fetch_range_response` builds.\nThe ``Cache-Control: no-store`` header is set because the\nresponse carries actor identifiers and event payloads that the\nintermediate caching layer must NOT mirror.","operationId":"get_audit_range_audit__tenant_id__range_get","parameters":[{"name":"tenant_id","in":"path","required":true,"schema":{"type":"string","title":"Tenant Id"}},{"name":"from_seq","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"From Seq"}},{"name":"to_seq","in":"query","required":false,"schema":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"To Seq"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/privacy/dsar/portability-schema.json":{"get":{"tags":["privacy"],"summary":"Get Portability Schema","description":"Return the JSON Schema for the Article 20 portability dump.\n\nPublished unauthenticated so a re-importer can validate a dump\nwithout holding a JWT-AT for the originating data subject.","operationId":"get_portability_schema_privacy_dsar_portability_schema_json_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/privacy/dsar/access":{"post":{"tags":["privacy"],"summary":"Dsar Access","description":"Article 15 right-of-access endpoint.\n\nReturns ``{\"status\": \"queued\", \"request_id\", \"estimated_delivery\"}``;\nthe dump is generated asynchronously and posted to the email\naddress the subject verified at registration. The endpoint emits\none audit-chain entry per call.\n\nIdempotency-Key support (outer ring):\nwhen the request carries an ``Idempotency-Key`` header the\nresponse (the queued ``request_id`` + the estimated delivery\ntimestamp) is cached at ``oid4pay:idem:as:dsar:access:<k>`` with a\n24 h TTL. Article 15 access requests are long-running; an agent\nthat crashes mid-request and replays the same key receives the\noriginal ``request_id`` rather than enqueueing a duplicate. A\nretry with the same key but a different body returns 409 Conflict\n(the body is empty in normal use; the defence-in-depth path\nhandles a future body shape addition without silent corruption).\nA request without the header runs the handler unconditionally so\nthe no-key path matches the pre-idempotency behaviour.","operationId":"dsar_access_privacy_dsar_access_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/privacy/dsar/rectification":{"post":{"tags":["privacy"],"summary":"Dsar Rectification","description":"Article 16 rectification endpoint.\n\nBody shape::\n\n    {\"email\": \"new@example.com\", \"display_name\": \"Alice Example\"}\n\nOnly the named fields are permitted; an unknown key returns 400.","operationId":"dsar_rectification_privacy_dsar_rectification_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/privacy/dsar/erasure":{"post":{"tags":["privacy"],"summary":"Dsar Erasure","description":"Article 17 right-to-be-forgotten endpoint.\n\nSide effects:\n\n1. Marks the principal row ``erased=True``; NULL the PII columns.\n2. Revokes every active agent_client owned by the principal.\n3. Cascade-revokes every issued token family (handled separately\n   by the wallet revoke pipeline; here we mark the registry rows).\n4. Audit-chain entries are RETAINED with PII redacted (INSERT-only + Article 17(3)(b)).\n\nReturns the redaction summary so the data subject sees exactly\nwhich columns were affected.","operationId":"dsar_erasure_privacy_dsar_erasure_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/privacy/dsar/portability":{"post":{"tags":["privacy"],"summary":"Dsar Portability","description":"Article 20 portability endpoint.\n\nReturns the structured, machine-readable dump synchronously (the\nresponse body itself is the dump). The asynchronous email delivery\nthat the ``/access`` endpoint uses is not required here because\nArticle 20 explicitly contemplates direct-from-controller\nstructured export.\n\nIdempotency-Key support (outer ring):\nwhen the request carries an ``Idempotency-Key`` header the dump\nis cached at ``oid4pay:idem:as:dsar:portability:<k>`` with a 1 h\nTTL. The dump for the same principal is stable on the scale of an\nhour; a caller retrying within that window receives the\nbyte-identical dump without re-running the assembly pipeline (which\ntouches the audit chain, agent_clients, and Redis revocation\nmarkers). Longer TTLs would risk serving stale data after a\nrectification or erasure between the original call and the retry.\nA retry with the same key but a different body returns 409 Conflict\n(defence-in-depth; the body is empty in normal use). A request\nwithout the header runs the handler unconditionally so the no-key\npath matches the pre-idempotency behaviour.","operationId":"dsar_portability_privacy_dsar_portability_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/privacy/dsar/restriction":{"post":{"tags":["privacy"],"summary":"Dsar Restriction","description":"Article 18 restriction-of-processing endpoint.\n\nSets ``processing_restricted=True`` on the principal row. The\ntoken endpoint MUST consult this flag and refuse to issue new\ntokens (the gate lives in ``app.services.token``; this endpoint\nonly flips the flag and records the audit entry).","operationId":"dsar_restriction_privacy_dsar_restriction_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/privacy/dsar/objection":{"post":{"tags":["privacy"],"summary":"Dsar Objection","description":"Article 21 right-to-object endpoint.\n\nSets ``marketing_objected=True``. The AS does not currently emit\nmarketing traffic; the flag is honoured by downstream surfaces\n(docs site, wallet portal) and persisted for compliance audit.","operationId":"dsar_objection_privacy_dsar_objection_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/eudiw/verify":{"post":{"tags":["eudiw"],"summary":"Verify Presentation","description":"Verify an OpenID4VP presentation; return the attestation.\n\nThe body MUST be JSON with ``vp_token``,\n``presentation_submission``, and ``region``; ``expected_aud``\nand ``expected_nonce`` MAY be supplied to bind the verification\nto the authorization-request context.\n\nReturns 200 with the verified attestation; on failure returns\nthe error envelope with the matching\nHTTP status.\n\nKill switch: :attr:`Settings.eudiw_enabled`. When false the\nroute returns 503 ``service_unavailable``.","operationId":"verify_presentation_eudiw_verify_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/eudiw/request":{"post":{"tags":["eudiw"],"summary":"Create Request","description":"Create a verifier-initiated OpenID4VP Authorization Request.\n\nBody (application/json): ``dcql_query`` (REQUIRED, section\n6B.10), optional ``transaction_data`` (the\npayment binding: ``offer_digest`` / ``amount_minor`` /\n``currency`` / ``merchant`` / ``mandate_ref``), optional\n``response_mode`` (defaults ``direct_post.jwt``), optional\n``jkt`` (holder DPoP thumbprint).\n\nReturns 200 with ``request_uri`` (passed by reference, section\n6B.8.1), ``deep_link`` / ``qr_payload`` (cross/same-device\ntransport), and the ``nonce`` / ``state``. Kill-switched on\n``Settings.eudiw_request_side_enabled``; when off (or the RP\nAccess Certificate is unprovisioned) returns 503 / the section\n6B.7 ``request_object_invalid`` envelope.","operationId":"create_request_eudiw_request_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/eudiw/request/{request_id}":{"post":{"tags":["eudiw"],"summary":"Get Request Object","description":"Serve the signed request object (``request_uri_method=post``).\n\nthe wallet POSTs ``wallet_metadata``\n``wallet_nonce`` (form or JSON) to the ``request_uri``; the\nverifier returns the signed Authorization Request object (the\nOAuth JAR Request Object, RFC 9101), echoing ``wallet_nonce`` to\nbind it to that retrieval. The response media type is\n``application/oauth-authz-req+jwt``.\n\n``request_uri_method=post`` is pinned MANDATORY (section\n6B.8.1); a GET is intentionally not offered (so the wallet\nmetadata / nonce exchange always happens).","operationId":"get_request_object_eudiw_request__request_id__post","parameters":[{"name":"request_id","in":"path","required":true,"schema":{"type":"string","title":"Request Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/oauth/status-list/{list_id}":{"get":{"tags":["status-list"],"summary":"Get Status List","description":"Serve the signed Status List Token for ``list_id``.\n\n Kill-switched on\n``Settings.eudiw_statuslist_enabled``; v1 serves a single list\n(``Settings.eudiw_statuslist_id``). A request for any other\n``list_id`` is 404 (the v1 publisher has exactly one list; an\nunknown id is not a server error).","operationId":"get_status_list_oauth_status_list__list_id__get","parameters":[{"name":"list_id","in":"path","required":true,"schema":{"type":"string","title":"List Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/oid4vci/credential-offer":{"post":{"tags":["oid4vci"],"summary":"Post Credential Offer","description":"Create a pre-authorized OpenID4VCI Credential Offer.\n\nBody (application/json): ``credential_configuration_id`` (one of\n``oid4pay_mandate`` / ``oid4pay_age_verified_buyer``) + ``subject``\n(the issuer-supplied claim inputs; for the mandate the section\n6A.3 MandateIssueRequest inputs, for the attestation the minimal\nover-18 result). Returns the ``credential_offer`` +\n``c_nonce``.","operationId":"post_credential_offer_oid4vci_credential_offer_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/oid4vci/credential":{"post":{"tags":["oid4vci"],"summary":"Post Credential","description":"The OpenID4VCI credential endpoint: verify proof + issue.\n\nBody (application/json): ``pre-authorized_code`` + ``proof``\n(``{proof_type: \"jwt\", jwt: \"<proof JWS>\"}``). The proof JWS\ncarries the holder key in its ``jwk`` header; the credential is\nbound to it. Returns the OpenID4VCI Credential Response.\nFail-closed on an unknown/consumed code or an unverified proof.","operationId":"post_credential_oid4vci_credential_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/webhooks/stripe":{"post":{"tags":["webhooks"],"summary":"Stripe Webhook","description":"Receive a verified Stripe event.\n\nOrder of checks:\n\n1. Kill switch: ``stripe_webhook_enabled`` must be true and the\n   secret must be set; otherwise 503.\n2. ``Stripe-Signature`` header presence; missing -> 400.\n3. HMAC verification + timestamp tolerance via\n   :func:`verify_signature`; failure -> 400.\n4. Idempotency dedup via the shared :func:`idempotent` decorator\n   keyed on ``event.id``; a retry hits the cached response.\n5. Dispatch to :func:`route_event` on cache miss.\n\nThe response body is ``{\"received\": true, \"event_id\": ...}`` on\na first delivery; a redelivery returns the same body with an\nadditional ``\"duplicate\": true`` marker so the wire shape lets\ndownstream observability distinguish the two without inspecting\nRedis.","operationId":"stripe_webhook_webhooks_stripe_post","parameters":[{"name":"Stripe-Signature","in":"header","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Stripe-Signature"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"SettleRequest":{"properties":{"mandate":{"type":"string","minLength":1,"title":"Mandate"},"idempotency_key":{"type":"string","minLength":1,"title":"Idempotency Key"},"connected_account_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Connected Account Id"},"application_fee_minor":{"type":"integer","minimum":0.0,"title":"Application Fee Minor","default":0}},"type":"object","required":["mandate","idempotency_key"],"title":"SettleRequest"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"WalletLimitsRequest":{"properties":{"daily_cap_minor":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Daily Cap Minor"},"currency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Currency"},"single_txn_cap_minor":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Single Txn Cap Minor"},"velocity_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Velocity Count"},"velocity_window_seconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Velocity Window Seconds"}},"additionalProperties":false,"type":"object","title":"WalletLimitsRequest","description":"POST /wallet/limits request body (O13).\n\nEvery field is optional so an operator can update caps\nincrementally. Strict-mode validation is layered:\nPydantic catches type errors (negative ints survive the cast in\nPython; the service-layer ``_validate_limits_input`` catches them)."},"WalletNotificationsRequest":{"properties":{"rcs_enabled":{"type":"boolean","title":"Rcs Enabled","default":false},"rcs_phone_e164":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Rcs Phone E164"},"email_enabled":{"type":"boolean","title":"Email Enabled","default":true}},"additionalProperties":false,"type":"object","title":"WalletNotificationsRequest","description":"POST /wallet/notifications request body (O13)."}}}}