diff --git a/.cspell.json b/.cspell.json index 3825edcf9..b9e32737d 100644 --- a/.cspell.json +++ b/.cspell.json @@ -22,7 +22,8 @@ "(\"x\"|\"y\")\\s*:\\s*\"[A-Za-z0-9_-]+\"", "sig1=:[A-Za-z0-9+/=_.]+:", "eyJ[A-Za-z0-9_-]+\\.\\.?[A-Za-z0-9_-]*", - "M[A-Z][A-Za-z0-9]{3,}\\.\\.\\." + "M[A-Z][A-Za-z0-9]{3,}\\.\\.\\.", + "(\"cursor\"|cursor)\\s*:\\s*(\"[^\"]*\"|'[^']*')" ], "dictionaryDefinitions": [ { diff --git a/.cspell/custom-words.txt b/.cspell/custom-words.txt index 051ff147b..f3e7b5f40 100644 --- a/.cspell/custom-words.txt +++ b/.cspell/custom-words.txt @@ -36,18 +36,23 @@ Shopee Splitit Streamable Stripe +Talla Target UCP Ulta Visa +Variante Wayfair WORKTREE Worldpay +Zapatillas Zalando adyen agentic +amortiguación atok backorder +barcodes checkout credentialless credentialization @@ -59,13 +64,17 @@ fontawesome fpan fulfillable gpay +gtin ingestions inlinehilite lifecycles +ligeras linenums +linkignore llms llmstxt mastercard +midsole mkdocs mtok openapi @@ -73,10 +82,12 @@ openrpc paypal permissionless podman +preorder preorders proto protobuf pymdownx +reactiva renderable repudiable schemas @@ -84,10 +95,16 @@ sdjwt shopify streamable superfences +talla +tracción upsell upsells +variante vulnz worktree yaml +zapatillas yml +EDITMSG +jwks keyid diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 833bcb9a0..07d80cb86 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,35 +1,35 @@ # Universal Commerce Protocol (UCP) Codeowners # Default for all files. -* @Universal-Commerce-Protocol/maintainers +* @Universal-Commerce-Protocol/maintainers @Universal-Commerce-Protocol/tech-council @Universal-Commerce-Protocol/governance-council # Infrastructure, Tooling & Configuration -/.github/ @Universal-Commerce-Protocol/devops-maintainers -/scripts/ @Universal-Commerce-Protocol/devops-maintainers -/generated/ @Universal-Commerce-Protocol/devops-maintainers -/hooks.py @Universal-Commerce-Protocol/devops-maintainers -/main.py @Universal-Commerce-Protocol/devops-maintainers -/generate_ts_schema_types.js @Universal-Commerce-Protocol/devops-maintainers -/.cspell/ @Universal-Commerce-Protocol/devops-maintainers -/.cspell.json @Universal-Commerce-Protocol/devops-maintainers -/.pre-commit-config.yaml @Universal-Commerce-Protocol/devops-maintainers -/.prettierignore @Universal-Commerce-Protocol/devops-maintainers -/.prettierrc @Universal-Commerce-Protocol/devops-maintainers -/.stylelintrc.json @Universal-Commerce-Protocol/devops-maintainers -/biome.json @Universal-Commerce-Protocol/devops-maintainers -/mkdocs.yml @Universal-Commerce-Protocol/devops-maintainers -/.gitignore @Universal-Commerce-Protocol/devops-maintainers -/pyproject.toml @Universal-Commerce-Protocol/devops-maintainers -/uv.lock @Universal-Commerce-Protocol/devops-maintainers -/package.json @Universal-Commerce-Protocol/devops-maintainers -/package-lock.json @Universal-Commerce-Protocol/devops-maintainers +/.github/ @Universal-Commerce-Protocol/devops-maintainers @Universal-Commerce-Protocol/maintainers @Universal-Commerce-Protocol/tech-council @Universal-Commerce-Protocol/governance-council +/scripts/ @Universal-Commerce-Protocol/devops-maintainers @Universal-Commerce-Protocol/maintainers @Universal-Commerce-Protocol/tech-council @Universal-Commerce-Protocol/governance-council +/generated/ @Universal-Commerce-Protocol/devops-maintainers @Universal-Commerce-Protocol/maintainers @Universal-Commerce-Protocol/tech-council @Universal-Commerce-Protocol/governance-council +/hooks.py @Universal-Commerce-Protocol/devops-maintainers @Universal-Commerce-Protocol/maintainers @Universal-Commerce-Protocol/tech-council @Universal-Commerce-Protocol/governance-council +/main.py @Universal-Commerce-Protocol/devops-maintainers @Universal-Commerce-Protocol/maintainers @Universal-Commerce-Protocol/tech-council @Universal-Commerce-Protocol/governance-council +/.cspell/ @Universal-Commerce-Protocol/devops-maintainers @Universal-Commerce-Protocol/maintainers @Universal-Commerce-Protocol/tech-council @Universal-Commerce-Protocol/governance-council +/.cspell.json @Universal-Commerce-Protocol/devops-maintainers @Universal-Commerce-Protocol/maintainers @Universal-Commerce-Protocol/tech-council @Universal-Commerce-Protocol/governance-council +/.pre-commit-config.yaml @Universal-Commerce-Protocol/devops-maintainers @Universal-Commerce-Protocol/maintainers @Universal-Commerce-Protocol/tech-council @Universal-Commerce-Protocol/governance-council +/.prettierignore @Universal-Commerce-Protocol/devops-maintainers @Universal-Commerce-Protocol/maintainers @Universal-Commerce-Protocol/tech-council @Universal-Commerce-Protocol/governance-council +/.prettierrc @Universal-Commerce-Protocol/devops-maintainers @Universal-Commerce-Protocol/maintainers @Universal-Commerce-Protocol/tech-council @Universal-Commerce-Protocol/governance-council +/.stylelintrc.json @Universal-Commerce-Protocol/devops-maintainers @Universal-Commerce-Protocol/maintainers @Universal-Commerce-Protocol/tech-council @Universal-Commerce-Protocol/governance-council +/biome.json @Universal-Commerce-Protocol/devops-maintainers @Universal-Commerce-Protocol/maintainers @Universal-Commerce-Protocol/tech-council @Universal-Commerce-Protocol/governance-council +/mkdocs.yml @Universal-Commerce-Protocol/devops-maintainers @Universal-Commerce-Protocol/maintainers @Universal-Commerce-Protocol/tech-council @Universal-Commerce-Protocol/governance-council +/.gitignore @Universal-Commerce-Protocol/devops-maintainers @Universal-Commerce-Protocol/maintainers @Universal-Commerce-Protocol/tech-council @Universal-Commerce-Protocol/governance-council +/pyproject.toml @Universal-Commerce-Protocol/devops-maintainers @Universal-Commerce-Protocol/maintainers @Universal-Commerce-Protocol/tech-council @Universal-Commerce-Protocol/governance-council +/uv.lock @Universal-Commerce-Protocol/devops-maintainers @Universal-Commerce-Protocol/maintainers @Universal-Commerce-Protocol/tech-council @Universal-Commerce-Protocol/governance-council +/package.json @Universal-Commerce-Protocol/devops-maintainers @Universal-Commerce-Protocol/maintainers @Universal-Commerce-Protocol/tech-council @Universal-Commerce-Protocol/governance-council +/package-lock.json @Universal-Commerce-Protocol/devops-maintainers @Universal-Commerce-Protocol/maintainers @Universal-Commerce-Protocol/tech-council @Universal-Commerce-Protocol/governance-council # Documentation -/docs/ @Universal-Commerce-Protocol/devops-maintainers -/docs/specification/ @Universal-Commerce-Protocol/maintainers +/docs/ @Universal-Commerce-Protocol/devops-maintainers @Universal-Commerce-Protocol/maintainers @Universal-Commerce-Protocol/tech-council @Universal-Commerce-Protocol/governance-council +/docs/specification/ @Universal-Commerce-Protocol/maintainers @Universal-Commerce-Protocol/tech-council @Universal-Commerce-Protocol/governance-council # Core Protocol -/source/ @Universal-Commerce-Protocol/tech-council +/source/ @Universal-Commerce-Protocol/tech-council @Universal-Commerce-Protocol/governance-council # Governance +/LICENSE @Universal-Commerce-Protocol/governance-council /.github/CODEOWNERS @Universal-Commerce-Protocol/governance-council diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index c87fea289..94cdfa44e 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,8 @@ on: - main paths: - ".github/workflows/docs.yml" - - "requirements-docs.txt" + - "pyproject.toml" + - "uv.lock" - "mkdocs.yml" - "main.py" - "hooks.py" @@ -63,7 +64,7 @@ jobs: uses: astral-sh/setup-uv@v7 - name: Install documentation dependencies - run: uv sync + run: uv sync --all-groups - name: Lint YAML files run: uv run yamllint -c .github/linters/.yamllint.yml . @@ -74,6 +75,14 @@ jobs: with: files: source/** + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache Cargo + uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true + - name: Install ucp-schema for runtime resolution run: | cargo install ucp-schema @@ -110,9 +119,19 @@ jobs: echo "Set SITE_URL to $PAGES_URL" echo "SITE_URL=$PAGES_URL" >> $GITHUB_ENV - - name: Build Documentation (PR Check) - if: github.event_name == 'pull_request' - run: uv run mkdocs build --strict + - name: Build and Verify Documentation Site (Main/PR) + if: github.ref == 'refs/heads/main' || github.event.pull_request.base.ref == 'main' + run: | + # Create a full local preview (including mike logic) for validation + bash scripts/build_local.sh --main-only + uv run python scripts/check_links.py local_preview + + - name: Build and Verify Specification Docs (Release Branches) + if: startsWith(github.ref, 'refs/heads/release/') || startsWith(github.event.pull_request.base.ref, 'release/') + run: | + export DOCS_MODE=spec + uv run mkdocs build --strict + uv run python scripts/check_links.py site - name: Deploy development version from main branch if: github.event_name == 'push' && github.ref == 'refs/heads/main' @@ -123,7 +142,7 @@ jobs: # 2. Build Root Site (DOCS_MODE=root) export DOCS_MODE=root - uv run mkdocs build + uv run mkdocs build --strict # 3. Deploy Root Site to gh-pages root # Fetch and checkout gh-pages @@ -134,6 +153,10 @@ jobs: cp -r site/* . rm -rf site + # Add redirects for all specification files + rm -rf specification + ln -s latest/specification specification + # Commit and push git add . git commit -m "Deploy root site from main" --allow-empty @@ -146,8 +169,22 @@ jobs: # Extract the date (e.g., release/2026-01-11 -> 2026-01-11) VERSION_NAME=${GITHUB_REF#refs/heads/release/} - # Deploy this version, tag it as 'latest', and set it as the default site root - uv run mike deploy --push --update-aliases $VERSION_NAME latest + # Fetch all release branches to determine if this is the latest + git fetch origin "+refs/heads/release/*:refs/remotes/origin/release/*" + + # Find the highest version number among all release branches + LATEST_VERSION=$(git branch -r --list "origin/release/*" | sed 's|origin/release/||' | sort -V | tail -n 1 | tr -d ' ') + + echo "Deploying version: $VERSION_NAME" + echo "Latest detected version: $LATEST_VERSION" + + if [ "$VERSION_NAME" = "$LATEST_VERSION" ]; then + echo "This is the latest version. Updating 'latest' alias." + uv run mike deploy --push --update-aliases "$VERSION_NAME" latest + else + echo "This is NOT the latest version. Deploying without updating 'latest' alias." + uv run mike deploy --push "$VERSION_NAME" + fi - name: Create GitHub Release and Tag if: startsWith(github.ref, 'refs/heads/release/') diff --git a/.github/workflows/linter.yaml b/.github/workflows/linter.yaml index 33096bf63..43994b289 100644 --- a/.github/workflows/linter.yaml +++ b/.github/workflows/linter.yaml @@ -19,12 +19,23 @@ on: branches: - main - 'release/**' + paths: + - ".github/workflows/linter.yaml" + - ".github/workflows/schema-validation.yml" + - ".github/linters/**" + - ".pre-commit-config.yaml" + - "pyproject.toml" + - "package.json" + - "package-lock.json" + - "biome.json" + - "scripts/**" + - "source/**" + - "docs/**" + - "*.py" permissions: contents: read # Required to checkout the code packages: read # Required to pull the Super-Linter docker image - statuses: write # Required to fix the 403 error (updating status checks) - pull-requests: write # Required for posting comments on PRs jobs: build: @@ -41,7 +52,8 @@ jobs: uses: super-linter/super-linter/slim@v8.5.0 env: DEFAULT_BRANCH: ${{ github.base_ref }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MULTI_STATUS: false + ENABLE_GITHUB_PULL_REQUEST_SUMMARY_COMMENT: false MARKDOWN_CONFIG_FILE: ".markdownlint.json" PYTHON_RUFF_CONFIG_FILE: ../../pyproject.toml PYTHON_RUFF_FORMAT_CONFIG_FILE: ../../pyproject.toml @@ -49,7 +61,8 @@ jobs: LOG_LEVEL: INFO SHELLCHECK_OPTS: -e SC1091 -e 2086 VALIDATE_ALL_CODEBASE: false - FILTER_REGEX_EXCLUDE: "^(\\.github/|\\.vscode/).*|CODE_OF_CONDUCT.md|CHANGELOG.md" + FILTER_REGEX_EXCLUDE: >- + ^(\\.github/|\\.vscode/).*|CODE_OF_CONDUCT.md|CHANGELOG.md VALIDATE_BIOME_FORMAT: false VALIDATE_PYTHON_BLACK: false VALIDATE_PYTHON_FLAKE8: false @@ -67,3 +80,4 @@ jobs: VALIDATE_GITHUB_ACTIONS_ZIZMOR: false VALIDATE_JSCPD: false VALIDATE_SPELL_CODESPELL: false # Using cspell (spellcheck.yaml) + VALIDATE_OPENAPI: false diff --git a/.linkignore b/.linkignore new file mode 100644 index 000000000..09965fb34 --- /dev/null +++ b/.linkignore @@ -0,0 +1,6 @@ +# Ignore ucp.dev dummy links +https://ucp\.dev/specification/reference\?v=2026-01-11 + +# Ignore latest specification links because the target version folder is not pulled down during --main-only checks +/latest/specification/.* +latest/specification/.* \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1077037d3..7afb86005 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -55,5 +55,6 @@ repos: hooks: - id: prettier name: prettier-css - types: [css] # Only run on CSS files - # If you want it to run on EVERYTHING (JS, JSON, MD), remove the 'types' line. + # Only run on CSS files + types: [css] + # To run on everything (JS, JSON, MD), remove the 'types' line. diff --git a/docs/assets/partner/endorsed/Affirm.svg b/docs/assets/partner/endorsed/Affirm.svg index 1403f1884..801511c32 100644 --- a/docs/assets/partner/endorsed/Affirm.svg +++ b/docs/assets/partner/endorsed/Affirm.svg @@ -1,7 +1,7 @@ - diff --git a/docs/assets/ucp-diagram-mobile.jpg b/docs/assets/ucp-diagram-mobile.jpg new file mode 100644 index 000000000..0435dfc62 Binary files /dev/null and b/docs/assets/ucp-diagram-mobile.jpg differ diff --git a/docs/assets/ucp-diagram-mobile.png b/docs/assets/ucp-diagram-mobile.png deleted file mode 100644 index a24ade3e5..000000000 Binary files a/docs/assets/ucp-diagram-mobile.png and /dev/null differ diff --git a/docs/assets/ucp-diagram.jpg b/docs/assets/ucp-diagram.jpg index d8ff97955..3534cdbb0 100644 Binary files a/docs/assets/ucp-diagram.jpg and b/docs/assets/ucp-diagram.jpg differ diff --git a/docs/documentation/core-concepts.md b/docs/documentation/core-concepts.md index 0eefb2f3f..8dd8ae06e 100644 --- a/docs/documentation/core-concepts.md +++ b/docs/documentation/core-concepts.md @@ -40,7 +40,7 @@ Its primary goal is to enable: - + UCP Diagram diff --git a/docs/documentation/schema-authoring.md b/docs/documentation/schema-authoring.md index 8160d1389..d04efe168 100644 --- a/docs/documentation/schema-authoring.md +++ b/docs/documentation/schema-authoring.md @@ -159,17 +159,17 @@ by `name` rather than arrays of objects with `name` fields. ```json { "capabilities": { - "dev.ucp.shopping.checkout": [{"version": "2026-01-11"}], - "dev.ucp.shopping.fulfillment": [{"version": "2026-01-11"}] + "dev.ucp.shopping.checkout": [{"version": "{{ ucp_version }}"}], + "dev.ucp.shopping.fulfillment": [{"version": "{{ ucp_version }}"}] }, "services": { "dev.ucp.shopping": [ - {"version": "2026-01-11", "transport": "rest"}, - {"version": "2026-01-11", "transport": "mcp"} + {"version": "{{ ucp_version }}", "transport": "rest"}, + {"version": "{{ ucp_version }}", "transport": "mcp"} ] }, "payment_handlers": { - "com.google.pay": [{"id": "gpay_1234", "version": "2026-01-11", "available_instruments": [{"type": "google_pay_card"}]}] + "com.google.pay": [{"id": "gpay_1234", "version": "{{ ucp_version }}", "available_instruments": [{"type": "google_pay_card"}]}] } } ``` @@ -205,9 +205,9 @@ Each entity type defines **three variants** for different contexts: ```json { "dev.ucp.shopping.fulfillment": [{ - "version": "2026-01-11", - "spec": "https://ucp.dev/specification/fulfillment", - "schema": "https://ucp.dev/schemas/shopping/fulfillment.json", + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/fulfillment", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/fulfillment.json", "config": { "supports_multi_group": true } @@ -220,7 +220,7 @@ Each entity type defines **three variants** for different contexts: ```json { "dev.ucp.shopping.fulfillment": [{ - "version": "2026-01-11", + "version": "{{ ucp_version }}", "config": { "allows_multi_destination": {"shipping": true} } @@ -234,7 +234,7 @@ Each entity type defines **three variants** for different contexts: { "ucp": { "capabilities": { - "dev.ucp.shopping.fulfillment": [{"version": "2026-01-11"}] + "dev.ucp.shopping.fulfillment": [{"version": "{{ ucp_version }}"}] } } } @@ -256,6 +256,31 @@ Define all three in your schema's `$defs`: } ``` +## String Vocabularies vs Enums + +Prefer **open string vocabularies** with documented well-known values over closed +`enum` arrays. Enums are a one-way door: adding a new value is a breaking change +for strict validators, and removing one breaks existing producers. + +```json +// PREFER: open vocabulary — extensible without schema changes +"type": { + "type": "string", + "description": "Media type. Well-known values: `image`, `video`, `model_3d`." +} + +// AVOID: closed enum — adding `audio` requires a schema version bump +"type": { + "type": "string", + "enum": ["image", "video", "model_3d"] +} +``` + +**Use `enum` only for provably closed sets** where new values would represent a +fundamental protocol change (e.g., `checkout.status: open | completed | expired`). +If the set might grow as new use cases emerge, use an open string with well-known +values documented in the `description`. + ## Versioning Strategy ### UCP Capabilities (`dev.ucp.*`) @@ -285,9 +310,9 @@ A capability schema defines both payload structure and declaration variants: ```json { "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://ucp.dev/schemas/shopping/checkout.json", + "$id": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/checkout.json", "name": "dev.ucp.shopping.checkout", - "version": "2026-01-11", + "version": "{{ ucp_version }}", "title": "Checkout", "description": "Base checkout schema. Extensions compose via allOf.", diff --git a/docs/index.md b/docs/index.md index 65a243066..ce85a15c5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -287,7 +287,7 @@ image: assets/banner.png "token_endpoint": "https://example.com/oauth2/token", "revocation_endpoint": "https://example.com/oauth2/revoke", "scopes_supported": [ - "ucp:scopes:checkout_session", + "dev.ucp.shopping.scopes.checkout_session" ], "response_types_supported": [ "code" diff --git a/docs/specification/ap2-mandates.md b/docs/specification/ap2-mandates.md index 4580b0c91..b23ef7be7 100644 --- a/docs/specification/ap2-mandates.md +++ b/docs/specification/ap2-mandates.md @@ -70,16 +70,16 @@ Businesses declare support by adding `dev.ucp.shopping.ap2_mandate` to their "capabilities": { "dev.ucp.shopping.checkout": [ { - "version": "2026-01-11", - "spec": "https://ucp.dev/specification/checkout", - "schema": "https://ucp.dev/schemas/shopping/checkout.json" + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/checkout", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/checkout.json" } ], "dev.ucp.shopping.ap2_mandate": [ { - "version": "2026-01-11", - "spec": "https://ucp.dev/specification/ap2-mandates", - "schema": "https://ucp.dev/schemas/shopping/ap2_mandate.json", + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/ap2-mandates", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/ap2_mandate.json", "extends": "dev.ucp.shopping.checkout", "config": { "vp_formats_supported": { @@ -420,7 +420,7 @@ checkout. ## Schema -### Business Authorization +### Business Authorization {: #merchant-authorization } {{ extension_schema_fields('ap2_mandate.json#/$defs/merchant_authorization', 'ap2-mandates') }} diff --git a/docs/specification/buyer-consent.md b/docs/specification/buyer-consent.md index 07301f70c..8d82eb9fc 100644 --- a/docs/specification/buyer-consent.md +++ b/docs/specification/buyer-consent.md @@ -39,7 +39,7 @@ Businesses advertise consent support in their profile: "capabilities": { "dev.ucp.shopping.buyer_consent": [ { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "extends": "dev.ucp.shopping.checkout" } ] diff --git a/docs/specification/cart-mcp.md b/docs/specification/cart-mcp.md index cd065bc3c..ef3fb84e2 100644 --- a/docs/specification/cart-mcp.md +++ b/docs/specification/cart-mcp.md @@ -29,13 +29,13 @@ Businesses advertise MCP transport availability through their UCP profile at ```json { "ucp": { - "version": "2026-01-15", + "version": "{{ ucp_version }}", "services": { "dev.ucp.shopping": { - "version": "2026-01-15", - "spec": "https://ucp.dev/specification/overview", + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/overview", "mcp": { - "schema": "https://ucp.dev/services/shopping/mcp.openrpc.json", + "schema": "https://ucp.dev/{{ ucp_version }}/services/shopping/mcp.openrpc.json", "endpoint": "https://business.example.com/ucp/mcp" } } @@ -43,15 +43,15 @@ Businesses advertise MCP transport availability through their UCP profile at "capabilities": [ { "name": "dev.ucp.shopping.checkout", - "version": "2026-01-11", - "spec": "https://ucp.dev/specification/checkout", - "schema": "https://ucp.dev/schemas/shopping/checkout.json" + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/checkout", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/checkout.json" }, { "name": "dev.ucp.shopping.cart", - "version": "2026-01-15", - "spec": "https://ucp.dev/specification/cart", - "schema": "https://ucp.dev/schemas/shopping/cart.json" + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/cart", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/cart.json" } ] } @@ -167,15 +167,15 @@ Maps to the [Create Cart](cart.md#create-cart) operation. "structuredContent": { "cart": { "ucp": { - "version": "2026-01-15", + "version": "{{ ucp_version }}", "capabilities": [ { "name": "dev.ucp.shopping.checkout", - "version": "2026-01-11" + "version": "{{ ucp_version }}" }, { "name": "dev.ucp.shopping.cart", - "version": "2026-01-15" + "version": "{{ ucp_version }}" } ] }, @@ -220,6 +220,34 @@ Maps to the [Create Cart](cart.md#create-cart) operation. } ``` +=== "Error Response" + + All items out of stock — no cart resource is created: + + ```json + { + "jsonrpc": "2.0", + "id": 1, + "result": { + "structuredContent": { + "ucp": { "version": "2026-01-15", "status": "error" }, + "messages": [ + { + "type": "error", + "code": "out_of_stock", + "content": "All requested items are currently out of stock", + "severity": "unrecoverable" + } + ], + "continue_url": "https://merchant.com/" + }, + "content": [ + {"type": "text", "text": "{\"ucp\":{...},\"messages\":[...]}"} + ] + } + } + ``` + ### `get_cart` Maps to the [Get Cart](cart.md#get-cart) operation. @@ -265,15 +293,15 @@ Maps to the [Get Cart](cart.md#get-cart) operation. "structuredContent": { "cart": { "ucp": { - "version": "2026-01-15", + "version": "{{ ucp_version }}", "capabilities": [ { "name": "dev.ucp.shopping.checkout", - "version": "2026-01-11" + "version": "{{ ucp_version }}" }, { "name": "dev.ucp.shopping.cart", - "version": "2026-01-15" + "version": "{{ ucp_version }}" } ] }, @@ -328,11 +356,11 @@ Maps to the [Get Cart](cart.md#get-cart) operation. "structuredContent": { "cart": { "ucp": { - "version": "2026-01-15", + "version": "{{ ucp_version }}", "capabilities": [ { "name": "dev.ucp.shopping.cart", - "version": "2026-01-15" + "version": "{{ ucp_version }}" } ] }, @@ -340,7 +368,8 @@ Maps to the [Get Cart](cart.md#get-cart) operation. { "type": "error", "code": "not_found", - "content": "Cart not found or has expired" + "content": "Cart not found or has expired", + "severity": "unrecoverable" } ], "continue_url": "https://merchant.com/" @@ -349,7 +378,7 @@ Maps to the [Get Cart](cart.md#get-cart) operation. "content": [ { "type": "text", - "text": "{\"cart\":{\"ucp\":{...},\"messages\":[...],\"continue_url\":\"...\"}}" + "text": "{\"ucp\":{...},\"messages\":[...],\"continue_url\":\"...\"}" } ] } @@ -424,15 +453,15 @@ Maps to the [Update Cart](cart.md#update-cart) operation. "structuredContent": { "cart": { "ucp": { - "version": "2026-01-15", + "version": "{{ ucp_version }}", "capabilities": [ { "name": "dev.ucp.shopping.checkout", - "version": "2026-01-11" + "version": "{{ ucp_version }}" }, { "name": "dev.ucp.shopping.cart", - "version": "2026-01-15" + "version": "{{ ucp_version }}" } ] }, @@ -536,15 +565,15 @@ Maps to the [Cancel Cart](cart.md#cancel-cart) operation. "structuredContent": { "cart": { "ucp": { - "version": "2026-01-15", + "version": "{{ ucp_version }}", "capabilities": [ { "name": "dev.ucp.shopping.checkout", - "version": "2026-01-11" + "version": "{{ ucp_version }}" }, { "name": "dev.ucp.shopping.cart", - "version": "2026-01-15" + "version": "{{ ucp_version }}" } ] }, @@ -614,23 +643,24 @@ JSON-RPC `result` with `structuredContent` containing the UCP envelope and "structuredContent": { "cart": { "ucp": { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "capabilities": { - "dev.ucp.shopping.cart": [{"version": "2026-01-11"}] + "dev.ucp.shopping.cart": [{"version": "{{ ucp_version }}"}] } }, "messages": [ { "type": "error", "code": "not_found", - "content": "Cart not found or has expired" + "content": "Cart not found or has expired", + "severity": "unrecoverable" } ], "continue_url": "https://merchant.com/" } }, "content": [ - {"type": "text", "text": "{\"cart\":{...}}"} + {"type": "text", "text": "{\"ucp\":{...},\"messages\":[...]}"} ] } } diff --git a/docs/specification/cart-rest.md b/docs/specification/cart-rest.md index 47b1123ce..f084140af 100644 --- a/docs/specification/cart-rest.md +++ b/docs/specification/cart-rest.md @@ -28,13 +28,13 @@ Businesses advertise REST transport availability through their UCP profile at ```json { "ucp": { - "version": "2026-01-15", + "version": "{{ ucp_version }}", "services": { "dev.ucp.shopping": { - "version": "2026-01-15", - "spec": "https://ucp.dev/specification/overview", + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/overview", "rest": { - "schema": "https://ucp.dev/services/shopping/openapi.json", + "schema": "https://ucp.dev/{{ ucp_version }}/services/shopping/rest.openapi.json", "endpoint": "https://business.example.com/ucp/v1" } } @@ -42,15 +42,15 @@ Businesses advertise REST transport availability through their UCP profile at "capabilities": [ { "name": "dev.ucp.shopping.checkout", - "version": "2026-01-11", - "spec": "https://ucp.dev/specification/checkout", - "schema": "https://ucp.dev/schemas/shopping/checkout.json" + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/checkout", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/checkout.json" }, { "name": "dev.ucp.shopping.cart", - "version": "2026-01-15", - "spec": "https://ucp.dev/specification/cart", - "schema": "https://ucp.dev/schemas/shopping/cart.json" + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/cart", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/cart.json" } ] } @@ -128,15 +128,15 @@ All REST endpoints **MUST** be served over HTTPS with minimum TLS version 1.3. { "ucp": { - "version": "2026-01-15", + "version": "{{ ucp_version }}", "capabilities": [ { "name": "dev.ucp.shopping.checkout", - "version": "2026-01-11" + "version": "{{ ucp_version }}" }, { "name": "dev.ucp.shopping.cart", - "version": "2026-01-15" + "version": "{{ ucp_version }}" } ] }, @@ -173,6 +173,28 @@ All REST endpoints **MUST** be served over HTTPS with minimum TLS version 1.3. } ``` +=== "Error Response" + + All items out of stock — no cart resource is created: + + ```json + HTTP/1.1 200 OK + Content-Type: application/json + + { + "ucp": { "version": "2026-01-15", "status": "error" }, + "messages": [ + { + "type": "error", + "code": "out_of_stock", + "content": "All requested items are currently out of stock", + "severity": "unrecoverable" + } + ], + "continue_url": "https://merchant.com/" + } + ``` + ### Get Cart #### Input Schema @@ -200,15 +222,15 @@ All REST endpoints **MUST** be served over HTTPS with minimum TLS version 1.3. { "ucp": { - "version": "2026-01-15", + "version": "{{ ucp_version }}", "capabilities": [ { "name": "dev.ucp.shopping.checkout", - "version": "2026-01-11" + "version": "{{ ucp_version }}" }, { "name": "dev.ucp.shopping.cart", - "version": "2026-01-15" + "version": "{{ ucp_version }}" } ] }, @@ -252,11 +274,12 @@ All REST endpoints **MUST** be served over HTTPS with minimum TLS version 1.3. { "ucp": { - "version": "2026-01-15", + "version": "{{ ucp_version }}", + "status": "error", "capabilities": [ { "name": "dev.ucp.shopping.cart", - "version": "2026-01-15" + "version": "{{ ucp_version }}" } ] }, @@ -264,7 +287,8 @@ All REST endpoints **MUST** be served over HTTPS with minimum TLS version 1.3. { "type": "error", "code": "not_found", - "content": "Cart not found or has expired" + "content": "Cart not found or has expired", + "severity": "unrecoverable" } ], "continue_url": "https://merchant.com/" @@ -326,15 +350,15 @@ All REST endpoints **MUST** be served over HTTPS with minimum TLS version 1.3. { "ucp": { - "version": "2026-01-15", + "version": "{{ ucp_version }}", "capabilities": [ { "name": "dev.ucp.shopping.checkout", - "version": "2026-01-11" + "version": "{{ ucp_version }}" }, { "name": "dev.ucp.shopping.cart", - "version": "2026-01-15" + "version": "{{ ucp_version }}" } ] }, @@ -413,15 +437,15 @@ All REST endpoints **MUST** be served over HTTPS with minimum TLS version 1.3. { "ucp": { - "version": "2026-01-15", + "version": "{{ ucp_version }}", "capabilities": [ { "name": "dev.ucp.shopping.checkout", - "version": "2026-01-11" + "version": "{{ ucp_version }}" }, { "name": "dev.ucp.shopping.cart", - "version": "2026-01-15" + "version": "{{ ucp_version }}" } ] }, @@ -510,16 +534,18 @@ HTTP 200 and the UCP envelope containing `messages`: ```json { "ucp": { - "version": "2026-01-11", + "version": "{{ ucp_version }}", + "status": "error", "capabilities": { - "dev.ucp.shopping.cart": [{"version": "2026-01-11"}] + "dev.ucp.shopping.cart": [{"version": "{{ ucp_version }}"}] } }, "messages": [ { "type": "error", "code": "not_found", - "content": "Cart not found or has expired" + "content": "Cart not found or has expired", + "severity": "unrecoverable" } ], "continue_url": "https://merchant.com/" diff --git a/docs/specification/cart.md b/docs/specification/cart.md index 0f5d4599e..08748a40d 100644 --- a/docs/specification/cart.md +++ b/docs/specification/cart.md @@ -128,6 +128,26 @@ The Cart capability defines the following logical operations. Creates a new cart session with line items and optional buyer/context information for localized pricing estimates. +When **all** requested items are unavailable, the business MAY return an +error response instead of creating a cart resource. `ucp.status` is the +primary discriminator; the absence of `id` is a consistent secondary +indicator: + +```json +{ + "ucp": { "version": "2026-01-15", "status": "error" }, + "messages": [ + { + "type": "error", + "code": "out_of_stock", + "content": "All requested items are currently out of stock", + "severity": "unrecoverable" + } + ], + "continue_url": "https://merchant.com/" +} +``` + * [REST Binding](cart-rest.md#create-cart) * [MCP Binding](cart-mcp.md#create_cart) @@ -161,6 +181,10 @@ Subsequent operations for this cart ID SHOULD return `not_found`. Cart reuses the same entity schemas as [Checkout](checkout.md). This ensures consistent data structures when converting a cart to a checkout session. +### UCP Response Cart {: #ucp-response-cart-schema } + +{{ extension_schema_fields('ucp.json#/$defs/response_cart_schema', 'cart') }} + ### Line Item #### Line Item Create Request @@ -171,9 +195,13 @@ consistent data structures when converting a cart to a checkout session. {{ schema_fields('types/line_item_update_req', 'checkout') }} -#### Line Item Response +#### Line Item + +{{ schema_fields('types/line_item_resp', 'cart') }} -{{ schema_fields('types/line_item_resp', 'checkout') }} +#### Item + +{{ schema_fields('types/item_resp', 'cart') }} ### Buyer @@ -183,8 +211,21 @@ consistent data structures when converting a cart to a checkout session. {{ schema_fields('context', 'checkout') }} +### Signals + +Environment data provided by the platform to support authorization +and abuse prevention. Signal values MUST NOT be buyer-asserted claims. See +[Signals](overview.md#signals) for details and privacy +requirements. + +{{ schema_fields('types/signals', 'checkout') }} + ### Total +The same totals contract applies to cart and checkout. See +[Checkout Totals](checkout.md#totals) for the rendering contract, accounting +identity, well-known types, repeating types, and sub-line semantics. + {{ schema_fields('types/total_resp', 'checkout') }} Taxes MAY be included where calculable. Platforms SHOULD assume cart totals diff --git a/docs/specification/catalog/index.md b/docs/specification/catalog/index.md new file mode 100644 index 000000000..f600f1f9c --- /dev/null +++ b/docs/specification/catalog/index.md @@ -0,0 +1,311 @@ + + +# Catalog Capability + +## Overview + +The Catalog capability allows platforms to search and browse business product catalogs. +This enables product discovery before checkout, supporting use cases like: + +* Free-text product search +* Category and filter-based browsing +* Batch product/variant retrieval by identifier +* Price comparison across variants + +## Capabilities + +| Capability | Description | +| :--- | :--- | +| [`dev.ucp.shopping.catalog.search`](search.md) | Search for products using query text and filters. | +| [`dev.ucp.shopping.catalog.lookup`](lookup.md) | Retrieve products or variants by identifier. | + +## Key Concepts + +* **Product**: A catalog item with title, description, media, and one or more + variants. +* **Variant**: A purchasable item with specific option selections (e.g., "Blue / + Large"), price, and availability. +* **Price**: Price values include both amount (in minor currency units) and + currency code, enabling multi-currency catalogs. + +### Relationship to Checkout + +Catalog operations return product and variant IDs that can be used directly in +checkout `line_items[].item.id`. The variant ID from catalog retrieval should match +the item ID expected by checkout. + +Catalog responses (pricing, availability, etc.) reflect the Business's current +terms for the given request but are not transactional commitments — checkout +is authoritative. Responses can be session-specific and **SHOULD NOT** be +reused across sessions without re-validation. + +## Shared Entities + +### Context + +Location and market context for catalog operations. All fields are optional +hints for relevance and localization. Platforms MAY geo-detect context from +request headers. + +Context signals are provisional—not authoritative data. Businesses SHOULD use +these values when verified inputs (e.g., shipping address) are absent, and MAY +ignore or down-rank them if inconsistent with higher-confidence signals +(authenticated account, risk detection) or regulatory constraints (export +controls). Eligibility and policy enforcement MUST occur at checkout time using +binding transaction data. + +Businesses determine market assignment—including currency—based on context +signals. Price filter values are denominated in `context.currency`; when +the presentment currency differs, businesses SHOULD convert before applying +(see [Price Filter](search.md#price-filter)). Response prices include +explicit currency codes confirming the resolution. + +When `context.eligibility` claims are present, Businesses that accept them +**MAY** adjust `price` / `list_price` directly for strikethrough display and +**MAY** use `messages` with `code: "eligibility_benefit"` to attribute the +adjustment to a specific claim. + +{{ schema_fields('types/context', 'catalog') }} + +### Signals + +Environment data provided by the platform to support authorization +and abuse prevention. Signal values MUST NOT be buyer-asserted claims. See +[Signals](../overview.md#signals) for details and privacy requirements. + +{{ schema_fields('types/signals', 'catalog') }} + +### Product + +A catalog item representing a sellable item with one or more purchasable variants. + +`media` and `variants` are ordered arrays. Businesses SHOULD return the most +relevant variant and image first—default for lookups, best match based on query +and context for search. Platforms SHOULD treat the first element as featured. + +{{ schema_fields('types/product', 'catalog') }} + +### Variant + +A purchasable item with specific option selections, price, and availability. + +In lookup responses, each variant carries an `inputs` array for correlation: +which request identifiers resolved to this variant, and whether the match +was `exact` or `featured` (server-selected). See +[Client Correlation](lookup.md#client-correlation) for details. + +`media` is an ordered array. Businesses SHOULD return the featured variant image +as the first element. Platforms SHOULD treat the first element as featured. + +{{ schema_fields('types/variant', 'catalog') }} + +### Price + +{{ schema_fields('types/price', 'catalog') }} + +### Price Range + +{{ schema_fields('types/price_range', 'catalog') }} + +### Media + +{{ schema_fields('types/media', 'catalog') }} + +### Product Option + +{{ schema_fields('types/product_option', 'catalog') }} + +### Option Value + +{{ schema_fields('types/option_value', 'catalog') }} + +### Selected Option + +{{ schema_fields('types/selected_option', 'catalog') }} + +### Rating + +{{ schema_fields('types/rating', 'catalog') }} + +## Messages and Error Handling + +All catalog responses include an optional `messages` array that allows businesses +to provide context about errors, warnings, or informational notices. + +### Message Types + +Messages communicate business outcomes and provide context: + +| Type | When to Use | Example Codes | +| :--- | :--- | :--- | +| `error` | Business-level errors | `NOT_FOUND`, `OUT_OF_STOCK`, `REGION_RESTRICTED` | +| `warning` | Important conditions affecting purchase | `DELAYED_FULFILLMENT`, `FINAL_SALE` | +| `info` | Additional context without issues | `PROMOTIONAL_PRICING`, `LIMITED_AVAILABILITY` | + +Warnings with `presentation: "disclosure"` carry notices (e.g., allergen +declarations, safety warnings) that platforms must not hide or dismiss. See +[Warning Presentation](../checkout.md#warning-presentation) for the full +rendering contract. + +**Note**: All catalog errors use `severity: "recoverable"` - agents handle them programmatically (retry, inform user, show alternatives). + +#### Message (Error) + +{{ schema_fields('types/message_error', 'catalog') }} + +#### Message (Warning) + +{{ schema_fields('types/message_warning', 'catalog') }} + +#### Message (Info) + +{{ schema_fields('types/message_info', 'catalog') }} + +### Common Scenarios + +#### Empty Search + +When search finds no matches, return an empty array without messages. + +```json +{ + "ucp": {...}, + "products": [] +} +``` + +This is not an error - the query was valid but returned no results. + +#### Backorder Warning + +When a product is available but has delayed fulfillment, return the product with +a warning message. Use the `path` field to target specific variants. + +```json +{ + "ucp": {...}, + "products": [ + { + "id": "prod_xyz789", + "title": "Professional Chef Knife Set", + "description": { "plain": "Complete professional knife collection." }, + "price_range": { + "min": { "amount": 29900, "currency": "USD" }, + "max": { "amount": 29900, "currency": "USD" } + }, + "variants": [ + { + "id": "var_abc", + "title": "12-piece Set", + "description": { "plain": "Complete professional knife collection." }, + "price": { "amount": 29900, "currency": "USD" }, + "availability": { "available": true } + } + ] + } + ], + "messages": [ + { + "type": "warning", + "code": "delayed_fulfillment", + "path": "$.products[0].variants[0]", + "content": "12-piece set on backorder, ships in 2-3 weeks" + } + ] +} +``` + +Agents can present the option and inform the user about the delay. The `path` +field uses RFC 9535 JSONPath to target specific components. + +#### Identifiers Not Found + +When requested identifiers don't exist, return success with the found products +(if any). The response MAY include informational messages indicating which +identifiers were not found. + +```json +{ + "ucp": {...}, + "products": [], + "messages": [ + { + "type": "info", + "code": "not_found", + "content": "prod_invalid" + } + ] +} +``` + +Agents correlate results using the `inputs` array on each variant. See +[Client Correlation](lookup.md#client-correlation). + +#### Product Disclosure + +When a product requires a disclosure (e.g., allergen notice, safety warning), +return it as a warning with `presentation: "disclosure"`. The `path` field targets the +relevant component in the response — when it targets a product, the +disclosure applies to all of its variants. + +```json +{ + "ucp": {...}, + "products": [ + { + "id": "prod_nut_butter", + "title": "Artisan Nut Butter Collection", + "variants": [ + { + "id": "var_almond", + "title": "Almond Butter", + "price": { "amount": 1299, "currency": "USD" }, + "availability": { "available": true } + }, + { + "id": "var_cashew", + "title": "Cashew Butter", + "price": { "amount": 1499, "currency": "USD" }, + "availability": { "available": true } + } + ] + } + ], + "messages": [ + { + "type": "warning", + "code": "allergens", + "path": "$.products[0]", + "content": "**Contains: tree nuts.** Produced in a facility that also processes peanuts, milk, and soy.", + "content_type": "markdown", + "presentation": "disclosure", + "image_url": "https://merchant.com/allergen-tree-nuts.svg", + "url": "https://merchant.com/allergen-info" + } + ] +} +``` + +See [Warning Presentation](../checkout.md#warning-presentation) for the +full rendering contract. + +## Transport Bindings + +The capabilities above are bound to specific transport protocols: + +* [REST Binding](rest.md): RESTful API mapping. +* [MCP Binding](mcp.md): Model Context Protocol mapping via JSON-RPC. diff --git a/docs/specification/catalog/lookup.md b/docs/specification/catalog/lookup.md new file mode 100644 index 000000000..ffe1fe2af --- /dev/null +++ b/docs/specification/catalog/lookup.md @@ -0,0 +1,82 @@ + + +# Catalog Lookup Capability + +* **Capability Name:** `dev.ucp.shopping.catalog.lookup` + +Retrieves products or variants by identifier. Use this when you already have +identifiers (e.g., from a saved list, deep links, or cart validation). + +## Operation + +| Operation | Description | +| :--- | :--- | +| **Lookup Catalog** | Retrieve products or variants by identifier. | + +### Supported Identifiers + +The `ids` parameter accepts an array of identifiers. Implementations MUST support +lookup by product ID and variant ID. Implementations MAY additionally support +secondary identifiers such as SKU or handle, provided these are also fields on +the returned product object. + +Duplicate identifiers in the request MUST be deduplicated. When an identifier +matches multiple products (e.g., a SKU shared across variants), implementations +return matching products and MAY limit the result set. When multiple identifiers +resolve to the same product, it MUST be returned once. + +### Client Correlation + +The response does not guarantee order. Each variant carries an `inputs` +array identifying which request identifiers resolved to it, and how. + +{{ schema_fields('types/input_correlation', 'catalog') }} + +Multiple request identifiers may resolve to the same variant (e.g., a +product ID and one of its variant IDs). When this occurs, the variant's +`inputs` array contains one entry per resolved identifier, each with its +own match type. Variants without an `inputs` entry MUST NOT appear in +lookup responses. + +### Batch Size + +Implementations SHOULD accept at least 10 identifiers per request. Implementations +MAY enforce a maximum batch size and MUST reject requests exceeding their limit +with an appropriate error (HTTP 400 `request_too_large` for REST, JSON-RPC +`-32602` for MCP). + +### Resolution Behavior + +`match` reflects the resolution level of the identifier, not its type: + +* **`exact`**: Identifier resolved directly to this variant + (e.g., variant ID, SKU, barcode). +* **`featured`**: Identifier resolved to the parent product; server + selected this variant as representative (e.g., product ID, handle). + +### Request + +{{ extension_schema_fields('catalog_lookup.json#/$defs/lookup_request', 'catalog') }} + +### Response + +{{ extension_schema_fields('catalog_lookup.json#/$defs/lookup_response', 'catalog') }} + +## Transport Bindings + +* [REST Binding](rest.md#post-cataloglookup): `POST /catalog/lookup` +* [MCP Binding](mcp.md#lookup_catalog): `lookup_catalog` tool diff --git a/docs/specification/catalog/mcp.md b/docs/specification/catalog/mcp.md new file mode 100644 index 000000000..ba666f28f --- /dev/null +++ b/docs/specification/catalog/mcp.md @@ -0,0 +1,509 @@ + + +# Catalog - MCP Binding + +This document specifies the Model Context Protocol (MCP) binding for the +[Catalog Capability](index.md). + +## Protocol Fundamentals + +### Discovery + +Businesses advertise MCP transport availability through their UCP profile at +`/.well-known/ucp`. + +```json +{ + "ucp": { + "version": "{{ ucp_version }}", + "services": { + "dev.ucp.shopping": { + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/overview", + "mcp": { + "schema": "https://ucp.dev/{{ ucp_version }}/services/shopping/mcp.openrpc.json", + "endpoint": "https://business.example.com/ucp/mcp" + } + } + }, + "capabilities": { + "dev.ucp.shopping.catalog.search": [{ + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/catalog/search", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/catalog_search.json" + }], + "dev.ucp.shopping.catalog.lookup": [{ + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/catalog/lookup", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/catalog_lookup.json" + }] + } + } +} +``` + +### Request Metadata + +MCP clients **MUST** include a `meta` object in every request containing +protocol metadata: + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { + "name": "search_catalog", + "arguments": { + "meta": { + "ucp-agent": { + "profile": "https://platform.example/profiles/v2026-01/shopping-agent.json" + } + }, + "catalog": { + "query": "blue running shoes", + "context": { + "address_country": "US", + "intent": "looking for comfortable everyday shoes" + } + } + } + } +} +``` + +The `meta["ucp-agent"]` field is **required** on all requests to enable +version compatibility checking and capability negotiation. + +## Tools + +| Tool | Capability | Description | +| :--- | :--- | :--- | +| `search_catalog` | [Search](search.md) | Search for products. | +| `lookup_catalog` | [Lookup](lookup.md) | Lookup one or more products or variants by identifier. | + +### `search_catalog` + +Maps to the [Catalog Search](search.md) capability. + +#### Search Request + +{{ extension_schema_fields( + 'catalog_search.json#/$defs/search_request', 'catalog/mcp' +) }} + +### Search Response + +{{ extension_schema_fields( + 'catalog_search.json#/$defs/search_response', 'catalog/mcp' +) }} + +#### Search Example + +=== "Request" + + ```json + { + "jsonrpc": "2.0", + "id": 1, + "method": "tools/call", + "params": { + "name": "search_catalog", + "arguments": { + "meta": { + "ucp-agent": { + "profile": "https://platform.example/profiles/v2026-01/shopping-agent.json" + } + }, + "catalog": { + "query": "blue running shoes", + "context": { + "address_country": "US", + "address_region": "CA", + "intent": "looking for comfortable everyday shoes" + }, + "filters": { + "categories": ["Footwear"], + "price": { + "max": 15000 + } + }, + "pagination": { + "limit": 20 + } + } + } + } + } + ``` + +=== "Response" + + ```json + { + "jsonrpc": "2.0", + "id": 1, + "result": { + "structuredContent": { + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.shopping.catalog.search": [ + {"version": "{{ ucp_version }}"} + ] + } + }, + "products": [ + { + "id": "prod_abc123", + "handle": "blue-runner-pro", + "title": "Blue Runner Pro", + "description": { + "plain": "Lightweight running shoes with responsive cushioning." + }, + "url": "https://business.example.com/products/blue-runner-pro", + "categories": [ + { "value": "187", "taxonomy": "google_product_category" }, + { "value": "aa-8-1", "taxonomy": "shopify" }, + { "value": "Footwear > Running", "taxonomy": "merchant" } + ], + "price_range": { + "min": { "amount": 12000, "currency": "USD" }, + "max": { "amount": 12000, "currency": "USD" } + }, + "media": [ + { + "type": "image", + "url": "https://cdn.example.com/products/blue-runner-pro.jpg", + "alt_text": "Blue Runner Pro running shoes" + } + ], + "options": [ + { + "name": "Size", + "values": [ + {"label": "8"}, + {"label": "9"}, + {"label": "10"}, + {"label": "11"}, + {"label": "12"} + ] + } + ], + "variants": [ + { + "id": "prod_abc123_size10", + "sku": "BRP-BLU-10", + "title": "Size 10", + "description": { "plain": "Size 10 variant" }, + "price": { "amount": 12000, "currency": "USD" }, + "availability": { "available": true }, + "selected_options": [ + { "name": "Size", "label": "10" } + ], + "tags": ["running", "road", "neutral"], + "seller": { + "name": "Example Store", + "links": [ + { + "type": "refund_policy", + "url": "https://business.example.com/refunds" + } + ] + } + } + ], + "rating": { + "value": 4.5, + "scale_max": 5, + "count": 128 + }, + "metadata": { + "collection": "Winter 2026", + "technology": { + "midsole": "React foam", + "outsole": "Continental rubber" + } + } + } + ], + "pagination": { + "cursor": "eyJwYWdlIjoxfQ==", + "has_next_page": true, + "total_count": 47 + } + } + } + } + ``` + +### `lookup_catalog` + +Maps to the [Catalog Lookup](lookup.md) capability. See capability documentation +for supported identifiers, resolution behavior, and client correlation requirements. + +The `catalog.ids` parameter accepts an array of identifiers and optional context. + +#### Lookup Request + +{{ extension_schema_fields( + 'catalog_lookup.json#/$defs/lookup_request', 'catalog/mcp' +) }} + +### Lookup Response + +{{ extension_schema_fields( + 'catalog_lookup.json#/$defs/lookup_response', 'catalog/mcp' +) }} + +#### Lookup Example + +=== "Request" + + ```json + { + "jsonrpc": "2.0", + "id": 2, + "method": "tools/call", + "params": { + "name": "lookup_catalog", + "arguments": { + "meta": { + "ucp-agent": { + "profile": "https://platform.example/profiles/v2026-01/shopping-agent.json" + } + }, + "catalog": { + "ids": ["prod_abc123", "var_xyz789"], + "context": { + "address_country": "US" + } + } + } + } + } + ``` + +=== "Response" + + ```json + { + "jsonrpc": "2.0", + "id": 2, + "result": { + "structuredContent": { + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.shopping.catalog.lookup": [ + {"version": "{{ ucp_version }}"} + ] + } + }, + "products": [ + { + "id": "prod_abc123", + "title": "Blue Runner Pro", + "description": { + "plain": "Lightweight running shoes with responsive cushioning." + }, + "price_range": { + "min": { "amount": 12000, "currency": "USD" }, + "max": { "amount": 12000, "currency": "USD" } + }, + "variants": [ + { + "id": "prod_abc123_size10", + "sku": "BRP-BLU-10", + "title": "Size 10", + "description": { "plain": "Size 10 variant" }, + "price": { "amount": 12000, "currency": "USD" }, + "availability": { "available": true }, + "inputs": [ + { "id": "prod_abc123", "match": "featured" } + ], + "tags": ["running", "road", "neutral"], + "seller": { + "name": "Example Store", + "links": [ + { + "type": "refund_policy", + "url": "https://business.example.com/policies/refunds" + } + ] + } + } + ], + "metadata": { + "collection": "Winter 2026", + "technology": { + "midsole": "React foam", + "outsole": "Continental rubber" + } + } + }, + { + "id": "prod_def456", + "title": "Trail Master X", + "description": { + "plain": "Rugged trail running shoes with aggressive tread." + }, + "price_range": { + "min": { "amount": 15000, "currency": "USD" }, + "max": { "amount": 15000, "currency": "USD" } + }, + "variants": [ + { + "id": "var_xyz789", + "sku": "TMX-GRN-11", + "title": "Size 11 - Green", + "description": { "plain": "Size 11 Green variant" }, + "price": { "amount": 15000, "currency": "USD" }, + "availability": { "available": true }, + "inputs": [ + { "id": "var_xyz789", "match": "exact" } + ], + "tags": ["trail", "waterproof"], + "seller": { + "name": "Example Store" + } + } + ] + } + ] + } + } + } + ``` + +#### Partial Success + +When some identifiers are not found, the response includes the found products. The +response MAY include informational messages indicating which identifiers were not found. + +```json +{ + "jsonrpc": "2.0", + "id": 3, + "result": { + "structuredContent": { + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.shopping.catalog.lookup": [ + {"version": "{{ ucp_version }}"} + ] + } + }, + "products": [ + { + "id": "prod_abc123", + "title": "Blue Runner Pro", + "price_range": { + "min": { "amount": 12000, "currency": "USD" }, + "max": { "amount": 12000, "currency": "USD" } + }, + "variants": [] + } + ], + "messages": [ + { + "type": "info", + "code": "not_found", + "content": "prod_notfound1" + }, + { + "type": "info", + "code": "not_found", + "content": "prod_notfound2" + } + ] + } + } +} +``` + +## Error Handling + +UCP uses a two-layer error model separating transport errors from business outcomes. + +### Transport Errors + +Use JSON-RPC 2.0 error codes for protocol-level issues that prevent request processing: + +| Code | Meaning | +| :--- | :--- | +| -32600 | Invalid Request - Malformed JSON-RPC | +| -32601 | Method not found | +| -32602 | Invalid params - Missing required parameter | +| -32603 | Internal error | + +### Business Outcomes + +All application-level outcomes return a successful JSON-RPC result with the UCP +envelope and optional `messages` array. See [Catalog Overview](index.md#messages-and-error-handling) +for message semantics and common scenarios. + +#### Example: All Products Not Found + +When all requested identifiers fail to resolve, the response contains an empty `products` +array. The response MAY include informational messages indicating which identifiers were +not found. + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "structuredContent": { + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.shopping.catalog.lookup": [ + {"version": "{{ ucp_version }}"} + ] + } + }, + "products": [], + "messages": [ + { + "type": "info", + "code": "not_found", + "content": "prod_invalid" + } + ] + } + } +} +``` + +Business outcomes use the JSON-RPC `result` field with messages in the response +payload. See the [Partial Success](#partial-success) section for handling mixed +results. + +## Conformance + +A conforming MCP transport implementation **MUST**: + +1. Implement JSON-RPC 2.0 protocol correctly. +2. Implement tools for each catalog capability advertised in the business's UCP profile, per their respective capability requirements ([Search](search.md), [Lookup](lookup.md)). Each capability may be adopted independently. +3. Use JSON-RPC errors for transport issues; use `messages` array for business outcomes. +4. Return successful result for lookup requests; unknown identifiers result in fewer products returned (MAY include informational `not_found` messages). +5. Validate tool inputs against UCP schemas. +6. Return products with valid `Price` objects (amount + currency). +7. Support cursor-based pagination with default limit of 10. +8. Return `-32602` (Invalid params) for requests exceeding batch size limits. diff --git a/docs/specification/catalog/rest.md b/docs/specification/catalog/rest.md new file mode 100644 index 000000000..78856dec9 --- /dev/null +++ b/docs/specification/catalog/rest.md @@ -0,0 +1,404 @@ + + +# Catalog - REST Binding + +This document specifies the HTTP/REST binding for the +[Catalog Capability](index.md). + +## Protocol Fundamentals + +### Discovery + +Businesses advertise REST transport availability through their UCP profile at +`/.well-known/ucp`. + +```json +{ + "ucp": { + "version": "{{ ucp_version }}", + "services": { + "dev.ucp.shopping": { + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/overview", + "rest": { + "schema": "https://ucp.dev/{{ ucp_version }}/services/shopping/rest.openapi.json", + "endpoint": "https://business.example.com/ucp" + } + } + }, + "capabilities": { + "dev.ucp.shopping.catalog.search": [{ + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/catalog/search", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/catalog_search.json" + }], + "dev.ucp.shopping.catalog.lookup": [{ + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/catalog/lookup", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/catalog_lookup.json" + }] + } + } +} +``` + +## Endpoints + +| Endpoint | Method | Capability | Description | +| :--- | :--- | :--- | :--- | +| `/catalog/search` | POST | [Search](search.md) | Search for products. | +| `/catalog/lookup` | POST | [Lookup](lookup.md) | Lookup one or more products by ID. | + +### `POST /catalog/search` + +Maps to the [Catalog Search](search.md) capability. + +{{ method_fields('search_catalog', 'rest.openapi.json', 'catalog/rest') }} + +#### Example + +=== "Request" + + ```json + { + "query": "blue running shoes", + "context": { + "address_country": "US", + "address_region": "CA", + "intent": "looking for comfortable everyday shoes" + }, + "filters": { + "categories": ["Footwear"], + "price": { + "max": 15000 + } + }, + "pagination": { + "limit": 20 + } + } + ``` + +=== "Response" + + ```json + { + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.shopping.catalog.search": [ + {"version": "{{ ucp_version }}"} + ] + } + }, + "products": [ + { + "id": "prod_abc123", + "handle": "blue-runner-pro", + "title": "Blue Runner Pro", + "description": { + "plain": "Lightweight running shoes with responsive cushioning." + }, + "url": "https://business.example.com/products/blue-runner-pro", + "categories": [ + { "value": "187", "taxonomy": "google_product_category" }, + { "value": "aa-8-1", "taxonomy": "shopify" }, + { "value": "Footwear > Running", "taxonomy": "merchant" } + ], + "price_range": { + "min": { "amount": 12000, "currency": "USD" }, + "max": { "amount": 12000, "currency": "USD" } + }, + "media": [ + { + "type": "image", + "url": "https://cdn.example.com/products/blue-runner-pro.jpg", + "alt_text": "Blue Runner Pro running shoes" + } + ], + "options": [ + { + "name": "Size", + "values": [ + {"label": "8"}, + {"label": "9"}, + {"label": "10"}, + {"label": "11"}, + {"label": "12"} + ] + } + ], + "variants": [ + { + "id": "prod_abc123_size10", + "sku": "BRP-BLU-10", + "title": "Size 10", + "description": { "plain": "Size 10 variant" }, + "price": { "amount": 12000, "currency": "USD" }, + "availability": { "available": true }, + "selected_options": [ + { "name": "Size", "label": "10" } + ], + "tags": ["running", "road", "neutral"], + "seller": { + "name": "Example Store", + "links": [ + { + "type": "refund_policy", + "url": "https://business.example.com/refunds" + } + ] + } + } + ], + "rating": { + "value": 4.5, + "scale_max": 5, + "count": 128 + }, + "metadata": { + "collection": "Winter 2026", + "technology": { + "midsole": "React foam", + "outsole": "Continental rubber" + } + } + } + ], + "pagination": { + "cursor": "eyJwYWdlIjoxfQ==", + "has_next_page": true, + "total_count": 47 + } + } + ``` + +### `POST /catalog/lookup` + +Maps to the [Catalog Lookup](lookup.md) capability. See capability documentation +for supported identifiers, resolution behavior, and client correlation requirements. + +The request body contains an array of identifiers and optional context that +applies to all lookups in the batch. + +{{ method_fields('lookup_catalog', 'rest.openapi.json', 'catalog/rest') }} + +#### Example: Batch Lookup with Context + +=== "Request" + + ```json + POST /catalog/lookup HTTP/1.1 + Host: business.example.com + Content-Type: application/json + + { + "ids": ["prod_abc123", "prod_def456"], + "context": { + "address_country": "US", + "language": "es" + } + } + ``` + +=== "Response" + + ```json + { + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.shopping.catalog.lookup": [ + {"version": "{{ ucp_version }}"} + ] + } + }, + "products": [ + { + "id": "prod_abc123", + "title": "Blue Runner Pro", + "description": { + "plain": "Zapatillas ligeras con amortiguación reactiva." + }, + "price_range": { + "min": { "amount": 12000, "currency": "USD" }, + "max": { "amount": 12000, "currency": "USD" } + }, + "variants": [ + { + "id": "prod_abc123_size10", + "sku": "BRP-BLU-10", + "title": "Talla 10", + "description": { "plain": "Variante talla 10" }, + "price": { "amount": 12000, "currency": "USD" }, + "availability": { "available": true }, + "inputs": [ + { "id": "prod_abc123", "match": "featured" } + ] + } + ] + }, + { + "id": "prod_def456", + "title": "Trail Blazer X", + "description": { + "plain": "Zapatillas de trail con tracción superior." + }, + "price_range": { + "min": { "amount": 15000, "currency": "USD" }, + "max": { "amount": 15000, "currency": "USD" } + }, + "variants": [ + { + "id": "prod_def456_size10", + "sku": "TBX-GRN-10", + "title": "Talla 10", + "price": { "amount": 15000, "currency": "USD" }, + "availability": { "available": true }, + "inputs": [ + { "id": "prod_def456", "match": "featured" } + ] + } + ] + } + ] + } + ``` + +#### Example: Partial Success (Some Identifiers Not Found) + +When some identifiers in the batch are not found, the response includes the +found products in the `products` array. The response MAY include informational +messages indicating which identifiers were not found. + +=== "Request" + + ```json + { + "ids": ["prod_abc123", "prod_invalid", "prod_def456"] + } + ``` + +=== "Response" + + ```json + { + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.shopping.catalog.lookup": [ + {"version": "{{ ucp_version }}"} + ] + } + }, + "products": [ + { + "id": "prod_abc123", + "title": "Blue Runner Pro", + "price_range": { + "min": { "amount": 12000, "currency": "USD" }, + "max": { "amount": 12000, "currency": "USD" } + } + }, + { + "id": "prod_def456", + "title": "Trail Blazer X", + "price_range": { + "min": { "amount": 15000, "currency": "USD" }, + "max": { "amount": 15000, "currency": "USD" } + } + } + ], + "messages": [ + { + "type": "info", + "code": "not_found", + "content": "prod_invalid" + } + ] + } + ``` + +## Error Handling + +UCP uses a two-layer error model separating transport errors from business outcomes. + +### Transport Errors + +Use HTTP status codes for protocol-level issues that prevent request processing: + +| Status | Meaning | +| :--- | :--- | +| 400 | Bad Request - Malformed JSON or missing required parameters | +| 401 | Unauthorized - Missing or invalid authentication | +| 429 | Too Many Requests - Rate limited | +| 500 | Internal Server Error | + +### Business Outcomes + +All application-level outcomes return HTTP 200 with the UCP envelope and optional +`messages` array. See [Catalog Overview](index.md#messages-and-error-handling) +for message semantics and common scenarios. + +#### Example: All Products Not Found + +When all requested identifiers fail lookup, the `products` array is empty. The response +MAY include informational messages indicating which identifiers were not found. + +```json +{ + "ucp": { + "version": "{{ ucp_version }}", + "capabilities": { + "dev.ucp.shopping.catalog.lookup": [ + {"version": "{{ ucp_version }}"} + ] + } + }, + "products": [], + "messages": [ + { + "type": "info", + "code": "not_found", + "content": "prod_invalid1" + }, + { + "type": "info", + "code": "not_found", + "content": "prod_invalid2" + } + ] +} +``` + +Business outcomes use the standard HTTP 200 status with messages in the response body. + +## Entities + +### UCP Response Catalog {: #ucp-response-catalog-schema } + +{{ extension_schema_fields('ucp.json#/$defs/response_catalog_schema', 'catalog/rest') }} + +## Conformance + +A conforming REST transport implementation **MUST**: + +1. Implement endpoints for each catalog capability advertised in the business's UCP profile, per their respective capability requirements ([Search](search.md), [Lookup](lookup.md)). Each capability may be adopted independently. +2. Return products with valid `Price` objects (amount + currency). +3. Support cursor-based pagination with default limit of 10. +4. Return HTTP 200 for lookup requests; unknown identifiers result in fewer products returned (MAY include informational `not_found` messages). +5. Return HTTP 400 with `request_too_large` error for requests exceeding batch size limits. diff --git a/docs/specification/catalog/search.md b/docs/specification/catalog/search.md new file mode 100644 index 000000000..aa706fcf1 --- /dev/null +++ b/docs/specification/catalog/search.md @@ -0,0 +1,88 @@ + + +# Catalog Search Capability + +* **Capability Name:** `dev.ucp.shopping.catalog.search` + +Performs a search against the business's product catalog. Supports free-text +queries, filtering by category and price, and pagination. + +## Operation + +| Operation | Description | +| :--- | :--- | +| **Search Catalog** | Search for products using provided inputs and filters. | + +### Request + +{{ extension_schema_fields('catalog_search.json#/$defs/search_request', 'catalog') }} + +### Response + +{{ extension_schema_fields('catalog_search.json#/$defs/search_response', 'catalog') }} + +## Search Inputs + +A valid search request MUST include at least one of: a `query` string, +one or more `filters`, or an extension-defined input. When `query` is +omitted, the request represents a browse operation — the business returns +products matching the provided filters without text-relevance ranking. +Extensions MAY define additional inputs (e.g., visual similarity, +product references). + +Implementations MUST validate that incoming requests contain at least one +recognized input and SHOULD reject empty or invalid requests with an +appropriate error. Implementations define and enforce their own rules for +input presence and content — for example, requiring `query`, rejecting +empty `query` strings, or accepting filter-only requests for category browsing. + +## Search Filters + +Filter criteria for narrowing search results. Standard filters are defined below; +merchants MAY support additional custom filters via `additionalProperties`. + +{{ schema_fields('types/search_filters', 'catalog') }} + +### Price Filter + +{{ schema_fields('types/price_filter', 'catalog') }} + +## Pagination + +Cursor-based pagination for list operations. Cursors are opaque strings +that implementations MAY encode as stateless keyset tokens. + +### Page Size + +The `limit` parameter is a requested page size, not a guaranteed count. +Implementations SHOULD accept a page size of at least 10. When the +requested limit exceeds the implementation's maximum, implementations +MAY clamp to their maximum silently — returning fewer results without +error. Clients MUST NOT assume the response size equals the requested limit. + +### Pagination Request + +{{ extension_schema_fields('types/pagination.json#/$defs/request', 'catalog') }} + +### Pagination Response + +{{ extension_schema_fields('types/pagination.json#/$defs/response', 'catalog') }} + +## Transport Bindings + +* [REST Binding](rest.md#post-catalogsearch): `POST /catalog/search` +* [MCP Binding](mcp.md#search_catalog): `search_catalog` tool diff --git a/docs/specification/checkout-a2a.md b/docs/specification/checkout-a2a.md index cb1e016e2..b6f7d6a27 100644 --- a/docs/specification/checkout-a2a.md +++ b/docs/specification/checkout-a2a.md @@ -28,12 +28,12 @@ platforms to interact with the business services over A2A Protocol. ```json { "ucp": { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "services": { "dev.ucp.shopping": [ { - "version": "2026-01-11", - "spec": "https://ucp.dev/specification/overview", + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/overview", "transport": "a2a", "endpoint": "https://example-business.com/.well-known/agent-card.json" } @@ -45,18 +45,18 @@ platforms to interact with the business services over A2A Protocol. ## Shopping Agent Profile Advertisement -Shopping platforms interacting with the business agent must -send their profile URI as `UCP-Agent` request headers with every request. +Shopping platforms interacting with the business agent must send their profile +URI as `UCP-Agent` request headers with every request. -```text +```json UCP-Agent: profile="https://agent.example/profiles/v2025-11/shopping-agent.json" Content-Type: application/json ``` ### Header Mapping Reference -The following table defines the required headers for enabling an A2A Agent -to communicate UCP data types with platforms. +The following table defines the required headers for enabling an A2A Agent to +communicate UCP data types with platforms. | Header Name | Description | | :----------------- | :----------------------------------------- | @@ -66,15 +66,17 @@ to communicate UCP data types with platforms. ## A2A Interactions The A2A Protocol provides a strong foundation for inter-agent communication. -[A2A extensions](https://a2a-protocol.org/latest/topics/extensions/) enable communication between agents with structured data -types. This enables businesses to build AI applications to leverage UCP data -types for communication with platforms. +[A2A extensions](https://a2a-protocol.org/latest/topics/extensions/) enable +communication between agents with structured data types. This enables businesses +to build AI applications to leverage UCP data types for communication with +platforms. -The URI for UCP A2A extension: `https://ucp.dev/specification/reference?v=2026-01-11` +The URI for UCP A2A extension: +`https://ucp.dev/{{ ucp_version }}/specification/reference` Businesses supporting UCP must advertise the extension and any optional -capabilities in their A2A Agent Card to allow platforms to activate -the extension. +capabilities in their A2A Agent Card to allow platforms to activate the +extension. An example: @@ -82,16 +84,16 @@ An example: { "extensions": [ { - "uri": "https://ucp.dev/specification/reference?v=2026-01-11", + "uri": "https://ucp.dev/{{ ucp_version }}/specification/reference", "description": "Business agent supporting UCP", "params": { "capabilities": { "dev.ucp.shopping.checkout": [ - {"version": "2026-01-11"} + {"version": "{{ ucp_version }}"} ], "dev.ucp.shopping.fulfillment": [ { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "extends": "dev.ucp.shopping.checkout" } ] @@ -105,24 +107,23 @@ An example: ### Agent2Agent Negotiation The business agents can leverage A2A `Message` objects for allowing interaction -with shopping agents/platforms. The A2A `Message` object returned by -the agent will return structured data in `DataPart` objects within the message. -Platforms must pass the business agent generated `contextId` for -subsequent turns in a session to preserve the current context. +with shopping agents/platforms. The A2A `Message` object returned by the agent +will return structured data in `DataPart` objects within the message. Platforms +must pass the business agent generated `contextId` for subsequent turns in a +session to preserve the current context. Business agents may also leverage A2A `Task` objects for scenarios where applicable. In such scenarios, the business agent will return `Task` objects -with appropriate payload for interaction with the platforms. -Platforms must pass the server generated `taskId` along with the -`contextId` for subsequent turns until the task is completed. - -Platforms must be capable of handling further negotiation in the -same session even after a task reaches a terminal state (e.g. user places an -order and wants to place another order in the same context or if the task -reaches a failed state due to an exception). Platforms must reset the -`taskId` once a task reaches terminal state to allow further interactions with -the agent, although the current `contextId` can be reused for subsequent -interactions. +with appropriate payload for interaction with the platforms. Platforms must pass +the server generated `taskId` along with the `contextId` for subsequent turns +until the task is completed. + +Platforms must be capable of handling further negotiation in the same session +even after a task reaches a terminal state (e.g. user places an order and wants +to place another order in the same context or if the task reaches a failed state +due to an exception). Platforms must reset the `taskId` once a task reaches +terminal state to allow further interactions with the agent, although the +current `contextId` can be reused for subsequent interactions. ## Request Idempotency @@ -132,23 +133,22 @@ to detect duplicate messages from platform retries. ## Checkout Functionality The Checkout capability allows consumers to manage items in a checkout session -and complete the purchase process. The business agent typically integrates -with the business's checkout APIs for offering this functionality. +and complete the purchase process. The business agent typically integrates with +the business's checkout APIs for offering this functionality. The extension defines the data schema for representing the Checkout functionality by business agent for any checkout related actions, completing or -canceling the checkout. `Checkout` entity is a profile of an A2A `Message`. -The Checkout entity must be returned by the business agent to the platform -that activated UCP-A2A Extension in an A2A `Message`'s `DataPart`. -The checkout object **MUST** be returned as part of a `DataPart` object with -key `a2a.ucp.checkout`. - -**Request format:** -Agentic applications can accept natural language input from users interacting -with the agent to identify the user's intent, negotiate with the user to -capture any required information and then invoke the appropriate tools to -perform the operation. Inputs from platforms can be sent to the remote business -agent as an A2A `Message`. +canceling the checkout. `Checkout` entity is a profile of an A2A `Message`. The +Checkout entity must be returned by the business agent to the platform that +activated UCP-A2A Extension in an A2A `Message`'s `DataPart`. The checkout +object **MUST** be returned as part of a `DataPart` object with key +`a2a.ucp.checkout`. + +**Request format:** Agentic applications can accept natural language input from +users interacting with the agent to identify the user's intent, negotiate with +the user to capture any required information and then invoke the appropriate +tools to perform the operation. Inputs from platforms can be sent to the remote +business agent as an A2A `Message`. Examples: @@ -192,12 +192,10 @@ Examples: "contextId": "aad14abc-4082-4748-84ca-4afff85aedfa" } } - ``` -**Response format:** -Following is an example response from a business agent implementing -Checkout functionality: +**Response format:** Following is an example response from a business agent +implementing Checkout functionality: ```json { @@ -222,19 +220,17 @@ Checkout functionality: ### Checkout Completion -When a user is ready to make a payment, `payment` must be submitted -to the business agent to complete the checkout process. `payment` is a -structured data type specified as part of UCP. When processing a payment to -complete the checkout, `payment` must be submitted to the business -agent -as a `DataPart` with attribute name `a2a.ucp.checkout.payment`. Any -associated risk signals should be sent with attribute -name `a2a.ucp.checkout.risk_signals`. +When a user is ready to make a payment, `payment` must be submitted to the +business agent to complete the checkout process. `payment` is a structured data +type specified as part of UCP. When processing a payment to complete the +checkout, `payment` must be submitted to the business agent as a `DataPart` with +attribute name `a2a.ucp.checkout.payment`. Any associated signals should be sent +with attribute name `a2a.ucp.checkout.signals`. Upon completion of the checkout process, the business agent must return the checkout object containing an `order` attribute with `id` and `permalink_url`. -**Request format:** +### Request format ```json { @@ -251,7 +247,10 @@ checkout object containing an `order` attribute with `id` and `permalink_url`. "a2a.ucp.checkout.payment": { ...paymentObject }, - "a2a.ucp.checkout.risk_signals":{...content} + "a2a.ucp.checkout.signals": { + "dev.ucp.buyer_ip": "203.0.113.42", + "dev.ucp.user_agent": "Mozilla/5.0 ..." + } } } ], @@ -263,9 +262,8 @@ checkout object containing an `order` attribute with `id` and `permalink_url`. ``` -**Response format:** -Following is an example response from a business agent implementing -Checkout functionality: +**Response format:** Following is an example response from a business agent +implementing Checkout functionality: ```json { @@ -294,15 +292,15 @@ Checkout functionality: Business agents can implement AP2 mandates extension that enables secure exchange of user intents and authorizations for Agent-to-Agent payment interactions. Businesses that support AP2 mandates extension for UCP must -specify this in the UCP discovery document and the A2A agent card. -The AP2 mandates extension is considered implicitly active when a platform and -business agent advertise AP2 mandates extension in their respective profiles. +specify this in the UCP discovery document and the A2A agent card. The AP2 +mandates extension is considered implicitly active when a platform and business +agent advertise AP2 mandates extension in their respective profiles. When AP2 mandates extension is enabled, the business agent must create a -detached JWS for the checkout object and must return the generated signature -as part of the `DataPart` as `ap2.merchant_authorization`. -This will allow the platform to cryptographically verify the -checkout payload against the business's public keys. +detached JWS for the checkout object and must return the generated signature as +part of the `DataPart` as `ap2.merchant_authorization`. This will allow the +platform to cryptographically verify the checkout payload against the business's +public keys. ```json { @@ -330,18 +328,17 @@ checkout payload against the business's public keys. } ``` -When the user confirms the payment on a platform, the user signed -checkout and payment mandate objects must be sent as `DataPart`s -to the business agent for completing checkout. The `payment` which -includes the payment mandate must be submitted as part of a `DataPart` -with attribute name `a2a.ucp.checkout.payment`. Signed checkout mandate -must be specified in the `DataPart` as `ap2.checkout_mandate`. The `token` -attribute of `payment.instruments[*].credential` contains the payment mandate. -Refer to [AP2 Mandates Extension](ap2-mandates.md) documentation for more -details about verification and processing of the mandates to complete the -checkout. +When the user confirms the payment on a platform, the user signed checkout and +payment mandate objects must be sent as `DataPart`s to the business agent for +completing checkout. The `payment` which includes the payment mandate must +be submitted as part of a `DataPart` with attribute name +`a2a.ucp.checkout.payment`. Signed checkout mandate must be specified in +the `DataPart` as `ap2.checkout_mandate`. The `token` attribute of +`payment` contains the payment mandate. Refer to +[AP2 Mandates Extension](ap2-mandates.md) documentation for more details about +verification and processing of the mandates to complete the checkout. -**Request format:** +### Request format ```json { @@ -358,28 +355,21 @@ checkout. "kind": "data", "data": { "a2a.ucp.checkout.payment": { - "instruments": [ - { - "id": "instr_1", - "handler_id": "gpay_1234", - "type": "card", - "selected": true, - "display": { - "description": "Visa •••• 1234", - }, - "billing_address": { - "street_address": "123 Main St", - "address_locality": "Anytown", - "address_region": "CA", - "address_country": "US", - "postal_code": "12345" - }, - "credential": { - "type": "PAYMENT_GATEWAY", - "token": "examplePaymentMethodToken" - } - } - ] + "id": "instr_1", + "handler_id": "gpay", + "type": "card", + "description": "Visa •••• 1234", + "billing_address": { + "street_address": "123 Main St", + "address_locality": "Anytown", + "address_region": "CA", + "address_country": "US", + "postal_code": "12345" + }, + "credential": { + "type": "PAYMENT_GATEWAY", + "token": "examplePaymentMethodToken" + } }, "ap2": { "checkout_mandate": "eyJhbGciOiJFUz..." diff --git a/docs/specification/checkout-mcp.md b/docs/specification/checkout-mcp.md index 2d9a003b5..3fcea71a8 100644 --- a/docs/specification/checkout-mcp.md +++ b/docs/specification/checkout-mcp.md @@ -29,14 +29,14 @@ Businesses advertise MCP transport availability through their UCP profile at ```json { "ucp": { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "services": { "dev.ucp.shopping": [ { - "version": "2026-01-11", - "spec": "https://ucp.dev/specification/overview", + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/overview", "transport": "mcp", - "schema": "https://ucp.dev/services/shopping/mcp.openrpc.json", + "schema": "https://ucp.dev/{{ ucp_version }}/services/shopping/mcp.openrpc.json", "endpoint": "https://business.example.com/ucp/mcp" } ] @@ -44,16 +44,16 @@ Businesses advertise MCP transport availability through their UCP profile at "capabilities": { "dev.ucp.shopping.checkout": [ { - "version": "2026-01-11", - "spec": "https://ucp.dev/specification/checkout", - "schema": "https://ucp.dev/schemas/shopping/checkout.json" + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/checkout", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/checkout.json" } ], "dev.ucp.shopping.fulfillment": [ { - "version": "2026-01-11", - "spec": "https://ucp.dev/specification/fulfillment", - "schema": "https://ucp.dev/schemas/shopping/fulfillment.json", + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/fulfillment", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/fulfillment.json", "extends": "dev.ucp.shopping.checkout" } ] @@ -62,7 +62,7 @@ Businesses advertise MCP transport availability through their UCP profile at "com.example.vendor.delegate_payment": [ { "id": "handler_1", - "version": "2026-01-11", + "version": "{{ ucp_version }}", "spec": "https://example.vendor.com/specs/delegate-payment", "schema": "https://example.vendor.com/schemas/delegate-payment-config.json", "available_instruments": [ @@ -212,18 +212,18 @@ Maps to the [Create Checkout](checkout.md#create-checkout) operation. "structuredContent": { "checkout": { "ucp": { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "capabilities": { "dev.ucp.shopping.checkout": [ - {"version": "2026-01-11"} + {"version": "{{ ucp_version }}"} ], "dev.ucp.shopping.fulfillment": [ - {"version": "2026-01-11"} + {"version": "{{ ucp_version }}"} ] }, "payment_handlers": { "com.example.vendor.delegate_payment": [ - {"id": "handler_1", "version": "2026-01-11", "available_instruments": [{"type": "card"}], "config": {}} + {"id": "handler_1", "version": "{{ ucp_version }}", "available_instruments": [{"type": "card"}], "config": {}} ] } }, @@ -340,6 +340,34 @@ Maps to the [Create Checkout](checkout.md#create-checkout) operation. } ``` +=== "Error Response" + + All items out of stock — no checkout resource is created: + + ```json + { + "jsonrpc": "2.0", + "id": 1, + "result": { + "structuredContent": { + "ucp": { "version": "2026-01-11", "status": "error" }, + "messages": [ + { + "type": "error", + "code": "out_of_stock", + "content": "All requested items are currently out of stock", + "severity": "unrecoverable" + } + ], + "continue_url": "https://merchant.com/" + }, + "content": [ + {"type": "text", "text": "{\"ucp\":{...},\"messages\":[...]}"} + ] + } + } + ``` + ### `get_checkout` Maps to the [Get Checkout](checkout.md#get-checkout) operation. @@ -434,18 +462,18 @@ Maps to the [Update Checkout](checkout.md#update-checkout) operation. "structuredContent": { "checkout": { "ucp": { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "capabilities": { "dev.ucp.shopping.checkout": [ - {"version": "2026-01-11"} + {"version": "{{ ucp_version }}"} ], "dev.ucp.shopping.fulfillment": [ - {"version": "2026-01-11"} + {"version": "{{ ucp_version }}"} ] }, "payment_handlers": { "com.example.vendor.delegate_payment": [ - {"id": "handler_1", "version": "2026-01-11", "available_instruments": [{"type": "card"}], "config": {}} + {"id": "handler_1", "version": "{{ ucp_version }}", "available_instruments": [{"type": "card"}], "config": {}} ] } }, @@ -621,9 +649,9 @@ as JSON-RPC `result` with `structuredContent` containing the UCP envelope and "structuredContent": { "checkout": { "ucp": { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "capabilities": { - "dev.ucp.shopping.checkout": [{"version": "2026-01-11"}] + "dev.ucp.shopping.checkout": [{"version": "{{ ucp_version }}"}] } }, "id": "checkout_abc123", @@ -653,6 +681,34 @@ as JSON-RPC `result` with `structuredContent` containing the UCP envelope and } ``` +For `create_checkout`, when all items unavailable and no checkout can be created, +JSON-RPC `result` with `structuredContent` containing the UCP envelope and `messages`: + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "result": { + "structuredContent": { + "ucp": { "version": "2026-01-11", "status": "error" }, + "messages": [ + { + "type": "error", + "code": "item_unavailable", + "content": "Items are not available for purchase in your region", + "severity": "unrecoverable", + "path": "$.line_items" + } + ], + "continue_url": "https://merchant.com/" + }, + "content": [ + {"type": "text", "text": "{\"ucp\":{...},\"messages\":[...]}"} + ] + } +} +``` + ## Message Signing Platforms **SHOULD** authenticate agents when using MCP transport. When using diff --git a/docs/specification/checkout-rest.md b/docs/specification/checkout-rest.md index 6524f0f52..5ffed0ef0 100644 --- a/docs/specification/checkout-rest.md +++ b/docs/specification/checkout-rest.md @@ -85,17 +85,17 @@ All REST endpoints **MUST** be served over HTTPS with minimum TLS version { "ucp": { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "capabilities": { "dev.ucp.shopping.checkout": [ - {"version": "2026-01-11"} + {"version": "{{ ucp_version }}"} ] }, "payment_handlers": { "com.shopify.shop_pay": [ { "id": "shop_pay_1234", - "version": "2026-01-11", + "version": "{{ ucp_version }}", "available_instruments": [ {"type": "shop_pay"} ], @@ -169,6 +169,28 @@ All REST endpoints **MUST** be served over HTTPS with minimum TLS version } ``` +=== "Error Response" + + All items out of stock — no checkout resource is created: + + ```json + HTTP/1.1 200 OK + Content-Type: application/json + + { + "ucp": { "version": "2026-01-11", "status": "error" }, + "messages": [ + { + "type": "error", + "code": "out_of_stock", + "content": "All requested items are currently out of stock", + "severity": "unrecoverable" + } + ], + "continue_url": "https://merchant.com/" + } + ``` + ### Update Checkout #### Update Buyer Info @@ -213,17 +235,17 @@ so clients must include all previously set fields they wish to retain. { "ucp": { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "capabilities": { "dev.ucp.shopping.checkout": [ - {"version": "2026-01-11"} + {"version": "{{ ucp_version }}"} ] }, "payment_handlers": { "com.shopify.shop_pay": [ { "id": "shop_pay_1234", - "version": "2026-01-11", + "version": "{{ ucp_version }}", "available_instruments": [ {"type": "shop_pay"} ], @@ -360,17 +382,17 @@ type & addresses. { "ucp": { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "capabilities": { "dev.ucp.shopping.checkout": [ - {"version": "2026-01-11"} + {"version": "{{ ucp_version }}"} ] }, "payment_handlers": { "com.google.pay": [ { "id": "gpay_1234", - "version": "2026-01-11", + "version": "{{ ucp_version }}", "config": { "allowed_payment_methods": [ { @@ -572,17 +594,17 @@ Follow-up calls after initial `fulfillment` data to update selection. { "ucp": { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "capabilities": { "dev.ucp.shopping.checkout": [ - {"version": "2026-01-11"} + {"version": "{{ ucp_version }}"} ] }, "payment_handlers": { "com.shopify.shop_pay": [ { "id": "shop_pay_1234", - "version": "2026-01-11", + "version": "{{ ucp_version }}", "available_instruments": [ {"type": "shop_pay"} ], @@ -744,8 +766,9 @@ place to set these expectations via `messages`. } ] }, - "risk_signals": { - //... risk signal related data (device fingerprint / risk token) + "signals": { + "dev.ucp.buyer_ip": "203.0.113.42", + "dev.ucp.user_agent": "Mozilla/5.0 ..." } } ``` @@ -758,17 +781,17 @@ place to set these expectations via `messages`. { "ucp": { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "capabilities": { "dev.ucp.shopping.checkout": [ - {"version": "2026-01-11"} + {"version": "{{ ucp_version }}"} ] }, "payment_handlers": { "com.google.pay": [ { "id": "gpay_1234", - "version": "2026-01-11", + "version": "{{ ucp_version }}", "config": { "allowed_payment_methods": [ { @@ -919,17 +942,17 @@ place to set these expectations via `messages`. { "ucp": { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "capabilities": { "dev.ucp.shopping.checkout": [ - {"version": "2026-01-11"} + {"version": "{{ ucp_version }}"} ] }, "payment_handlers": { "com.shopify.shop_pay": [ { "id": "shop_pay_1234", - "version": "2026-01-11", + "version": "{{ ucp_version }}", "available_instruments": [ {"type": "shop_pay"} ], @@ -1074,17 +1097,17 @@ place to set these expectations via `messages`. { "ucp": { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "capabilities": { "dev.ucp.shopping.checkout": [ - {"version": "2026-01-11"} + {"version": "{{ ucp_version }}"} ] }, "payment_handlers": { "com.google.pay": [ { "id": "gpay_1234", - "version": "2026-01-11", + "version": "{{ ucp_version }}", "config": { "allowed_payment_methods": [ { @@ -1268,9 +1291,9 @@ with HTTP 200 and the UCP envelope containing `messages`: ```json { "ucp": { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "capabilities": { - "dev.ucp.shopping.checkout": [{"version": "2026-01-11"}] + "dev.ucp.shopping.checkout": [{"version": "{{ ucp_version }}"}] } }, "id": "checkout_abc123", @@ -1294,6 +1317,24 @@ with HTTP 200 and the UCP envelope containing `messages`: } ``` +For `create_checkout`, when all items unavailable and no checkout can be created, returns +HTTP 200 and the UCP envelope containing `messages` + +```json +{ + "ucp": { "version": "2026-01-11", "status": "error" }, + "messages": [ + { + "type": "error", + "code": "item_unavailable", + "content": "All items are not available for purchase", + "severity": "unrecoverable" + } + ], + "continue_url": "https://merchant.com/" +} +``` + ## Message Signing Platforms **MAY** choose among authentication mechanisms (API keys, OAuth, diff --git a/docs/specification/checkout.md b/docs/specification/checkout.md index a845eb59e..9e566d7ab 100644 --- a/docs/specification/checkout.md +++ b/docs/specification/checkout.md @@ -118,13 +118,17 @@ platform receives messages indicating what's needed to progress. The `messages` array contains errors, warnings, and informational messages about the checkout state. Error messages include a `severity` field that -declares **who resolves the error**: - -| Severity | Meaning | Platform Action | -| :---------------------- | :-------------------------------------------- | :---------------------------- | -| `recoverable` | Platform can fix via API | Resolve using Update Checkout | -| `requires_buyer_input` | Business requires input not available via API | Hand off via `continue_url` | -| `requires_buyer_review` | Buyer review and authorization is required | Hand off via `continue_url` | +reflects the resource state and recommended action. When `ucp.status` +is `"success"`, a resource is returned and severity indicates the +recommended action. When `ucp.status` is `"error"`, no valid resource +exists — severity is `unrecoverable`: + +| Severity | Meaning | Platform Action | +| :---------------------- | :----------------------------------------------- | :---------------------------------------------------------------- | +| `recoverable` | Platform can resolve by modifying inputs via API | Update resource and retry | +| `requires_buyer_input` | Business requires input not available via API | Hand off via `continue_url` | +| `requires_buyer_review` | Buyer review and authorization is required | Hand off via `continue_url` | +| `unrecoverable` | No resource exists to act on | Retry with new resource or inputs, or hand off via `continue_url` | Errors with `requires_*` severity contribute to `status: requires_escalation`. Both result in buyer handoff, but represent different checkout states. @@ -135,6 +139,31 @@ requires information their API doesn't support collecting programmatically. regulatory, or entitlement rules require buyer authorization before order placement (e.g., high-value order approval, first-purchase policy). +When the business cannot create a new resource or the requested resource +no longer exists, the response contains `ucp.status: "error"` with +`messages` describing the failure — no resource is included in the +response body. Error responses MUST use `severity: "unrecoverable"`. +For example, a business may reject a create checkout request where all +items are unavailable: + +```json +{ + "ucp": { "version": "2026-01-11", "status": "error" }, + "messages": [ + { + "type": "error", + "code": "out_of_stock", + "content": "All requested items are currently out of stock", + "severity": "unrecoverable" + } + ], + "continue_url": "https://merchant.com/" +} +``` + +See [REST](checkout-rest.md#create-checkout) and +[MCP](checkout-mcp.md#create_checkout) binding examples. + #### Error Processing Algorithm When status is `incomplete` or `requires_escalation`, platforms should process @@ -174,7 +203,13 @@ Businesses **SHOULD** surface such messages as early as possible, and platforms Example error processing algorithm: ```text -GIVEN checkout with messages array +GIVEN response with messages array + +IF ucp.status = "error" + -- No resource exists; severity is unrecoverable + RETRY with new resource or inputs, or hand off via continue_url + RETURN + FILTER errors FROM messages WHERE type = "error" PARTITION errors INTO @@ -205,6 +240,7 @@ handle with specific, appropriate UX rather than generic error treatment. | `item_unavailable` | Item cannot be purchased (e.g. delisted) | | `address_undeliverable` | Cannot deliver to the provided address | | `payment_failed` | Payment processing failed | +| `eligibility_invalid` | Eligibility claim could not be verified at completion | Businesses **SHOULD** mark standard errors with `severity: recoverable` to signal that platforms should provide appropriate UX (out-of-stock messaging, @@ -214,6 +250,207 @@ messages or deferring to checkout completion. Example: `out_of_stock` requires specific upfront UX, whereas `payment_required` can be handled generically at submission. +#### Eligibility Verification at Completion + +Platforms provide `context.eligibility` — buyer claims about eligible benefits +such as loyalty membership, payment instrument perks, and similar. These are +claims, not verified facts. Businesses **MAY** act on recognized claims during +the session (adjusting pricing, granting product access, applying provisional +discounts), but all accepted claims **MUST** be resolved before the +transaction can complete. + +Unrecognized or inapplicable claims **MUST NOT** block the checkout. +Businesses **SHOULD** notify the buyer via `messages` with `type: "warning"` +when a claim is not accepted, and **MAY** use `type: "info"` to explain +the effects of accepted claims. At completion, accepted claims that remain +unverified **MUST** result in `type: "error"` with +`code: "eligibility_invalid"` (see below). + +**Eligibility message codes:** + +| Type | Code | When | +| --------- | -------------------------- | -------------------------------------------------- | +| `warning` | `eligibility_not_accepted` | Claim not recognized or not applicable | +| `info` | `eligibility_accepted` | Effect of an accepted claim | +| `error` | `eligibility_invalid` | Accepted claim could not be verified at completion | + +A claim is resolved when it is either **verified** or **rescinded**: + +* **Verified**: The Business confirms the claim against a proof provided at + completion time. UCP does not prescribe how verification occurs — proof + may come from the payment credential, an identity verification capability, + or any other mechanism negotiated between Platform and Business. +* **Rescinded**: The Platform removes the claim from `context.eligibility` + before completion (e.g., buyer changes payment method, withdraws a + membership claim). Once removed, the Business recalculates without it. + +Businesses **MUST NOT** complete a transaction with unresolved eligibility +claims. Unverified claims may result in incorrect pricing or unauthorized +access to restricted products. + +**When verification fails:** + +Verification failure **MUST** only affect the `messages` array. The +Business **MUST** return an error in `messages` with +`code: "eligibility_invalid"` and `severity: "recoverable"`. Messages +**SHOULD** use the `path` field to identify which specific claim(s) could +not be verified. The Platform **MAY** then provide valid proof and +resubmit, restructure the checkout (e.g., remove ineligible items, update +claims), or abandon the attempt. + +For example, the Platform claims a store card benefit via +`context.eligibility`. The Business applies member pricing during the session. +At completion, the payment credential does not match the claimed instrument: + +```json +{ + "ucp": { "version": "2026-01-11", "status": "success" }, + "id": "checkout_abc", + "status": "ready_for_complete", + "line_items": [ "..." ], + "totals": [ "..." ], + "messages": [ + { + "type": "error", + "code": "eligibility_invalid", + "severity": "recoverable", + "content": "Payment credential does not match the claimed store card benefit.", + "path": "$.context.eligibility[0]" + } + ] +} +``` + +The Platform can resolve this by having the buyer switch to the qualifying +payment instrument, or by removing the claim from `context.eligibility` to +renegotiate the checkout (obtaining updated pricing, availability, etc.) +and then resubmitting for completion. + +### Warning Presentation + +The `presentation` field on warning messages controls the rendering +contract the platform **MUST** follow. When omitted, it defaults to +`"notice"`. + +| | `notice` (default) | `disclosure` | +| :--- | :--- | :--- | +| Display content | **MUST** | **MUST** | +| Proximity to `path` | **MAY** | **MUST** | +| Dismissible | **MAY** | **MUST NOT** | +| Render `image_url` | **MAY** | **MUST** | +| Render `url` | **MAY** | **SHOULD** | +| Escalate if cannot honor | — | **MUST** via `continue_url` | + +#### `notice` (default) + +The default rendering contract for warnings. Platforms **MUST** display +the warning content to the buyer. Platforms **MAY** render notices in a +banner, tray, or toast, and **MAY** allow the buyer to dismiss them. + +#### `disclosure` + +Warnings with `presentation: "disclosure"` carry notices — safety +warnings, allergen declarations, compliance content, etc. — that +**MUST** follow the prescribed rendering contract below. + +**Platform requirements:** + +* **MUST** display the warning `content` to the buyer. +* **MUST** display the warning in proximity to the component referenced + by `path`, preserving the association between the disclosure and its + subject. When `path` is omitted, the disclosure applies to the response + as a whole. +* **MUST NOT** hide, collapse, or auto-dismiss the warning. +* **MUST** render `image_url` when present (e.g., warning symbol, + energy class label). +* **SHOULD** render `url` as a navigable reference link when present. + +Warnings with `presentation: "disclosure"` **SHOULD** be given rendering +priority over notices. + +Platforms that cannot honor the disclosure rendering contract **MUST** +escalate to merchant UI via `continue_url` rather than silently +downgrading to a notice. + +**Business requirements:** + +* **MUST** set `presentation: "disclosure"` when the warning content must + be displayed alongside a specific component and must not be hidden or + auto-dismissed. +* **SHOULD** use the `path` field to associate disclosures with the + relevant component in the response. +* **SHOULD** provide a `code` that identifies the disclosure category + (e.g., `prop65`, `allergens`, `energy_label`). +* **SHOULD** provide `image_url` when the disclosure has an associated + visual element (e.g., warning symbol, energy class label). +* **SHOULD** provide `url` when a reference link is available for the + buyer to learn more. + +#### Disclosure and Acknowledgment + +The `presentation` field controls how the warning is rendered, not +whether the checkout can proceed. When affirmative buyer acknowledgment +or authorization is also required, the business **MAY** combine the +disclosure with the escalation mechanisms described in the +[Checkout Status Lifecycle](#checkout-status-lifecycle) to ensure the +appropriate buyer input is obtained. + +#### Jurisdiction and Applicability + +It is the business's responsibility to determine which disclosures apply +to a given session and return only those that are relevant. Businesses +**SHOULD** use buyer-provided data (`context` and other inputs) and +product attributes to resolve jurisdiction-specific requirements. +Platforms do not affect or resolve disclosure applicability — they render +what they receive from the business. + +#### Example + +A checkout response containing both a recoverable error and a disclosure +warning on a line item: + +```json +{ + "ucp": { "version": "{{ ucp_version }}", "status": "success" }, + "id": "chk_abc123", + "status": "incomplete", + "currency": "USD", + "line_items": [ + { + "id": "li_1", + "item": { "id": "item_456", "title": "Artisan Nut Butter Collection", "image_url": "https://merchant.com/nut-butter.jpg" }, + "quantity": 1, + "totals": [{ "type": "subtotal", "amount": 1299 }] + } + ], + "totals": [{ "type": "total", "amount": 1299 }], + "messages": [ + { + "type": "error", + "code": "field_required", + "path": "$.buyer.email", + "content": "Buyer email is required", + "severity": "recoverable" + }, + { + "type": "warning", + "code": "allergens", + "path": "$.line_items[0]", + "content": "**Contains: tree nuts.** Produced in a facility that also processes peanuts, milk, and soy.", + "content_type": "markdown", + "presentation": "disclosure", + "image_url": "https://merchant.com/allergen-tree-nuts.svg", + "url": "https://merchant.com/allergen-info" + } + ], + "links": [] +} +``` + +The platform resolves the recoverable error programmatically while +rendering the allergen disclosure in proximity to the referenced line +item. + ## Continue URL The `continue_url` field enables checkout handoff from platform to business UI, @@ -284,13 +521,14 @@ platform can prefill checkout state when initiating a buy-now flow. * Logic handling the checkout sessions **MUST** be deterministic. * **MUST** provide `continue_url` when returning `status` = `requires_escalation`. -* **MUST** include at least one message with `severity: escalation` when - returning `status` = `requires_escalation`. +* **MUST** include at least one message with `severity` of + `requires_buyer_input` or `requires_buyer_review` when returning + `status` = `requires_escalation`. * **SHOULD** provide `continue_url` in all non-terminal checkout responses. * After a checkout session reaches the state "completed", it is considered immutable. -## Capability Schema Definition +## Capability Schema Definition {{ schema_fields('checkout_resp', 'checkout') }} @@ -383,13 +621,27 @@ defined below: ### Context -Context signals are provisional hints. Businesses SHOULD use these values when -authoritative data (e.g. address) is absent, and MAY ignore unsupported values -without returning errors. This differs from authoritative selections which -require explicit validation and error feedback. +Context signals are provisional—not authoritative data. Businesses SHOULD use +these values when verified inputs (e.g., shipping address) are absent, and MAY +ignore or down-rank them if inconsistent with higher-confidence signals +(authenticated account, risk detection) or regulatory constraints (export +controls). Eligibility and policy enforcement MUST occur at checkout time using +binding transaction data. {{ schema_fields('context', 'checkout') }} +### Signals + +Environment data provided by the platform to support authorization +and abuse prevention. Unlike `context` (buyer-asserted preferences) and `buyer` +(self-reported identity), signal values MUST NOT be buyer-asserted claims — +platforms provide signals based on direct observation or by relaying +independently verifiable third-party attestations. See +[Signals](overview.md#signals) for details and privacy +requirements. + +{{ schema_fields('types/signals', 'checkout') }} + ### Fulfillment Option {{ extension_schema_fields('fulfillment.json#/$defs/fulfillment_option', 'checkout') }} @@ -404,7 +656,7 @@ require explicit validation and error feedback. {{ schema_fields('types/item_update_req', 'checkout') }} -#### Item Response +#### Item {{ schema_fields('types/item_resp', 'checkout') }} @@ -418,7 +670,7 @@ require explicit validation and error feedback. {{ schema_fields('types/line_item_update_req', 'checkout') }} -#### Line Item Response +#### Line Item {{ schema_fields('types/line_item_resp', 'checkout') }} @@ -451,6 +703,10 @@ field or omitting them. {{ schema_fields('types/message_error', 'checkout') }} +#### Error Code + +{{ schema_fields('types/error_code', 'checkout') }} + ### Message Info {{ schema_fields('types/message_info', 'checkout') }} @@ -463,9 +719,9 @@ field or omitting them. {{ schema_fields('payment', 'checkout') }} -### Payment Instrument +#### Selected Payment Instrument -{{ schema_fields('payment_instrument', 'checkout') }} +{{ extension_schema_fields('types/payment_instrument.json#/$defs/selected_payment_instrument', 'checkout') }} ### Payment Credential @@ -479,16 +735,152 @@ field or omitting them. {{ extension_schema_fields('capability.json#/$defs/response_schema', 'checkout') }} -### Total - -#### Total Response +### Total {: #totals } {{ schema_fields('types/total_resp', 'checkout') }} -### UCP Response Checkout +#### Rendering Contract + +Businesses are the authoritative source for presented totals — their content +and order — because the correct presentation is subject to regional, product, +and regulatory requirements that the business is obligated to satisfy (e.g., +multi-jurisdiction tax itemization, mandatory fee disclosures). + +Platforms MUST render all top-level entries in the order provided: + +```python +for entry in totals: + render_line(entry.display_text, entry.amount) +``` + +Platforms MAY render sub-lines as supplementary detail: + +```python +for entry in totals: + render_line(entry.display_text, entry.amount) + if entry.lines: + for sub in entry.lines: + render_detail_line(sub.display_text, sub.amount) +``` + +Platforms MUST NOT interpret, filter, reorder, aggregate, or apply display +logic of their own. + +Invariants of `totals[]`: + +* Every entry carries a `type` and an `amount`. Platforms SHOULD use + `display_text` when provided. Well-known types have default display labels + as fallback (see table below); unknown types MUST include `display_text`. +* Amounts are signed integers — negative values are subtractive (e.g., + discounts), positive values are additive. The sign IS the direction. +* Exactly one `type: "subtotal"` MUST be present. +* Exactly one `type: "total"` MUST be present. + +#### Verification + +Platforms MUST NOT substitute their own computed totals for the business's +values. Platforms MAY verify the provided totals: + +```python +assert sum(e.amount for e in totals if e.type != "total") == total_entry.amount +``` + +If the computed sum does not match the `type: "total"` entry, the platform +MUST NOT alter the rendered output — the business's presented totals are +authoritative for display. However, platforms MUST NOT autonomously complete +a checkout with mismatched totals. Platforms SHOULD reject the checkout or +escalate and ask for buyer review via `continue_url`. + +#### Well-Known Types + +| Type | Sign | Default label | Meaning | +| ----------------- | ---- | ---------------- | ----------------------------------------- | +| `subtotal` | + | Subtotal | Sum of line item prices | +| `discount` | − | Discount | Order or line-item level discount | +| `items_discount` | − | Item Discounts | Rollup of line-item discounts | +| `fulfillment` | + | Shipping | Shipping, delivery, or pickup charges | +| `tax` | + | Tax | Tax charges | +| `fee` | + | Fee | Fees and surcharges | +| `total` | = | Total | Authoritative grand total (exactly one) | + +When `display_text` is provided, platforms MUST use it. When omitted on a +well-known type, platforms SHOULD use the default label above. The sign +convention for well-known types is schema-enforced: subtractive types +(discount, items_discount) MUST have negative amounts; additive types +(subtotal, fulfillment, tax, fee) MUST have non-negative amounts. + +The `type` field is an open string — businesses MAY use values beyond the +well-known set. Unknown types MUST include `display_text` (schema-enforced) +and the sign on the amount is self-describing. + +#### Repeating Types + +All types except `subtotal` and `total` MAY appear multiple times — +for example, multi-jurisdiction tax lines or itemized fees. + +#### Sub-Lines (`lines`) + +Each top-level entry MAY include a `lines` array. Sub-lines share the same +base shape as top-level entries — `display_text` and `amount` — providing an +itemized breakdown under the parent. + +**Invariant:** `sum(lines[].amount)` MUST equal the parent entry's `amount`. + +The business controls what MUST be rendered (top-level entries) versus what +MAY be optionally surfaced (sub-lines). Platforms SHOULD render sub-lines +when provided. + +#### Examples + +**Split tax, itemized at top-level:** + +```json +"totals": [ + { "type": "subtotal", "display_text": "Subtotal", "amount": 5750 }, + { "type": "fulfillment", "display_text": "Shipping", "amount": 899 }, + { "type": "tax", "display_text": "Federal Tax", "amount": 332 }, + { "type": "tax", "display_text": "State Tax", "amount": 465 }, + { "type": "total", "display_text": "Total", "amount": 7446 } +] +``` + +**Collapsed fees with optional breakdown:** + +```json +"totals": [ + { "type": "subtotal", "display_text": "Subtotal", "amount": 4999 }, + { + "type": "fee", "display_text": "Fees", "amount": 549, + "lines": [ + { "display_text": "Service Fee", "amount": 399 }, + { "display_text": "Recycling Fee", "amount": 150 } + ] + }, + { "type": "tax", "display_text": "Tax", "amount": 444 }, + { "type": "total", "display_text": "Total", "amount": 5992 } +] +``` + +**Discount and account credit — negative amounts:** + +```json +"totals": [ + { "type": "subtotal", "display_text": "Subtotal", "amount": 10000 }, + { "type": "discount", "display_text": "Summer Sale", "amount": -1500 }, + { "type": "tax", "display_text": "Tax", "amount": 680 }, + { "type": "account_credit", "display_text": "Account Credit", "amount": -2500 }, + { "type": "total", "display_text": "Amount Due", "amount": 6680 } +] +``` + +### UCP Response Checkout {: #ucp-response-checkout-schema } {{ extension_schema_fields('ucp.json#/$defs/response_checkout_schema', 'checkout') }} ### Order Confirmation {{ schema_fields('order_confirmation', 'checkout') }} + +### Error Response + +{{ schema_fields('types/error_response', 'checkout') }} diff --git a/docs/specification/discount.md b/docs/specification/discount.md index b9ea735b3..f8f7c69f7 100644 --- a/docs/specification/discount.md +++ b/docs/specification/discount.md @@ -19,8 +19,8 @@ ## Overview Discount extension allows businesses to indicate that they support discount -codes on checkout sessions, and specifies how the discount codes are to be -shared between the platform and the business. +codes on cart and checkout sessions, and specifies how the discount codes are +to be shared between the platform and the business. **Key features:** @@ -31,23 +31,24 @@ shared between the platform and the business. **Dependencies:** -- Checkout Capability +- Cart Capability or Checkout Capability ## Discovery -Businesses advertise discount support in their profile: +Businesses advertise discount support in their profile. The capability can +extend cart, checkout, or both: ```json { "ucp": { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "capabilities": { "dev.ucp.shopping.discount": [ { - "version": "2026-01-11", - "extends": "dev.ucp.shopping.checkout", - "spec": "https://ucp.dev/specification/discount", - "schema": "https://ucp.dev/schemas/shopping/discount.json" + "version": "{{ ucp_version }}", + "extends": ["dev.ucp.shopping.cart", "dev.ucp.shopping.checkout"], + "spec": "https://ucp.dev/{{ ucp_version }}/specification/discount", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/discount.json" } ] } @@ -55,9 +56,14 @@ Businesses advertise discount support in their profile: } ``` +Businesses MAY advertise discount support for cart only, checkout only, or +both. Platforms SHOULD check which resources are extended before submitting +discount codes. + ## Schema -When this capability is active, checkout is extended with a `discounts` object. +When this capability is active, cart and/or checkout are extended with a +`discounts` object. ### Discounts Object @@ -74,6 +80,9 @@ When this capability is active, checkout is extended with a `discounts` object. ## Allocation Details The `applied` array explains how discounts were calculated and distributed. +The `applied[].amount` describes the magnitude of the applied discount (always +positive); the corresponding `totals[]` entry amount represents its signed +effect on the receipt (negative for discounts). ### Allocation Method @@ -116,7 +125,8 @@ each line item, even when multiple discounts stack. ## Operations -Discount codes are submitted via standard checkout create/update operations. +Discount codes are submitted via standard cart or checkout create/update +operations. The same semantics apply to both resources. **Request behavior:** @@ -130,6 +140,12 @@ Discount codes are submitted via standard checkout create/update operations. - Rejected codes communicated via `messages[]` (see below) - Discount amounts reflected in `totals[]` and `line_items[].discount` +**Cart-to-checkout continuity:** When a cart is converted to a checkout via the +cart capability's `cart_id` field, businesses MUST carry forward any discount +codes that were applied to the cart. Codes that are no longer valid at checkout +time (e.g., expired, ineligible) SHOULD be communicated via `messages[]` using +standard rejection codes. + ## Rejected Codes When a submitted discount code cannot be applied, businesses communicate this @@ -175,10 +191,105 @@ segment, or promotional rules: - Cannot be removed by the platform - Surfaced for transparency (platform can explain to user why discount was applied) +## Eligibility Claims + +Eligibility claims are buyer claims about eligible benefits (see +[Context](checkout.md#context)) such as loyalty membership, payment instrument +perks, and similar. When the discount extension is active, Businesses that +choose to accept eligibility claims **MUST** surface their effect on pricing +as provisional discounts in the `applied` array. Platforms **MUST** display +provisional discounts to the buyer. + +### Discount Behavior + +Platforms send buyer claims via `context.eligibility` on cart or checkout +requests (see [Context](checkout.md#context)). When a Business recognizes a +claim and it affects pricing, it **MUST** surface a corresponding provisional +discount in the `discounts.applied` array. This gives the Platform structured +attribution to display to the buyer. + +Eligibility-triggered discounts use the following fields: + +| Field | Value | Purpose | +| ------------- | -------------------------- | --------------------------------------- | +| `automatic` | `true` | No code required | +| `provisional` | `true` | Requires verification at completion | +| `eligibility` | `"com.example.store_card"` | The accepted claim | +| `code` | *(omitted)* | Not code-based | + +Standard `priority`, `method`, and `allocations` fields apply for stacking with +other discounts. + +### Verification at Checkout + +Discounts from accepted but unverified claims carry `provisional: true`. +Provisional discounts remain until the claim is verified, rescinded, or +replaced during the session. At checkout completion, all remaining provisional +claims **MUST** be resolved (see +[Eligibility Verification at Completion](checkout.md#eligibility-verification-at-completion)). + +### Example: Provisional Discount with Attribution + +Building on the store card example from +[Eligibility Verification at Completion](checkout.md#eligibility-verification-at-completion), +the discount extension provides structured attribution. The Platform claims a +store card benefit; the Business surfaces the provisional discount with full +stacking and allocation details: + +=== "Request" + + ```json + { + "context": { + "eligibility": ["com.example.store_card"] + }, + "line_items": [ + { + "item": { + "id": "prod_shirt", + "quantity": 2, + "price": 2500 + } + } + ] + } + ``` + +=== "Response" + + ```json + { + "discounts": { + "applied": [ + { + "title": "Store Card 5% Off", + "amount": 250, + "automatic": true, + "provisional": true, + "eligibility": "com.example.store_card", + "priority": 1, + "method": "each", + "allocations": [ + {"path": "$.line_items[0]", "amount": 250} + ] + } + ] + }, + "totals": [ + {"type": "subtotal", "display_text": "Subtotal", "amount": 5000}, + {"type": "items_discount", "display_text": "Discounts", "amount": -250}, + {"type": "total", "display_text": "Total", "amount": 4750} + ] + } + ``` + +The Platform can now render: "Store Card 5% Off: -$2.50 *(verified at +purchase)*" with full confidence in the attribution, amount, and allocation. + ## Impact on Line Items and Totals -Applied discounts are reflected in the core checkout fields using two distinct -total types: +Applied discounts are reflected in the core cart or checkout fields using two +distinct total types: | Total Type | When to Use | | ---------------- | --------------------------------------------------------- | @@ -206,105 +317,175 @@ subtractive (e.g., "-$13.99"). ## Examples +### Cart with discount codes + +Discount codes applied during cart exploration. The cart response includes +estimated discount amounts, giving the buyer visibility into savings before +proceeding to checkout. + +=== "Request" + + ```json + { + "line_items": [ + { + "item": { + "id": "prod_1", + "quantity": 2, + "title": "T-Shirt", + "price": 2000 + } + } + ], + "discounts": { + "codes": ["SUMMER20"] + } + } + ``` + +=== "Response" + + ```json + { + "id": "cart_abc123", + "line_items": [ + { + "id": "li_1", + "item": { + "id": "prod_1", + "quantity": 2, + "title": "T-Shirt", + "price": 2000 + }, + "totals": [ + {"type": "subtotal", "amount": 4000}, + {"type": "items_discount", "amount": -800}, + {"type": "total", "amount": 3200} + ] + } + ], + "discounts": { + "codes": ["SUMMER20"], + "applied": [ + { + "code": "SUMMER20", + "title": "Summer Sale 20% Off", + "amount": 800, + "method": "each", + "allocations": [ + {"path": "$.line_items[0]", "amount": 800} + ] + } + ] + }, + "currency": "USD", + "totals": [ + {"type": "subtotal", "display_text": "Subtotal", "amount": 4000}, + {"type": "items_discount", "display_text": "Item Discounts", "amount": -800}, + {"type": "total", "display_text": "Estimated Total", "amount": 3200} + ] + } + ``` + ### Order-level discount A flat discount applied to the order total. No allocations—the discount applies to the order as a whole and uses `type: "discount"` in totals. -**Request:** +=== "Request" -```json -{ - "discounts": { - "codes": ["SAVE10"] - } -} -``` + ```json + { + "discounts": { + "codes": ["SAVE10"] + } + } + ``` -**Response:** +=== "Response" -```json -{ - "discounts": { - "codes": ["SAVE10"], - "applied": [ - { - "code": "SAVE10", - "title": "$10 Off Your Order", - "amount": 1000 - } - ] - }, - "totals": [ - {"type": "subtotal", "display_text": "Subtotal", "amount": 5000}, - {"type": "discount", "display_text": "Order Discount", "amount": 1000}, - {"type": "total", "display_text": "Total", "amount": 4000} - ] -} -``` + ```json + { + "discounts": { + "codes": ["SAVE10"], + "applied": [ + { + "code": "SAVE10", + "title": "$10 Off Your Order", + "amount": 1000 + } + ] + }, + "totals": [ + {"type": "subtotal", "display_text": "Subtotal", "amount": 5000}, + {"type": "discount", "display_text": "Order Discount", "amount": -1000}, + {"type": "total", "display_text": "Total", "amount": 4000} + ] + } + ``` ### Mixed discounts (item + order level) This example shows both discount types: a per-item discount (20% off) allocated to line items, and an automatic shipping discount at the order level. -**Request:** +=== "Request" -```json -{ - "discounts": { - "codes": ["SUMMER20"] - } -} -``` + ```json + { + "discounts": { + "codes": ["SUMMER20"] + } + } + ``` -**Response:** +=== "Response" -```json -{ - "line_items": [ + ```json { - "id": "li_1", - "item": { - "id": "prod_1", - "quantity": 2, - "title": "T-Shirt", - "price": 2000 + "line_items": [ + { + "id": "li_1", + "item": { + "id": "prod_1", + "quantity": 2, + "title": "T-Shirt", + "price": 2000 + }, + "totals": [ + {"type": "subtotal", "amount": 4000}, + {"type": "items_discount", "amount": -800}, + {"type": "total", "amount": 3200} + ] + } + ], + "discounts": { + "codes": ["SUMMER20"], + "applied": [ + { + "code": "SUMMER20", + "title": "Summer Sale 20% Off", + "amount": 800, + "allocations": [ + {"path": "$.line_items[0]", "amount": 800} + ] + }, + { + "title": "Free shipping on orders over $30", + "amount": 599, + "automatic": true + } + ] }, "totals": [ - {"type": "subtotal", "amount": 4000}, - {"type": "items_discount", "amount": 800}, - {"type": "total", "amount": 3200} + {"type": "subtotal", "display_text": "Subtotal", "amount": 4000}, + {"type": "items_discount", "display_text": "Item Discounts", "amount": -800}, + {"type": "discount", "display_text": "Order Discounts", "amount": -599}, + {"type": "fulfillment", "display_text": "Shipping", "amount": 0}, + {"type": "total", "display_text": "Total", "amount": 2601} ] } - ], - "discounts": { - "codes": ["SUMMER20"], - "applied": [ - { - "code": "SUMMER20", - "title": "Summer Sale 20% Off", - "amount": 800, - "allocations": [ - {"path": "$.line_items[0]", "amount": 800} - ] - }, - { - "title": "Free shipping on orders over $30", - "amount": 599, - "automatic": true - } - ] - }, - "totals": [ - {"type": "subtotal", "display_text": "Subtotal", "amount": 4000}, - {"type": "items_discount", "display_text": "Item Discounts", "amount": 800}, - {"type": "discount", "display_text": "Order Discounts", "amount": 599}, - {"type": "fulfillment", "display_text": "Shipping", "amount": 0}, - {"type": "total", "display_text": "Total", "amount": 2601} - ] -} -``` + ``` ### Rejected discount code @@ -312,114 +493,114 @@ When a discount code cannot be applied, the rejection is communicated via the `messages[]` array. The code still appears in `discounts.codes` (echoed back) but not in `discounts.applied`. -**Request:** +=== "Request" -```json -{ - "discounts": { - "codes": ["SAVE10", "EXPIRED50"] - } -} -``` + ```json + { + "discounts": { + "codes": ["SAVE10", "EXPIRED50"] + } + } + ``` -**Response:** +=== "Response" -```json -{ - "discounts": { - "codes": ["SAVE10", "EXPIRED50"], - "applied": [ - { - "code": "SAVE10", - "title": "$10 Off Your Order", - "amount": 1000 - } - ] - }, - "totals": [ - {"type": "subtotal", "display_text": "Subtotal", "amount": 5000}, - {"type": "discount", "display_text": "Order Discount", "amount": 1000}, - {"type": "total", "display_text": "Total", "amount": 4000} - ], - "messages": [ + ```json { - "type": "warning", - "code": "discount_code_expired", - "path": "$.discounts.codes[1]", - "content": "Code 'EXPIRED50' expired on December 1st" + "discounts": { + "codes": ["SAVE10", "EXPIRED50"], + "applied": [ + { + "code": "SAVE10", + "title": "$10 Off Your Order", + "amount": 1000 + } + ] + }, + "totals": [ + {"type": "subtotal", "display_text": "Subtotal", "amount": 5000}, + {"type": "discount", "display_text": "Order Discount", "amount": -1000}, + {"type": "total", "display_text": "Total", "amount": 4000} + ], + "messages": [ + { + "type": "warning", + "code": "discount_code_expired", + "path": "$.discounts.codes[1]", + "content": "Code 'EXPIRED50' expired on December 1st" + } + ] } - ] -} -``` + ``` ### Stacked discounts with allocations Multiple discounts applied with full allocation breakdown: -**Response:** +=== "Response" -```json -{ - "line_items": [ - { - "id": "li_1", - "item": { - "title": "T-Shirt", - "price": 6000 - }, - "totals": [ - {"type": "subtotal", "amount": 6000}, - {"type": "items_discount", "amount": 1500}, - {"type": "total", "amount": 4500} - ] - }, + ```json { - "id": "li_2", - "item": { - "title": "Socks", - "price": 4000 + "line_items": [ + { + "id": "li_1", + "item": { + "title": "T-Shirt", + "price": 6000 + }, + "totals": [ + {"type": "subtotal", "amount": 6000}, + {"type": "items_discount", "amount": -1500}, + {"type": "total", "amount": 4500} + ] + }, + { + "id": "li_2", + "item": { + "title": "Socks", + "price": 4000 + }, + "totals": [ + {"type": "subtotal", "amount": 4000}, + {"type": "items_discount", "amount": -1000}, + {"type": "total", "amount": 3000} + ] + } + ], + "discounts": { + "codes": ["SUMMER20", "LOYALTY5"], + "applied": [ + { + "code": "SUMMER20", + "title": "Summer Sale 20% Off", + "amount": 2000, + "method": "each", + "priority": 1, + "allocations": [ + {"path": "$.line_items[0]", "amount": 1200}, + {"path": "$.line_items[1]", "amount": 800} + ] + }, + { + "code": "LOYALTY5", + "title": "$5 Loyalty Reward", + "amount": 500, + "method": "across", + "priority": 2, + "allocations": [ + {"path": "$.line_items[0]", "amount": 300}, + {"path": "$.line_items[1]", "amount": 200} + ] + } + ] }, "totals": [ - {"type": "subtotal", "amount": 4000}, - {"type": "items_discount", "amount": 1000}, - {"type": "total", "amount": 3000} + {"type": "subtotal", "display_text": "Subtotal", "amount": 10000}, + {"type": "items_discount", "display_text": "Item Discounts", "amount": -2500}, + {"type": "total", "display_text": "Total", "amount": 7500} ] } - ], - "discounts": { - "codes": ["SUMMER20", "LOYALTY5"], - "applied": [ - { - "code": "SUMMER20", - "title": "Summer Sale 20% Off", - "amount": 2000, - "method": "each", - "priority": 1, - "allocations": [ - {"path": "$.line_items[0]", "amount": 1200}, - {"path": "$.line_items[1]", "amount": 800} - ] - }, - { - "code": "LOYALTY5", - "title": "$5 Loyalty Reward", - "amount": 500, - "method": "across", - "priority": 2, - "allocations": [ - {"path": "$.line_items[0]", "amount": 300}, - {"path": "$.line_items[1]", "amount": 200} - ] - } - ] - }, - "totals": [ - {"type": "subtotal", "display_text": "Subtotal", "amount": 10000}, - {"type": "items_discount", "display_text": "Item Discounts", "amount": 2500}, - {"type": "total", "display_text": "Total", "amount": 7500} - ] -} -``` + ``` With this data, an agent can explain: > "Your T-Shirt ($60) got $12 off from the 20% summer sale, plus $3 from your diff --git a/docs/specification/embedded-checkout.md b/docs/specification/embedded-checkout.md index 59dbf9c8d..4ceb2ef56 100644 --- a/docs/specification/embedded-checkout.md +++ b/docs/specification/embedded-checkout.md @@ -95,21 +95,21 @@ profile, they declare support for the Embedded Checkout Protocol. "services": { "dev.ucp.shopping": [ { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "transport": "rest", - "schema": "https://ucp.dev/services/shopping/openapi.json", + "schema": "https://ucp.dev/{{ ucp_version }}/services/shopping/rest.openapi.json", "endpoint": "https://merchant.example.com/ucp/v1" }, { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "transport": "mcp", - "schema": "https://ucp.dev/services/shopping/mcp.openrpc.json", + "schema": "https://ucp.dev/{{ ucp_version }}/services/shopping/mcp.openrpc.json", "endpoint": "https://merchant.example.com/ucp/mcp" }, { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "transport": "embedded", - "schema": "https://ucp.dev/services/shopping/embedded.openrpc.json" + "schema": "https://ucp.dev/{{ ucp_version }}/services/shopping/embedded.openrpc.json" } ] } @@ -134,14 +134,14 @@ indicate ECP availability and allowed delegations for a specific session. "status": "open", "continue_url": "https://merchant.example.com/checkout/abc123", "ucp": { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "services": { "dev.ucp.shopping": [ { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "transport": "embedded", "config": { - "delegate": ["payment.credential", "fulfillment.address_change"] + "delegate": ["payment.credential", "fulfillment.address_change", "window.open"] } } ] @@ -250,12 +250,13 @@ message following a consistent pattern: `ec.{delegation}_request` | `payment.instruments_change` | `ec.payment.instruments_change_request` | | `payment.credential` | `ec.payment.credential_request` | | `fulfillment.address_change` | `ec.fulfillment.address_change_request` | +| `window.open` | `ec.window.open_request` | Extensions define their own delegation identifiers; see each extension's specification for available options. ```text -?ec_version=2026-01-11&ec_delegate=payment.instruments_change,payment.credential,fulfillment.address_change +?ec_version=2026-01-11&ec_delegate=payment.instruments_change,payment.credential,fulfillment.address_change,window.open ``` #### Color Scheme @@ -333,7 +334,7 @@ The Embedded Checkout determines which delegations to honor based on: The Embedded Checkout **MUST** indicate accepted delegations in the `ec.ready` request via the `delegate` field (see [`ec.ready`](#ecready)). If a requested delegation is not accepted, the Embedded Checkout **MUST** handle that -capability using its own UI. +action using its own UI. #### Binding Requirements @@ -354,7 +355,7 @@ capability using its own UI. #### 3.3.3 Delegation Flow -1. **Request**: Embedded Checkout sends an `ec.{capability}.{action}_request` +1. **Request**: Embedded Checkout sends an `ec.{domain}.{action}_request` message with current state (includes `id`) 2. **Native UI**: Host presents native UI for the delegated action 3. **Response**: host sends back a JSON-RPC response with matching `id` and @@ -362,9 +363,10 @@ capability using its own UI. 4. **Update**: Embedded Checkout updates its state and may send subsequent change notifications -See [Payment Extension](#payment-extension) and -[Fulfillment Extension](#fulfillment-extension) for -capability-specific delegation details. +See [Payment Extension](#payment-extension), +[Fulfillment Extension](#fulfillment-extension), and +[Window Extension](#window-extension) for +domain-specific delegation details. ### Navigation Constraints @@ -501,14 +503,15 @@ all implementations. All messages are sent from Embedded Checkout to host. Extensions **MAY** extend the Embedded protocol by defining additional messages. Extension messages **MUST** follow the naming convention: -- **Notifications**: `ec.{capability}.change` — state change notifications (no +- **Notifications**: `ec.{domain}.change` — state change notifications (no `id`) -- **Delegation requests**: `ec.{capability}.{action}_request` — requires +- **Delegation requests**: `ec.{domain}.{action}_request` — requires response (has `id`) Where: -- `{capability}` matches the capability identifier from discovery +- `{domain}` matches the domain identifier from discovery (e.g., `payment`, + `fulfillment`, `window`) - `{action}` describes the specific action being delegated (e.g., `instruments_change`, `address_change`) - `_request` suffix signals this is a delegation point requiring a response @@ -554,7 +557,7 @@ actions. "id": "ready_1", "method": "ec.ready", "params": { - "delegate": ["payment.credential", "fulfillment.address_change"] + "delegate": ["payment.credential", "fulfillment.address_change", "window.open"] } } ``` @@ -1265,8 +1268,110 @@ rather than attempting to merge the new data with existing state. The address object uses the UCP [PostalAddress](site:specification/checkout/#postal-address) format: +### Postal Address + {{ schema_fields('postal_address', 'embedded-checkout') }} +## Window Extension + +The window extension defines how the Embedded Checkout notifies the host when +the buyer activates a link presented by the business. When a checkout URL +includes `ec_delegate=window.open`, the host **MUST** handle every +`ec.window.open_request` and acknowledge the request. + +This is distinct from +[Navigation Constraints](#navigation-constraints), which the Embedded Checkout +enforces unconditionally to prevent navigation to unrelated pages. + +### Window Overview & Host Choice + +Window delegation allows for two different patterns: + +**Option A: Host Delegates to Embedded Checkout** The host does NOT include +`window.open` in `ec_delegate`. The Embedded Checkout handles link presentation +using its own inline UI. This is the standard, non-delegated flow. + +**Option B: Host Takes Control** The host includes +`ec_delegate=window.open` in the Checkout URL, informing the Embedded Checkout +to send `ec.window.open_request` when the buyer activates a link. When delegated: + +**Embedded Checkout responsibilities**: + +- **MUST** send `ec.window.open_request` when the buyer activates a link + presented by the business + +**Host responsibilities**: + +- **MUST** validate that the requested URL uses the `https` scheme +- **SHOULD** apply additional host security policies (e.g., verifying + origins) +- **MUST** present the content to the buyer for every approved request + (e.g., in a modal, new tab, or similar) +- **MUST** respond with a JSON-RPC success result when the request was + processed, or a `window_open_rejected_error` error if host policy prevented + the navigation +- **MAY** notify the buyer if the request was rejected + +By accepting `window.open` delegation, the host assumes responsibility for +handling the buyer's link interactions. The Embedded Checkout **MUST NOT** +present its own UI for the link. + +The `ec.window.open_request` payload contains only the URL. Hosts that need +richer context (e.g., link type or label) **MAY** cross-reference the requested +URL against the `checkout.links` array from the checkout session to obtain +additional metadata. + +### Window Message API Reference + +#### `ec.window.open_request` + +Requests the host to handle a link activated by the buyer within the checkout. + +- **Direction:** Embedded Checkout → Host +- **Type:** Request +- **Payload:** + - `url` (string, uri, **REQUIRED**): The URL of the resource to present. + +**Example Message:** + +```json +{ + "jsonrpc": "2.0", + "id": "window_1", + "method": "ec.window.open_request", + "params": { + "url": "https://merchant.com/privacy-policy" + } +} +``` + +- **Direction:** Host → Embedded Checkout +- **Type:** Response +- **Payload:** Empty object (`{}`). + +**Example Success Response:** + +```json +{ + "jsonrpc": "2.0", + "id": "window_1", + "result": {} +} +``` + +**Example Error Response:** + +```json +{ + "jsonrpc": "2.0", + "id": "window_1", + "error": { + "code": "window_open_rejected_error", + "message": "Window open rejected by host." + } +} +``` + ## Security & Error Handling ### Error Codes @@ -1277,13 +1382,14 @@ error codes mapped to **[W3C DOMException](https://webidl.spec.whatwg.org/#idl-DOMException)** names where possible. -| Code | Description | -| :-------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------- | -| `abort_error` | The user cancelled the interaction (e.g., closed the sheet). | -| `security_error` | The host origin validation failed. | -| `not_supported_error` | The requested payment method is not supported by the host. | -| `invalid_state_error` | Handshake was attempted out of order. | -| `not_allowed_error` | The request was missing valid User Activation (see [Prevention of Unsolicited Payment Requests](#prevention-of-unsolicited-payment-requests)). | +| Code | Description | +| :--------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------- | +| `abort_error` | The user cancelled the interaction (e.g., closed the sheet). | +| `security_error` | The host origin validation failed. | +| `not_supported_error` | The requested payment method is not supported by the host. | +| `invalid_state_error` | Handshake was attempted out of order. | +| `not_allowed_error` | The request was missing valid User Activation (see [Prevention of Unsolicited Payment Requests](#prevention-of-unsolicited-payment-requests)). | +| `window_open_rejected_error` | Host policy prevented the navigation. The host **MAY** notify the buyer that their request was rejected. | ### Security for Web-Based Hosts @@ -1382,10 +1488,30 @@ account, or wallet credential) available to the buyer. {{ schema_fields('payment_instrument', 'embedded-checkout') }} -### Payment Handler Response +#### Selected Payment Instrument + +{{ extension_schema_fields('types/payment_instrument.json#/$defs/selected_payment_instrument', 'embedded-checkout') }} + +### Card Payment Instrument + +{{ schema_fields('types/card_payment_instrument', 'embedded-checkout') }} + +### Payment Credential + +{{ schema_fields('types/payment_credential', 'embedded-checkout') }} + +### Token Credential + +{{ schema_fields('types/token_credential_resp', 'embedded-checkout') }} + +### Card Credential + +{{ schema_fields('types/card_credential', 'embedded-checkout') }} + +### Payment Handler Represents the processor or wallet provider responsible for authenticating and processing a specific payment instrument (e.g., Google Pay, Stripe, or a Bank App). -{{ schema_fields('payment_handler_resp', 'embedded-checkout') }} +{{ extension_schema_fields('payment_handler.json#/$defs/response_schema', 'embedded-checkout') }} diff --git a/docs/specification/examples/encrypted-credential-handler.md b/docs/specification/examples/encrypted-credential-handler.md index 7bcdd47ce..97b175fb5 100644 --- a/docs/specification/examples/encrypted-credential-handler.md +++ b/docs/specification/examples/encrypted-credential-handler.md @@ -121,7 +121,7 @@ Businesses advertise the platform's handler. The `business_id` field identifies the business, which the platform uses to look up the correct public key for encryption. -The only supported instrument schema is [CardPaymentInstrument](https://ucp.dev/schemas/shopping/types/card_payment_instrument.json), the only supported checkout credential schema is `EncryptedCredential`, and the only supported source credential schema is [CardCredential](https://ucp.dev/schemas/shopping/types/card_credential.json). +The only supported instrument schema is [CardPaymentInstrument](site:schemas/shopping/types/card_payment_instrument.json), the only supported checkout credential schema is `EncryptedCredential`, and the only supported source credential schema is [CardCredential](site:schemas/shopping/types/card_credential.json). **Note:** The `EncryptedCredential` shape would be formally defined in the handler's schema (referenced via the `schema` field in the handler declaration). @@ -144,12 +144,12 @@ have their own compliance requirements. ```json { "ucp": { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "payment_handlers": { "com.example.platform_encrypted": [ { "id": "platform_encrypted", - "version": "2026-01-11", + "version": "{{ ucp_version }}", "spec": "https://platform.example.com/ucp/encrypted-handler.json", "schema": "https://platform.example.com/ucp/encrypted-handler/schema.json", "available_instruments": [ @@ -188,7 +188,7 @@ The response config includes information about the encryption used. ```json { "id": "platform_encrypted", - "version": "2026-01-11", + "version": "{{ ucp_version }}", "available_instruments": [ { "type": "card", @@ -254,12 +254,12 @@ registry using `platform_config`. ```json { "ucp": { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "payment_handlers": { "com.example.platform_encrypted": [ { "id": "platform_encrypted", - "version": "2026-01-11", + "version": "{{ ucp_version }}", "spec": "https://platform.example.com/ucp/encrypted-handler.json", "schema": "https://platform.example.com/ucp/encrypted-handler/schema.json", "available_instruments": [ @@ -328,8 +328,9 @@ Content-Type: application/json } ] }, - "risk_signals": { - // ... the key value pair for potential risk signal data + "signals": { + "dev.ucp.buyer_ip": "203.0.113.42", + "dev.ucp.user_agent": "Mozilla/5.0 ..." } } ``` @@ -353,5 +354,5 @@ Content-Type: application/json ## References -* **Identity Schema:** `https://ucp.dev/schemas/shopping/types/payment_identity.json` -* **Instrument Schema:** `https://ucp.dev/schemas/shopping/types/card_payment_instrument.json` +* **Identity Schema:** [schemas/shopping/types/payment_identity.json](site:schemas/shopping/types/payment_identity.json) +* **Instrument Schema:** [schemas/shopping/types/card_payment_instrument.json](site:schemas/shopping/types/card_payment_instrument.json) diff --git a/docs/specification/examples/platform-tokenizer-payment-handler.md b/docs/specification/examples/platform-tokenizer-payment-handler.md index 42f71c96d..62bbc41e6 100644 --- a/docs/specification/examples/platform-tokenizer-payment-handler.md +++ b/docs/specification/examples/platform-tokenizer-payment-handler.md @@ -176,7 +176,7 @@ platform's handler specification (referenced via `spec`) documents the `/detokenize` endpoint URL exposed by the platform's **payment credential provider**. -The handler accepts [CardCredential](https://ucp.dev/schemas/shopping/types/card_credential.json) for tokenization and produces [TokenCredential](https://ucp.dev/schemas/shopping/types/token_credential.json) for checkout. +The handler accepts [CardCredential](site:schemas/shopping/types/card_credential.json) for tokenization and produces [TokenCredential](site:schemas/shopping/types/token_credential.json) for checkout. **Note:** The result of `/detokenize` contains **sensitive payment data**. Both the sender (platform's credential provider) and receiver @@ -195,12 +195,12 @@ credential type (e.g., PCI DSS for cards). ```json { "ucp": { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "payment_handlers": { "com.example.platform_tokenizer": [ { "id": "platform_wallet", - "version": "2026-01-11", + "version": "{{ ucp_version }}", "spec": "https://platform.example.com/ucp/handler.json", "schema": "https://platform.example.com/ucp/handler/schema.json", "available_instruments": [ @@ -237,7 +237,7 @@ The response config includes runtime token lifecycle information. ```json { "id": "platform_wallet", - "version": "2026-01-11", + "version": "{{ ucp_version }}", "available_instruments": [ { "type": "card", @@ -326,12 +326,12 @@ registry using `platform_config`. ```json { "ucp": { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "payment_handlers": { "com.example.platform_tokenizer": [ { "id": "platform_wallet", - "version": "2026-01-11", + "version": "{{ ucp_version }}", "spec": "https://platform.example.com/ucp/handler.json", "schema": "https://platform.example.com/ucp/handler/schema.json", "available_instruments": [ @@ -396,8 +396,9 @@ Content-Type: application/json } ] }, - "risk_signals": { - // ... the key value pair for potential risk signal data + "signals": { + "dev.ucp.buyer_ip": "203.0.113.42", + "dev.ucp.user_agent": "Mozilla/5.0 ..." } } ``` @@ -488,6 +489,6 @@ The platform's payment credential provider verifies that: ## References -* **Pattern:** [Tokenization Payment Handler](https://ucp.dev/specification/payment-handler-guide) -* **API Pattern:** `https://ucp.dev/handlers/tokenization/openapi.json` -* **Identity Schema:** `https://ucp.dev/schemas/shopping/types/payment_identity.json` +* **Pattern:** [Tokenization Payment Handler](../payment-handler-guide.md) +* **API Pattern:** [handlers/tokenization/openapi.json](site:handlers/tokenization/openapi.json) +* **Identity Schema:** [schemas/shopping/types/payment_identity.json](site:schemas/shopping/types/payment_identity.json) diff --git a/docs/specification/examples/processor-tokenizer-payment-handler.md b/docs/specification/examples/processor-tokenizer-payment-handler.md index eea4ffd5c..d48063909 100644 --- a/docs/specification/examples/processor-tokenizer-payment-handler.md +++ b/docs/specification/examples/processor-tokenizer-payment-handler.md @@ -110,12 +110,12 @@ The handler's specification (referenced via the `spec` field) documents the ```json { "ucp": { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "payment_handlers": { "com.example.processor_tokenizer": [ { "id": "processor_tokenizer", - "version": "2026-01-11", + "version": "{{ ucp_version }}", "spec": "https://example.com/ucp/processor-tokenizer.json", "schema": "https://example.com/ucp/processor-tokenizer/schema.json", "available_instruments": [ @@ -151,7 +151,7 @@ The response config includes runtime information about what's available for this ```json { "id": "processor_tokenizer", - "version": "2026-01-11", + "version": "{{ ucp_version }}", "available_instruments": [ { "type": "card", @@ -200,7 +200,7 @@ business's configuration. "com.example.processor_tokenizer": [ { "id": "processor_tokenizer", - "version": "2026-01-11", + "version": "{{ ucp_version }}", "available_instruments": [ {"type": "card", "constraints": {"brands": ["visa", "mastercard", "amex"]}} ], diff --git a/docs/specification/fulfillment.md b/docs/specification/fulfillment.md index c80e1ce8f..b511f1652 100644 --- a/docs/specification/fulfillment.md +++ b/docs/specification/fulfillment.md @@ -60,35 +60,35 @@ method. {{ schema_fields('types/fulfillment_resp', 'fulfillment') }} -#### Fulfillment Method Response +#### Fulfillment Method {{ schema_fields('types/fulfillment_method_resp', 'fulfillment') }} -#### Fulfillment Destination Response +#### Fulfillment Destination {{ schema_fields('types/fulfillment_destination_resp', 'fulfillment') }} -#### Shipping Destination Response +#### Shipping Destination {{ schema_fields('types/shipping_destination_resp', 'fulfillment') }} -#### Retail Location Response +#### Retail Location {{ schema_fields('types/retail_location_resp', 'fulfillment') }} -#### Fulfillment Group Response +#### Fulfillment Group {{ schema_fields('types/fulfillment_group_resp', 'fulfillment') }} -#### Fulfillment Option Response +#### Fulfillment Option {{ schema_fields('types/fulfillment_option_resp', 'fulfillment') }} -#### Fulfillment Available Method Response +#### Fulfillment Available Method {{ schema_fields('types/fulfillment_available_method_resp', 'fulfillment') }} -#### Total Response +#### Total {{ schema_fields('types/total_resp', 'fulfillment') }} @@ -282,10 +282,10 @@ within each method. ```json // Default: single group per method -{ "dev.ucp.shopping.fulfillment": [{"version": "2026-01-11"}] } +{ "dev.ucp.shopping.fulfillment": [{"version": "{{ ucp_version }}"}] } // Opt-in: business MAY return multiple groups per method -{ "dev.ucp.shopping.fulfillment": [{"version": "2026-01-11", "config": { "supports_multi_group": true }}] } +{ "dev.ucp.shopping.fulfillment": [{"version": "{{ ucp_version }}", "config": { "supports_multi_group": true }}] } ``` ### Business Profile @@ -300,7 +300,7 @@ Businesses declare what fulfillment configurations they support using "capabilities": { "dev.ucp.shopping.fulfillment": [ { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "config": { "allows_multi_destination": { "shipping": true diff --git a/docs/specification/identity-linking.md b/docs/specification/identity-linking.md index c78c7ce30..3cf857843 100644 --- a/docs/specification/identity-linking.md +++ b/docs/specification/identity-linking.md @@ -16,7 +16,7 @@ # Identity Linking Capability -* **Capability Name:** `dev.ucp.common.identity_linking` +- **Capability Name:** `dev.ucp.common.identity_linking` ## Overview @@ -24,138 +24,341 @@ The Identity Linking capability enables a **platform** (e.g., Google, an agentic service) to obtain authorization to perform actions on behalf of a user on a **business**'s site. -This linkage is foundational for commerce experiences, such as accessing -loyalty benefits, utilizing personalized offers, managing wishlists, and -executing authenticated checkouts. - -**This specification leverages -[OAuth 2.0](https://datatracker.ietf.org/doc/html/rfc6749){ target="_blank" }** as the mechanism -for securely linking a user's platform account with their business account. - -## General guidelines - -(In addition to the overarching guidelines) - -### For platforms - -* **MUST** authenticate using their `client_id` and `client_secret` - ([RFC 6749 2.3.1](https://datatracker.ietf.org/doc/html/rfc6749#section-2.3.1){target="_blank"}) - through HTTP Basic Authentication - ([RFC 7617](https://datatracker.ietf.org/doc/html/rfc7617){target="_blank"}) - when exchanging codes for tokens. - * **MAY** support Client Metadata - * **MAY** support Dynamic Client Registration mechanisms to supersede - static credential exchange. -* The platform must include the token in the HTTP Authorization header using - the Bearer schema (`Authorization: Bearer `) -* **MUST** implement the OAuth 2.0 Authorization Code flow - ([RFC 6749 4.1](https://datatracker.ietf.org/doc/html/rfc6749#section-4.1){target="_blank"}) - as the primary linking mechanism. -* **SHOULD** include a unique, unguessable state parameter in the - authorization request to prevent Cross-Site Request Forgery (CSRF) - ([RFC 6749 10.12](https://datatracker.ietf.org/doc/html/rfc6749#section-10.12){target="_blank"}) - (part of - [OAuth 2.1 draft](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-14#name-preventing-csrf-attacks){target="_blank"}) - . -* Revocation and security events - * **SHOULD** call the business's revocation endpoint - ([RFC 7009](https://datatracker.ietf.org/doc/html/rfc7009){target="_blank"}) when a user - initiates an unlink action on the platform side. - * **SHOULD** support - [OpenID RISC Profile 1.0](https://openid.net/specs/openid-risc-1_0-final.html) - to handle asynchronous account updates, unlinking events, and - cross-account protection. - -### For businesses - -* **MUST** implement OAuth 2.0 - ([RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749)) -* **MUST** adhere to [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) to - declare the location of their OAuth 2.0 endpoints - (`/.well-known/oauth-authorization-server`) - * **SHOULD** implement - [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728/) (HTTP - Resource Metadata) to allow platforms to discover the Authorization - Server associated with specific resources. - * **SHOULD** fill in `scopes_supported` as part of - [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414). -* **MUST** enforce Client Authentication at the Token Endpoint. -* **MUST** provide an account creation flow if the user does not already have - an account. -* **MUST** support standard UCP scopes, as defined in the Scopes section, - granting the tokens permission to all associated Operations for a given - resource. -* Additional permissions **MAY** be granted beyond those explicitly requested, - provided that the requested scopes are, at minimum, included. -* The platform and business **MAY** define additional custom scopes beyond the - minimum scope requirements. -* Revocation and security events - * **MUST** implement standard Token Revocation as defined in - [RFC 7009](https://datatracker.ietf.org/doc/html/rfc7009). - * **MUST** revoke the specified token and **SHOULD** recursively revoke - all associated tokens (e.g., revoking a `refresh_token` **MUST** also - immediately revoke all active `access_token`s issued from it). - * **MUST** support revocation requests authenticated with the same client - credentials used for the token endpoint. - * **SHOULD** support - [OpenID RISC Profile 1.0](https://openid.net/specs/openid-risc-1_0-final.html) - to enable Cross-Account Protection and securely signal revocation or - account state changes initiated by the business side. - ([See Cross-Account protection](https://developers.google.com/identity/account-linking/unlinking#cross-account_protection_risc)) - -## Scopes - -We'd ask users to authorize the platform to have access to all the scopes that -could be required for UCP, regardless of whether the business supports them. - -### Structure - -The scope complexity should be hidden in the consent screen shown to the user: -they shouldn't see one row for each action, but rather a general one, for -example "Allow \[platform\] to manage checkout sessions". - -### Mapping between resources, actions and capabilities - -Resources | Operation | Scope Action -:-------------- | :------------------------- | :---------------------------- -CheckoutSession | Get | `ucp:scopes:checkout_session` -CheckoutSession | Create | `ucp:scopes:checkout_session` -CheckoutSession | Update | `ucp:scopes:checkout_session` -CheckoutSession | Delete | `ucp:scopes:checkout_session` -CheckoutSession | Cancel | `ucp:scopes:checkout_session` -CheckoutSession | Complete | `ucp:scopes:checkout_session` - -A scope covering a capability must grant access to all operations associated to -the capability. For example, ucp:scopes:checkout\_session must grant all of: -Get, Create, Update, Delete, Cancel, Complete. - -## Examples - -### Authorization server metadata - -Example of [metadata](https://datatracker.ietf.org/doc/html/rfc8414#section-2){target="_blank"} -supposed to be hosted in /.well-known/oauth-authorization-server as per -[RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414){target="_blank"}: +This linkage is foundational for commerce experiences, such as accessing loyalty +benefits, utilizing personalized offers, managing wishlists, and executing +authenticated checkouts. + +**This specification implements a Mechanism Registry pattern**, allowing +platforms and businesses to negotiate the authentication mechanism dynamically. +While +OAuth +2.0 is the primary recommended mechanism, the design natively supports +future extensibility securely. + +## Mechanism Registry Pattern + +The Identity Linking capability configuration acts as a **registry** of +supported authentication mechanisms. Platforms and businesses discover and +negotiate the mechanism exactly like other UCP capabilities. + +### UCP Capability Declaration + +Businesses **MUST** declare the supported mechanisms in the capability `config` +using the `supported_mechanisms` array. Each mechanism must dictate its `type` +using an open string vocabulary (e.g., `oauth2`, `verifiable_credential`) and +provide the necessary resolution endpoints (like `issuer`). + +```json +{ + "dev.ucp.common.identity_linking": [ + { + "version": "2026-03-14", + "config": { + "supported_mechanisms": [ + { + "type": "oauth2", + "issuer": "https://auth.merchant.example.com" + } + ] + } + } + ] +} +``` + +### Mechanism Selection Algorithm + +The `supported_mechanisms` array is **ordered by the business's preference** +(index 0 = highest priority). Platforms **MUST** use the following algorithm to +select a mechanism: + +1. Iterate the `supported_mechanisms` array from index 0 (first element). +2. For each entry, check whether the platform supports the declared `type`. +3. Select the **first** entry whose `type` the platform supports and proceed + with that mechanism. +4. If no entry in the array has a `type` the platform supports, the platform + **MUST** abort the identity linking process. The platform **MUST NOT** + attempt a partial or fallback linking flow. + +If the platform supports multiple `type` values that appear in the array, the +business's ordering takes precedence — the platform **MUST** use whichever +supported type appears first in the array, regardless of the platform's own +internal preference. + +## Capability-Driven Scope Negotiation (Least Privilege) + +To maintain the **Principle of Least Privilege**, authorization scopes are +**NOT** hardcoded within the identity linking capability. + +Instead, **authorization scopes are dynamically derived from the final +intersection of negotiated capabilities**. + +1. **Schema Declaration:** Each individual capability schema explicitly defines + its own required identity scopes (e.g., `dev.ucp.shopping.checkout` declares + `dev.ucp.shopping.scopes.checkout_session`). +2. **Dynamic Derivation:** During UCP Discovery, when the platform computes the + intersection of supported capabilities between itself and the business, it + extracts the required scopes from **only** the successfully negotiated + capabilities. +3. **Authorization:** The platform initiates the connection requesting **exactly** + the derived scopes — the union of `identity_scopes` from all capabilities in + the finalized intersection. If a capability (e.g., `order`) is excluded from + the active capability set, its respective scopes **MUST NOT** be requested by + the platform. If the final derived scope list is completely empty, the platform + **MUST** abort the identity linking process, as there are no secured resources + to authorize. + +### Scope Structure & Mapping + +Consent screens **MUST** present permissions to users in clear, human-readable +language that accurately describes what access is being granted. Rather than +listing each individual operation (Get, Create, Update, Delete, etc.) as a +separate line, consent screens **SHOULD** group them under a single +capability-level description (e.g., "Allow \[platform\] to manage checkout +sessions"). This grouping is for readability — it **MUST NOT** reduce the +transparency of what access the user is authorizing. A scope grants access to +all operations associated with the capability and the consent screen must +accurately reflect that. + +### Scope Naming Convention + +Scopes **MUST** use **reverse DNS dot notation**, consistent with UCP capability +names, to prevent namespace collisions: + +- **UCP-defined scopes:** `dev.ucp..scopes.` (e.g., + `dev.ucp.shopping.scopes.checkout_session`) +- **Third-party scopes:** `.scopes.` (e.g., + `com.example.loyalty.scopes.points_balance`) + +This format strictly adheres to the scope token syntax defined in +[RFC 6749 Section 3.3](https://datatracker.ietf.org/doc/html/rfc6749#section-3.3). + +Example capability-to-scope mapping based on UCP schemas: + +| Resources | Operation | Scope Action | +| :-------------- | :-------------------------------------------- | :----------------------------------------- | +| CheckoutSession | Get, Create, Update, Cancel, Complete | `dev.ucp.shopping.scopes.checkout_session` | + +## Supported Mechanisms + +### OAuth 2.0 (`"type": "oauth2"`) + +When the negotiated mechanism type is `oauth2`, platforms and businesses +**MUST** adhere to the following standard parameters. + +#### Discovery Bridging + +When a platform encounters `"type": "oauth2"`, it **MUST** parse the capability +configuration and securely locate the Authorization Server metadata. + +Platforms **MUST** implement the following resolution hierarchy to determine the +discovery URL: + +1. **Explicit Endpoint (Highest Priority)**: If the capability configuration + provides a `discovery_endpoint` string, the platform **MUST** fetch metadata + directly from that exact URI. If this fetch fails (e.g., non-2xx HTTP response + or connection timeout), the platform **MUST** abort the discovery process and + **MUST NOT** fall back to any other endpoints. +2. **RFC 8414 Standard Discovery**: If no explicit endpoint is provided, the + platform **MUST** append `/.well-known/oauth-authorization-server` to the + defined `issuer` string and fetch. If this fetch returns any non-2xx response + other than `404 Not Found` (e.g., `500 Internal Server Error`, `503 Service + Unavailable`), or if a connection timeout or network error occurs, the + platform **MUST** abort the discovery process and **MUST NOT** proceed to the + OIDC fallback. +3. **OIDC Fallback (Lowest Priority)**: If and only if the RFC 8414 fetch + returns exactly `404 Not Found`, the platform **MUST** append + `/.well-known/openid-configuration` to the defined `issuer` string and fetch. + If this final fetch returns any non-2xx response or a network error, the + platform **MUST** abort the identity linking process. + +**Issuer Validation**: Regardless of the discovery method used above, the +platform **MUST** perform an exact string comparison between the `issuer` value +returned in the metadata and the `issuer` string defined in the capability +configuration, as required by +[RFC 8414 Section 3.3](https://datatracker.ietf.org/doc/html/rfc8414#section-3.3). +No normalization (e.g., trailing slash stripping) is permitted — the comparison +**MUST** be an exact string comparison. + +Businesses **MUST** ensure the `issuer` string declared in their UCP capability +configuration exactly matches both the `issuer` field in their authorization +server metadata and the `iss` claim in any issued JWT access tokens. This +guarantees that standard JWT validation libraries, which perform exact string +equality on `iss`, will succeed without modification. + +Failure to validate the issuer exposes the integration to Mix-Up Attacks and +**MUST** result in an aborted linking process. + +Example metadata retrieved via RFC 8414: + +```json +{ + "issuer": "https://auth.merchant.example.com", + "authorization_endpoint": "https://auth.merchant.example.com/oauth2/authorize", + "token_endpoint": "https://auth.merchant.example.com/oauth2/token", + "revocation_endpoint": "https://auth.merchant.example.com/oauth2/revoke", + "scopes_supported": [ + "dev.ucp.shopping.scopes.checkout_session" + ], + "response_types_supported": [ + "code" + ], + "grant_types_supported": [ + "authorization_code", + "refresh_token" + ] +} +``` + +#### For platforms + +- **MUST** authenticate using their `client_id` and `client_secret` + (RFC + 6749 2.3.1) through HTTP Basic Authentication + (RFC + 7617) when exchanging codes for tokens. + - **MAY** support Client Metadata + - **MAY** support Dynamic Client Registration mechanisms to supersede static + credential exchange. +- The platform must include the token in the HTTP Authorization header using the + Bearer schema (`Authorization: Bearer `) +- **MUST** implement the OAuth 2.0 Authorization Code flow + (RFC + 6749 4.1) as the primary linking mechanism. +- **MUST** strictly implement Proof Key for Code Exchange (PKCE) + ([RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636)) using the `S256` + challenge method to prevent authorization code interception attacks. +- **MUST** securely validate the `iss` parameter returned in the authorization + response ([RFC 9207](https://www.rfc-editor.org/rfc/rfc9207.html)) to prevent + Mix-Up Attacks. +- **SHOULD** include a unique, unguessable state parameter in the authorization + request to prevent Cross-Site Request Forgery (CSRF) + (RFC + 6749 10.12). +- Revocation and security events + - **SHOULD** call the business's revocation endpoint + (RFC + 7009) when a user initiates an unlink action on the platform side. + - **SHOULD** support + [OpenID RISC Profile 1.0](https://openid.net/specs/openid-risc-1_0-final.html) + to handle asynchronous account updates, unlinking events, and + cross-account protection. + +#### For businesses + +- **MUST** implement OAuth 2.0 + ([RFC 6749](https://datatracker.ietf.org/doc/html/rfc6749)) +- **MUST** adhere to [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) + to declare the location of their OAuth 2.0 endpoints + (`/.well-known/oauth-authorization-server`) +- **MUST** populate `scopes_supported` in their RFC 8414 metadata to allow + platforms to detect scope mismatches early, before initiating the authorization + flow. +- **MUST** enforce Client Authentication at the Token Endpoint. +- **MUST** enforce exact string matching for the `redirect_uri` parameter during + the authorization request to prevent open redirects and token theft. +- **MUST** enforce Proof Key for Code Exchange (PKCE) + ([RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636)) validation at the + Token Endpoint for all authorization code exchanges. +- **MUST** return the `iss` parameter in the authorization response + ([RFC 9207](https://www.rfc-editor.org/rfc/rfc9207.html)) matching the + established issuer string. +- **MUST** provide an account creation flow if the user does not already have an + account. +- **MUST** support dynamically requested UCP scopes mapped strictly to the + capabilities actively negotiated in the session. +- Revocation and security events + - **MUST** implement standard Token Revocation as defined in + [RFC 7009](https://datatracker.ietf.org/doc/html/rfc7009). + - **MUST** revoke the specified token and **SHOULD** recursively revoke all + associated tokens. + - **SHOULD** support + [OpenID RISC Profile 1.0](https://openid.net/specs/openid-risc-1_0-final.html) + to enable Cross-Account Protection. + +## End-to-End Workflow & Example + +### Scenario: An AI Shopping Agent (Platform) and a Shopping Merchant (Business) + +#### 1. The Merchant's Profile (`/.well-known/ucp`) + +The Merchant supports checkout, order management, and secure identity features. + +```json +{ + "dev.ucp.shopping.checkout": [{ "version": "2026-03-14", "config": {} }], + "dev.ucp.shopping.order": [{ "version": "2026-03-14", "config": {} }], + "dev.ucp.common.identity_linking": [{ + "version": "2026-03-14", + "config": { + "supported_mechanisms": [{ + "type": "oauth2", + "issuer": "https://auth.merchant.example.com" + }] + } + }] +} +``` + +#### 2. The AI Agent's Profile + +The AI Shopping Agent only knows how to perform checkouts. It does NOT yet know how to manage existing orders. ```json { - "issuer": "https://merchant.example.com", - "authorization_endpoint": "https://merchant.example.com/oauth2/authorize", - "token_endpoint": "https://merchant.example.com/oauth2/token", - "revocation_endpoint": "https://merchant.example.com/oauth2/revoke", - "scopes_supported": [ - "ucp:scopes:checkout_session", - ], - "response_types_supported": [ - "code" - ], - "grant_types_supported": [ - "authorization_code", - "refresh_token" - ], - "token_endpoint_auth_methods_supported": [ - "client_secret_basic" - ], - "service_documentation": "https://merchant.example.com/docs/oauth2" + "dev.ucp.shopping.checkout": [{ "version": "2026-03-14" }], + "dev.ucp.common.identity_linking": [{ "version": "2026-03-14" }] } ``` + +#### 3. Execution Steps + +1. **Capability Discovery & Intersection**: The AI Agent intersects its own profile + with the business's and successfully negotiates `dev.ucp.shopping.checkout` + and `dev.ucp.common.identity_linking`. `dev.ucp.shopping.order` is strictly + excluded because the agent does not support it. +2. **Schema Fetch & Dynamic Scope Derivation**: The agent fetches the JSON Schema + definitions for the **Active Capability List** (`checkout.json` and + `identity_linking.json`). The agent parses the schema logic for + `dev.ucp.shopping.checkout`, looking for the top-level `"identity_scopes"` + annotation, and statically derives that the required scope is strictly + `dev.ucp.shopping.scopes.checkout_session`. `dev.ucp.shopping.scopes.order_management` + is inherently omitted. +3. **Identity Mechanism Selection & Execution**: The agent applies the + Mechanism Selection Algorithm to the business's `supported_mechanisms` array. + The first (and only) entry has `type: oauth2`, which the agent supports, so + it is selected. The agent executes standard OAuth discovery (appending + `/.well-known/oauth-authorization-server` to the issuer string) and validates + that the returned `issuer` is an exact string match to the configured value. +4. **User Consent & Authorization**: The agent generates a consent URL to prompt + the user (or invokes the authorization flow directly in the GUI), using the + dynamically derived scopes. + + ```http + GET https://auth.merchant.example.com/oauth2/authorize + ?response_type=code + &client_id=shopping_agent_client_123 + &redirect_uri=https://shoppingagent.com/callback + &scope=dev.ucp.shopping.scopes.checkout_session + &state=xyz123 + &code_challenge=code_challenge_123 + &code_challenge_method=S256 + ``` + + The business will respond with the authorization code and the `iss` + parameter per RFC 9207: + + ```http + HTTP/1.1 302 Found + Location: https://shoppingagent.com/callback + ?code=code123 + &state=xyz123 + &iss=https://auth.merchant.example.com + ``` + + *The user is prompted to consent **only** to "Manage Checkout Sessions".* + +5. **Authorized UCP Execution**: The platform securely exchanges the + authorization code for an `access_token` bound only to checkout and + successfully utilizes the UCP REST APIs via + `Authorization: Bearer `. diff --git a/docs/specification/order.md b/docs/specification/order.md index 66472d96b..9830dfd4d 100644 --- a/docs/specification/order.md +++ b/docs/specification/order.md @@ -159,9 +159,9 @@ Examples: `refund`, `return`, `credit`, `price_adjustment`, `dispute`, ```json { "ucp": { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "capabilities": { - "dev.ucp.shopping.order": [{"version": "2026-01-11"}] + "dev.ucp.shopping.order": [{"version": "{{ ucp_version }}"}] } }, "id": "order_abc123", @@ -281,7 +281,7 @@ platform's profile and uses it to send order lifecycle events. { "dev.ucp.shopping.order": [ { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "config": { "webhook_url": "https://platform.example.com/webhooks/ucp/orders" } @@ -379,7 +379,7 @@ zero-downtime key rotation procedures. ## Entities -### Item Response +### Item {{ schema_fields('types/item_resp', 'order') }} @@ -391,10 +391,10 @@ zero-downtime key rotation procedures. {{ extension_schema_fields('capability.json#/$defs/response_schema', 'order') }} -### Total Response +### Total {{ schema_fields('types/total_resp', 'order') }} -### UCP Response Order +### UCP Response Order Schema {: #ucp-response-order-schema } {{ extension_schema_fields('ucp.json#/$defs/response_order_schema', 'order') }} diff --git a/docs/specification/overview.md b/docs/specification/overview.md index 08cebef98..14aea05c7 100644 --- a/docs/specification/overview.md +++ b/docs/specification/overview.md @@ -33,11 +33,21 @@ Schema notes: ## Discovery, Governance, and Negotiation -UCP employs a server-selects architecture where the business (server) chooses -the protocol version and capabilities from the intersection of both parties' -capabilities. Both business and platform profiles can be cached by both parties, -allowing efficient capability negotiation within the normal request/response -flow between platform and business. +UCP separates protocol version compatibility from capability negotiation. +The business's profile at `/.well-known/ucp` describes capabilities for +the protocol version it declares. Businesses that support older protocol +versions **SHOULD** publish version-specific profiles and advertise them +via the `supported_versions` field — a map from protocol version to +profile URI, enabling platforms to discover the exact capabilities for a +specific protocol version. Version lifecycle, including when to deprecate +or remove older versions from `supported_versions`, is a business policy +decision. The protocol does not prescribe a deprecation schedule. +Capability negotiation follows a server-selects architecture where the +business (server) determines the active capabilities from the +intersection of both parties' declared capabilities. Both business and +platform profiles can be cached by both parties, allowing efficient +capability negotiation within the normal request/response flow between +platform and business. ### Namespace Governance @@ -132,9 +142,9 @@ appended to this endpoint to form the complete URL. ```json { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "transport": "rest", - "schema": "https://ucp.dev/services/shopping/openapi.json", + "schema": "https://ucp.dev/{{ ucp_version }}/services/shopping/rest.openapi.json", "endpoint": "https://business.example.com/api/v2" } ``` @@ -171,9 +181,9 @@ Extensions use the `extends` field to declare their parent(s): { "dev.ucp.shopping.fulfillment": [ { - "version": "2026-01-23", - "spec": "https://ucp.dev/2026-01-23/specification/fulfillment", - "schema": "https://ucp.dev/2026-01-23/schemas/shopping/fulfillment.json", + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/fulfillment", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/fulfillment.json", "extends": "dev.ucp.shopping.checkout" } ] @@ -188,9 +198,9 @@ Extensions **MAY** extend multiple parent capabilities by using an array: { "dev.ucp.shopping.discount": [ { - "version": "2026-01-23", - "spec": "https://ucp.dev/2026-01-23/specification/discount", - "schema": "https://ucp.dev/2026-01-23/schemas/shopping/discount.json", + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/discount", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/discount.json", "extends": ["dev.ucp.shopping.checkout", "dev.ucp.shopping.cart"] } ] @@ -264,6 +274,56 @@ This convention ensures: - **Verifiable**: Build-time checks can confirm each `extends` entry has a matching `$defs` key +##### Version Requirements + +Extension schemas **SHOULD** declare a `requires` object (alongside +`name`, `title`, `description`) to indicate the protocol and +capability versions required for correct operation: + +```json +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://acme.com/ucp/schemas/loyalty.json", + "name": "com.acme.shopping.loyalty", + "title": "Acme Loyalty Points", + "requires": { + "protocol": { "min": "2026-01-23" }, + "capabilities": { + "dev.ucp.shopping.checkout": { "min": "2026-06-01" } + } + }, + "$defs": { + "dev.ucp.shopping.checkout": { ... } + } +} +``` + +The schema author — not the profile publisher — declares version +requirements. The profile publisher selects and advertises compatible +versions in their profile. + +Each constraint is an object with a required `min` (inclusive) and +optional `max` (inclusive) version. When `max` is absent, there is +no upper bound: + +```json +"requires": { + "protocol": { "min": "2026-01-23", "max": "2026-09-01" }, + "capabilities": { + "dev.ucp.shopping.checkout": { "min": "2026-06-01" } + } +} +``` + +Keys in `requires.capabilities` **MUST** be a subset of the +extension's `$defs` keys. If `requires` is present, platforms and +businesses **MUST** verify the negotiated protocol version and +capability versions satisfy the declared constraints during schema +resolution. Incompatible extensions are excluded from the active +capability set (see [Resolution Flow](#resolution-flow)). If +`requires` is absent, the extension is assumed to be compatible +with the versions declared by the profile. + #### Schema Resolution Convention To validate payloads, implementations resolve extension schemas as follows: @@ -286,8 +346,13 @@ Platforms **MUST** resolve schemas following this sequence: 2. **Negotiation**: Compute capability intersection (see [Intersection Algorithm](#intersection-algorithm)) 3. **Schema Fetch**: Fetch base schema and all active extension schemas -4. **Compose**: Merge schemas via `allOf` chains based on active extensions -5. **Validate**: Validate requests and responses against the composed schema +4. **Version Compatibility**: For each fetched extension schema, + if `requires` is present, verify the negotiated protocol version + and capability versions satisfy the declared constraints. Exclude + incompatible extensions and re-prune orphaned extensions + (steps 3-4 of the [Intersection Algorithm](#intersection-algorithm)) +5. **Compose**: Merge schemas via `allOf` chains based on active extensions +6. **Validate**: Validate requests and responses against the composed schema ### Profile Structure @@ -298,67 +363,82 @@ Businesses publish their profile at `/.well-known/ucp`. An example: ```json { "ucp": { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "services": { "dev.ucp.shopping": [ { - "version": "2026-01-11", - "spec": "https://ucp.dev/specification/overview", + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/overview", "transport": "rest", "endpoint": "https://business.example.com/ucp/v1", - "schema": "https://ucp.dev/services/shopping/openapi.json" + "schema": "https://ucp.dev/{{ ucp_version }}/services/shopping/rest.openapi.json" }, { - "version": "2026-01-11", - "spec": "https://ucp.dev/specification/overview", + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/overview", "transport": "mcp", "endpoint": "https://business.example.com/ucp/mcp", - "schema": "https://ucp.dev/services/shopping/mcp.openrpc.json" + "schema": "https://ucp.dev/{{ ucp_version }}/services/shopping/mcp.openrpc.json" }, { - "version": "2026-01-11", - "spec": "https://ucp.dev/specification/overview", + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/overview", "transport": "a2a", "endpoint": "https://business.example.com/.well-known/agent-card.json" }, { - "version": "2026-01-11", - "spec": "https://ucp.dev/specification/overview", + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/overview", "transport": "embedded", - "schema": "https://ucp.dev/services/shopping/embedded.openrpc.json" + "schema": "https://ucp.dev/{{ ucp_version }}/services/shopping/embedded.openrpc.json" } ] }, "capabilities": { "dev.ucp.shopping.checkout": [ { - "version": "2026-01-11", - "spec": "https://ucp.dev/specification/checkout", - "schema": "https://ucp.dev/schemas/shopping/checkout.json" + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/checkout", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/checkout.json" } ], "dev.ucp.shopping.fulfillment": [ { - "version": "2026-01-11", - "spec": "https://ucp.dev/specification/fulfillment", - "schema": "https://ucp.dev/schemas/shopping/fulfillment.json", + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/fulfillment", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/fulfillment.json", "extends": "dev.ucp.shopping.checkout" } ], "dev.ucp.shopping.discount": [ { - "version": "2026-01-11", - "spec": "https://ucp.dev/specification/discount", - "schema": "https://ucp.dev/schemas/shopping/discount.json", + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/discount", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/discount.json", "extends": "dev.ucp.shopping.checkout" } + ], + "dev.ucp.common.identity_linking": [ + { + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/identity-linking", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/common/identity_linking.json", + "config": { + "supported_mechanisms": [ + { + "type": "oauth2", + "issuer": "https://auth.merchant.example.com" + } + ] + } + } ] }, "payment_handlers": { "com.example.processor_tokenizer": [ { "id": "processor_tokenizer", - "version": "2026-01-11", + "version": "{{ ucp_version }}", "spec": "https://example.com/specs/payments/processor_tokenizer", "schema": "https://example.com/specs/payments/merchant_tokenizer.json", "available_instruments": [ @@ -402,6 +482,11 @@ used to verify signatures on webhooks and other authenticated messages from the business. See [Key Discovery](#key-discovery) for key lookup and resolution, and [Message Signatures](signatures.md) for signing mechanics. +Businesses that support older protocol versions **SHOULD** include a +`supported_versions` object mapping each older version to a +version-specific profile URI. See [Protocol Version](#protocol-version) +for details. + #### Platform Profile Platform profiles are similar and include signing keys for capabilities @@ -412,42 +497,49 @@ example: ```json { "ucp": { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "services": { "dev.ucp.shopping": [ { - "version": "2026-01-11", - "spec": "https://ucp.dev/specification/overview", + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/overview", "transport": "rest", - "schema": "https://ucp.dev/services/shopping/openapi.json" + "schema": "https://ucp.dev/{{ ucp_version }}/services/shopping/rest.openapi.json" } ] }, "capabilities": { "dev.ucp.shopping.checkout": [ { - "version": "2026-01-11", - "spec": "https://ucp.dev/specification/checkout", - "schema": "https://ucp.dev/schemas/shopping/checkout.json" + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/checkout", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/checkout.json" } ], "dev.ucp.shopping.fulfillment": [ { - "version": "2026-01-11", - "spec": "https://ucp.dev/specification/fulfillment", - "schema": "https://ucp.dev/schemas/shopping/fulfillment.json", + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/fulfillment", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/fulfillment.json", "extends": "dev.ucp.shopping.checkout" } ], "dev.ucp.shopping.order": [ { - "version": "2026-01-11", - "spec": "https://ucp.dev/specification/order", - "schema": "https://ucp.dev/schemas/shopping/order.json", + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/order", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/order.json", "config": { "webhook_url": "https://platform.example.com/webhooks/ucp/orders" } } + ], + "dev.ucp.common.identity_linking": [ + { + "version": "{{ ucp_version }}", + "spec": "https://ucp.dev/{{ ucp_version }}/specification/identity-linking", + "schema": "https://ucp.dev/{{ ucp_version }}/schemas/common/identity_linking.json" + } ] }, "payment_handlers": { @@ -462,7 +554,7 @@ example: "dev.shopify.shop_pay": [ { "id": "shop_pay_1234", - "version": "2026-01-11", + "version": "{{ ucp_version }}", "spec": "https://shopify.dev/ucp/shop-pay-handler", "schema": "https://shopify.dev/ucp/schemas/shop-pay-config.json", "available_instruments": [ @@ -473,9 +565,9 @@ example: "dev.ucp.processor_tokenizer": [ { "id": "processor_tokenizer", - "version": "2026-01-11", + "version": "{{ ucp_version }}", "spec": "https://example.com/specs/payments/processor_tokenizer-payment", - "schema": "https://ucp.dev/schemas/payments/delegate-payment.json", + "schema": "https://example.com/schemas/payments/delegate-payment.json", "available_instruments": [ {"type": "card", "constraints": {"brands": ["visa", "mastercard"]}} ] @@ -574,17 +666,34 @@ for a session: 1. **Compute intersection**: For each business capability, include it in the result if a platform capability with the same `name` exists. -2. **Prune orphaned extensions**: Remove any capability where `extends` is - set but **none** of its parent capabilities are in the intersection. - - For single-parent extensions (`extends: "string"`): parent must be present - - For multi-parent extensions (`extends: ["a", "b"]`): at least one parent - must be present - -3. **Repeat pruning**: Continue step 2 until no more capabilities are removed - (handles transitive extension chains). - -The result is the set of capabilities both parties support, with extension -dependencies satisfied. +2. **Select version**: For each capability in the intersection, compute the + set of version strings present in **both** the business and platform + arrays. If the set is non-empty, select the **highest** version + (latest date). If the set is empty (no mutual version), **exclude** the + capability from the intersection. + +3. **Prune orphaned extensions & unauthorized capabilities**: Remove any capability that lacks its required structural or functional dependencies: + - **Structural Dependencies**: Remove any capability where `extends` is + set but **none** of its parent capabilities are in the intersection. + - For single-parent extensions (`extends: "string"`): parent must be present + - For multi-parent extensions (`extends: ["a", "b"]`): at least one parent + must be present + - **Scope Dependencies**: Remove any capability declaring `identity_scopes` + if `dev.ucp.common.identity_linking` is not present in the intersection. + +4. **Repeat pruning**: Continue step 3 until no more capabilities are removed + (handles transitive extension chains and chained scope dependencies). + +5. **Derive Scopes (Final Pass)**: If `dev.ucp.common.identity_linking` is + present in the negotiated capabilities, the authorization scope set + **MUST ONLY** be derived from the finalized intersection list *after* all + pruning loops have stabilized. Capabilities excluded during pruning MUST NOT + contribute to the derived authorization scopes. If the final derived scope + list is mathematically empty (no active capabilities request scopes), the + agent **SHOULD** abort the identity linking process. + +The result is the set of capabilities both parties support at mutually +compatible versions, with extension dependencies satisfied. #### Error Handling @@ -596,10 +705,13 @@ UCP negotiation can fail in two ways: 2. **Negotiation failure**: The provided profile is valid but capability intersection is empty or versions are incompatible. -These failure types require different handling: +Discovery failures are transport errors — the required inputs could +not be retrieved or were malformed. Negotiation failures are business +outcomes — the handler executed on the provided inputs and reported +the result in the UCP response: -- **Discovery failure** → transport error with optional `continue_url` -- **Negotiation failure** → UCP response with optional `continue_url` +- **Discovery or version failure** → transport error with optional `continue_url` +- **Capability negotiation failure** → UCP response with optional `continue_url` ##### Error Codes @@ -610,8 +722,8 @@ These failure types require different handling: | `invalid_profile_url` | Profile URL is malformed, missing, or unresolvable | 400 | -32001 | | `profile_unreachable` | Resolved URL but fetch failed (timeout, non-2xx) | 424 | -32001 | | `profile_malformed` | Fetched content is not valid JSON or violates schema | 422 | -32001 | +| `version_unsupported` | Platform's protocol version not supported | 422 | -32001 | | `capabilities_incompatible` | No compatible capabilities in intersection | 200 | result | -| `version_unsupported` | Platform's UCP version is not supported | 200 | result | **Signature Errors:** @@ -670,7 +782,20 @@ task through the standard web interface. } ``` - **Negotiation Failure (200):** + **Version Unsupported (422):** + + ```http + HTTP/1.1 422 Unprocessable Content + Content-Type: application/json + + { + "code": "version_unsupported", + "content": "Protocol version 2026-01-12 is not supported. This business supports versions 2026-01-11 and 2026-01-23.", + "continue_url": "https://merchant.com/cart" + } + ``` + + **Capabilities Incompatible (200):** ```http HTTP/1.1 200 OK @@ -678,7 +803,8 @@ task through the standard web interface. { "ucp": { - "version": "2026-01-11", + "version": "{{ ucp_version }}", + "status": "error", "capabilities": {} }, "messages": [ @@ -686,7 +812,7 @@ task through the standard web interface. "type": "error", "code": "version_unsupported", "content": "UCP version 2024-01-01 is not supported", - "severity": "requires_buyer_input" + "severity": "unrecoverable" } ], "continue_url": "https://merchant.com" @@ -730,7 +856,25 @@ task through the standard web interface. } ``` - **Negotiation Failure (JSON-RPC result):** + **Version Unsupported (JSON-RPC error):** + + ```json + { + "jsonrpc": "2.0", + "id": 1, + "error": { + "code": -32001, + "message": "Protocol version not supported", + "data": { + "code": "version_unsupported", + "content": "Protocol version 2026-01-12 is not supported. This business supports versions 2026-01-11 and 2026-01-23.", + "continue_url": "https://merchant.com/cart" + } + } + } + ``` + + **Capabilities Incompatible (JSON-RPC result):** ```json { @@ -739,15 +883,15 @@ task through the standard web interface. "result": { "structuredContent": { "ucp": { - "version": "2026-01-11", - "capabilities": {} + "version": "{{ ucp_version }}", + "status": "error" }, "messages": [ { "type": "error", "code": "version_unsupported", "content": "UCP version 2024-01-01 is not supported", - "severity": "requires_buyer_input" + "severity": "unrecoverable" } ], "continue_url": "https://merchant.com" @@ -800,18 +944,18 @@ The `capabilities` registry in responses indicates active capabilities: ```json { "ucp": { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "capabilities": { "dev.ucp.shopping.checkout": [ - {"version": "2026-01-11"} + {"version": "{{ ucp_version }}"} ], "dev.ucp.shopping.fulfillment": [ - {"version": "2026-01-11"} + {"version": "{{ ucp_version }}"} ] }, "payment_handlers": { "com.example.processor_tokenizer": [ - {"id": "processor_tokenizer", "version": "2026-01-11", "available_instruments": [{"type": "card"}]} + {"id": "processor_tokenizer", "version": "{{ ucp_version }}", "available_instruments": [{"type": "card"}]} ] } }, @@ -1081,26 +1225,6 @@ and cart context, then returns the resolved result. Platforms **MUST** treat the the [Payment Handler Guide](payment-handler-guide.md#resolving-available_instruments) for the full resolution semantics. -### Risk Signals - -To aid in fraud assessment, the Platform **MAY** include additional risk signals -in the `complete` call, providing the Business with more context about the -transaction's legitimacy. The structure and content of these risk signals are -not strictly defined by this specification, allowing flexibility based on the -agreement between the Platform and Business or specific payment handler -requirements. - -**Example (Flexible Structure):** - -```json -{ - "risk_signals": { - "session_id": "abc_123_xyz", - "score": 0.95, - } -} -``` - ### Implementation Scenarios The following scenarios illustrate how different payment handlers and @@ -1117,12 +1241,12 @@ an encrypted payment token. ```json { "ucp": { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "payment_handlers": { "com.google.pay": [ { "id": "8c9202bd-63cc-4241-8d24-d57ce69ea31c", - "version": "2026-01-11", + "version": "{{ ucp_version }}", "config": { "api_version": 2, "api_version_minor": 0, @@ -1154,7 +1278,7 @@ an encrypted payment token. "dev.shopify.shop_pay": [ { "id": "shop_pay_1234", - "version": "2026-01-11", + "version": "{{ ucp_version }}", "available_instruments": [ {"type": "shop_pay"} ], @@ -1210,8 +1334,9 @@ POST /checkout-sessions/{id}/complete } ] }, - "risk_signals": { - // ... + "signals": { + "dev.ucp.buyer_ip": "203.0.113.42", + "dev.ucp.user_agent": "Mozilla/5.0 ..." } } ``` @@ -1232,7 +1357,7 @@ request a challenge. "com.example.tokenizer": [ { "id": "merchant_tokenizer", - "version": "2026-01-11", + "version": "{{ ucp_version }}", "spec": "https://example.com/specs/tokenizer", "schema": "https://example.com/schemas/tokenizer.json", "available_instruments": [ @@ -1275,8 +1400,9 @@ POST /checkout-sessions/{id}/complete } ] }, - "risk_signals": { - // ... host could send risk_signals here + "signals": { + "dev.ucp.buyer_ip": "203.0.113.42", + "dev.ucp.user_agent": "Mozilla/5.0 ..." } } ``` @@ -1317,9 +1443,9 @@ session token, the agent generates cryptographic mandates. "dev.ucp.ap2_mandate_compatible_handlers": [ { "id": "ap2_234352", - "version": "2026-01-11", - "spec": "https://ucp.dev/specs/ap2-handler", - "schema": "https://ucp.dev/schemas/ap2-handler.json", + "version": "{{ ucp_version }}", + "spec": "https://example.com/specs/ap2-handler", + "schema": "https://example.com/schemas/ap2-handler.json", "available_instruments": [ {"type": "ap2_mandate"} ] @@ -1353,9 +1479,9 @@ POST /checkout-sessions/{id}/complete } ] }, - "risk_signals": { - "session_id": "abc_123_xyz", - "score": 0.95 + "signals": { + "dev.ucp.buyer_ip": "203.0.113.42", + "com.example.risk_score": 0.95 }, "ap2": { "checkout_mandate": "eyJhbGciOiJ...", // Signed proof of checkout terms @@ -1437,19 +1563,17 @@ certified and handle: ### Fraud Prevention Integration -While UCP does not define fraud prevention APIs, the payment architecture -supports fraud signal integration: +UCP supports fraud prevention through [Signals](#signals) and the +payment architecture: +- Platforms provide transaction environment [signals](#signals) (IP, user + agent) on catalog, cart, and checkout requests - Businesses can require additional fields in handler configurations (e.g., 3DS requirements) -- Platforms can submit device fingerprints and session data alongside credentials - Payment credential providers can perform risk assessment during credential acquisition - Businesses can reject high-risk transactions and request additional - verification - -Future extensions **MAY** standardize fraud signal schemas, but the current -architecture allows flexible integration with existing fraud prevention systems. + verification via signal feedback ### Payment Architecture Extensions @@ -1526,7 +1650,7 @@ MCP servers: "result": { "structuredContent": { "checkout": { - "ucp": {"version": "2026-01-11", "capabilities": {...}}, + "ucp": {"version": "{{ ucp_version }}", "capabilities": {...}}, "id": "checkout_abc123", "status": "incomplete", ... @@ -1556,11 +1680,12 @@ Initiation comes through a `continue_url` that is returned by the business. UCP defines a set of standard capabilities: -| Capability Name | ID (URI) | Description | -| :------------------- | :--------------------------------------------- | :----------------------------------------------------------------------------------------------------------- | -| **Checkout** | `{{ ucp_url }}/schemas/shopping/checkout.json` | Facilitates the creation and management of checkout sessions, including cart management and tax calculation. | -| **Identity Linking** | - | Enables platforms to obtain authorization via OAuth 2.0 to perform actions on a user's behalf. | -| **Order** | `{{ ucp_url }}/schemas/shopping/order.json` | Allows businesses to push asynchronous updates about an order's lifecycle (shipping, delivery, returns). | +| Capability Name | ID (URI) | Description | +| :------------------- | :-------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------- | +| **Cart**. | [schemas/shopping/cart.json](site:schemas/shopping/cart.json) | Enables basket building before purchase intent is established. | +| **Checkout** | [schemas/shopping/checkout.json](site:schemas/shopping/checkout.json) | Facilitates the creation and management of checkout sessions, including cart management and tax calculation. | +| **Identity Linking** | - | Enables platforms to obtain authorization via OAuth 2.0 to perform actions on a user's behalf. | +| **Order** | [schemas/shopping/order.json](site:schemas/shopping/order.json) | Allows businesses to push asynchronous updates about an order's lifecycle (shipping, delivery, returns). | ### Definition & Extensions @@ -1580,6 +1705,65 @@ Sensitive data (such as Payment Credentials or PII) **MUST** be handled according to PCI-DSS and GDPR guidelines. UCP encourages the use of tokenized payment data to minimize business and platform liability. +### Signals + +Businesses require environment data for authorization, rate +limiting, and abuse prevention. Signal values **MUST NOT** be buyer-asserted +claims — platforms provide signals based on direct observation (e.g., +connection IP, user agent) or by relaying independently verifiable +third-party attestations, such as cryptographically signed results from an +external verifier that the business can validate against the provider's +published key set. + +All signal keys **MUST** use reverse-domain naming to ensure provenance and +prevent collisions when multiple extensions contribute to the shared namespace. +Well-known signals use the `dev.ucp` namespace (e.g., `dev.ucp.buyer_ip`); +extension signals use their own namespace (e.g., `com.example.device_id`). + +```json +{ + "signals": { + "dev.ucp.buyer_ip": "203.0.113.42", + "dev.ucp.user_agent": "Mozilla/5.0 ...", + "com.example.attestation": { + "provider_jwks": "https://example.com/.well-known/jwks.json", + "kid": "example-key-2026-01", + "payload": { "id": "att-7c3e9f", "pass": true, "...": "..." }, + "sig": "base64url..." + } + } +} +``` + +Signal fields may contain personally identifiable information +(PII). Platforms **SHOULD** include only signals relevant to the current +transaction. Businesses **SHOULD NOT** persist signal data beyond the +operational needs of the transaction (e.g., order finalization, fraud review). + +Businesses **MAY** use messages with code `signal` to request additional +data. The `path` field identifies the requested signal; the message `type` +determines enforcement. An `error` blocks status progression until the +signal is provided; an `info` is advisory and non-blocking. + +```json +{ + "messages": [ + { + "type": "error", + "code": "signal", + "path": "$.signals['dev.ucp.buyer_ip']", + "content": "Buyer IP is required to proceed." + }, + { + "type": "info", + "code": "signal", + "path": "$.signals['dev.ucp.user_agent']", + "content": "Providing user agent may improve checkout outcomes." + } + ] +} +``` + ### Transaction Integrity and Non-Repudiation For scenarios requiring cryptographic proof of authorization (e.g., autonomous @@ -1617,7 +1801,7 @@ Both businesses and platforms declare a single version in their profiles: ```json { "ucp": { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "services": { ... }, "capabilities": { ... }, "payment_handlers": { ... } @@ -1630,7 +1814,7 @@ Both businesses and platforms declare a single version in their profiles: ```json { "ucp": { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "services": { ... }, "capabilities": { ... }, "payment_handlers": { ... } @@ -1642,23 +1826,85 @@ Both businesses and platforms declare a single version in their profiles: ![High-level resolution flow sequence diagram](site:specification/images/ucp-discovery-negotiation.png) -Businesses **MUST** validate the platform's version and determine compatibility: +Version compatibility operates at two levels: the **protocol version** +and **capability versions**. The protocol version (`ucp.version`) +governs core protocol mechanisms — discovery, negotiation flow, +transport bindings, and signature requirements. Capability versions +govern the semantics of each feature independently, as defined in +[Independent Component Versioning](#independent-component-versioning). + +#### Protocol Version + +The `version` field declares the business's current protocol version. +The profile at `/.well-known/ucp` describes the capabilities, services, +and payment handlers available at that version. + +Businesses that support older protocol versions **SHOULD** declare a +`supported_versions` object mapping each older version to a profile +URI. Each URI points to a complete, self-contained profile for that +version — including its own capabilities, services, payment handlers, +and signing keys. When `supported_versions` is omitted, only +`version` is supported. + +```json +{ + "ucp": { + "version": "2026-01-23", + "supported_versions": { + "2026-01-11": "https://business.example.com/.well-known/ucp/2026-01-11" + } + } +} +``` + +##### Initial Service and Capability Discovery + +Platforms discover a business's capabilities through the following flow: + +1. Platform fetches `/.well-known/ucp` — this is the current version + profile. +2. If the platform's protocol version matches `version`: use this + profile directly. Proceed to capability negotiation. +3. If the platform's protocol version is a key in + `supported_versions`: fetch the profile at the mapped URI. This + profile describes the capabilities available at that protocol + version. Proceed to capability negotiation. +4. Otherwise: the business does not support the platform's protocol + version. Platforms **SHOULD NOT** send requests with an incompatible + version; businesses **MUST** respond with a `version_unsupported` + error. + +Version-specific profiles are leaf documents — they describe exactly +one protocol version and **MUST NOT** contain a `supported_versions` +field. + +##### Request-Time Validation -1. Platform declares version via profile referenced in request +Businesses **MUST** validate the platform's protocol version on +every request: + +1. Platform declares the protocol version it uses via the + `version` field in the profile referenced in the request. 2. Business validates: - - If platform version ≤ business version: Business **MUST** - process the request - - If platform version > business version: Business **MUST** return - `version_unsupported` error -3. Businesses **MUST** include the version used for processing in every - response. + - If the platform's `version` matches the business's `version` + or is a key in `supported_versions`: the request **MAY** + proceed to capability negotiation using the matching + version of the business profile. + - Otherwise: Business **MUST** return a `version_unsupported` + error. +3. If capability negotiation yields no mutually supported version + for a capability required by the requested operation, the + business **MUST** return a `capabilities_incompatible` error + (see [Error Handling](#error-handling)). +4. Businesses **MUST** include the negotiated protocol version in + every response. Response with version confirmation: ```json { "ucp": { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "capabilities": { ... }, "payment_handlers": { ... } }, @@ -1668,20 +1914,35 @@ Response with version confirmation: } ``` -Version unsupported error: +Version unsupported error — no resource is created: ```json { - "status": "requires_escalation", + "ucp": { "version": "2026-01-11", "status": "error" }, "messages": [{ "type": "error", "code": "version_unsupported", "content": "Version 2026-01-12 is not supported. This business implements version 2026-01-11.", - "severity": "requires_buyer_input" - }] + "severity": "unrecoverable" + }], + "continue_url": "https://merchant.com/" } ``` +#### Capability Versions + +Capability versions are negotiated independently of the protocol +version. Each capability in the profile is an array. Multiple entries +for the same capability, each with a different `version`, advertise +support for multiple versions of that capability. The capability +intersection algorithm considers only capability versions supported +by both parties. + +Businesses **MUST** include only capabilities compatible with the +negotiated protocol version in their response. A capability that +depends on features introduced in a newer protocol version **MUST +NOT** be included when processing at an older protocol version. + ### Backwards Compatibility #### Backwards-Compatible Changes diff --git a/docs/specification/payment-handler-guide.md b/docs/specification/payment-handler-guide.md index 28b769ce7..46cafc7df 100644 --- a/docs/specification/payment-handler-guide.md +++ b/docs/specification/payment-handler-guide.md @@ -125,7 +125,7 @@ PREREQUISITES(participant, onboarding_input) → prerequisites_output **Prerequisites Output:** The `prerequisites_output` contains what a participant receives from onboarding. -At minimum, this includes an **identity** (see [Payment Identity](https://ucp.dev/schemas/shopping/types/payment_identity.json)). +At minimum, this includes an **identity** (see [Payment Identity](site:schemas/shopping/types/payment_identity.json)). It **MAY** also include additional configuration, credentials, or settings specific to the handler. @@ -163,7 +163,7 @@ HANDLER_DECLARATION(prerequisites_output) → handler_declaration **Output Structure:** -The handler declaration conforms to the [`PaymentHandler`](https://ucp.dev/schemas/payment_handler.json) +The handler declaration conforms to the [`PaymentHandler`](site:schemas/payment_handler.json) schema. The specification **SHOULD** define the available config and instrument schemas, and how to construct each based on the business's prerequisites output and desired configuration. @@ -175,7 +175,7 @@ and desired configuration. "com.example.handler": [ { "id": "processor_tokenizer_1234", - "version": "2026-01-11", + "version": "{{ ucp_version }}", "spec": "https://example.com/ucp/handler", "schema": "https://example.com/ucp/handler/schema.json", "available_instruments": [ @@ -216,7 +216,7 @@ and typically includes different configuration: ```json { "id": "processor_tokenizer_1234", - "version": "2026-01-11", + "version": "{{ ucp_version }}", "spec": "https://example.com/ucp/handler", "schema": "https://example.com/ucp/handler/schema.json", "available_instruments": [ @@ -239,7 +239,7 @@ and typically includes different configuration: ```json { "id": "platform_tokenizer_2345", // note: ids are for disambiguation, they may differ between business and platform - "version": "2026-01-11", + "version": "{{ ucp_version }}", "spec": "https://example.com/ucp/handler", "schema": "https://example.com/ucp/handler/schema.json", "available_instruments": [ @@ -262,7 +262,7 @@ and typically includes different configuration: ```json { "id": "processor_tokenizer_1234", - "version": "2026-01-11", + "version": "{{ ucp_version }}", "available_instruments": [ { "type": "card", @@ -334,7 +334,7 @@ Authors typically define each shape in its own file and reference them: "title": "Tokenizer Handler Schema", "description": "Schema for the com.example.tokenizer payment handler.", "name": "com.example.tokenizer", - "version": "2026-01-11", + "version": "{{ ucp_version }}", "$defs": { "tokenizer_token": { "$ref": "types/tokenizer_token.json" }, @@ -356,7 +356,7 @@ Authors typically define each shape in its own file and reference them: "title": "Tokenizer (Platform)", "description": "Platform-level handler configuration for discovery.", "allOf": [ - { "$ref": "https://ucp.dev/schemas/payment_handler.json#/$defs/platform_schema" }, + { "$ref": "https://ucp.dev/{{ ucp_version }}/schemas/payment_handler.json#/$defs/platform_schema" }, { "properties": { "config": { @@ -371,7 +371,7 @@ Authors typically define each shape in its own file and reference them: "title": "Tokenizer (Business)", "description": "Business-level handler configuration for discovery.", "allOf": [ - { "$ref": "https://ucp.dev/schemas/payment_handler.json#/$defs/business_schema" }, + { "$ref": "https://ucp.dev/{{ ucp_version }}/schemas/payment_handler.json#/$defs/business_schema" }, { "properties": { "config": { @@ -386,7 +386,7 @@ Authors typically define each shape in its own file and reference them: "title": "Tokenizer (Response)", "description": "Runtime handler configuration in checkout responses.", "allOf": [ - { "$ref": "https://ucp.dev/schemas/payment_handler.json#/$defs/response_schema" }, + { "$ref": "https://ucp.dev/{{ ucp_version }}/schemas/payment_handler.json#/$defs/response_schema" }, { "properties": { "config": { @@ -494,10 +494,10 @@ Each variant has its own config schema tailored to its context: **Base Instrument Schemas:** -| Schema | Description | -| :---------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------- | -| [`payment_instrument.json`](https://ucp.dev/schemas/shopping/types/payment_instrument.json) | Base: id, handler_id, type, billing_address, credential, display | -| [`card_payment_instrument.json`](https://ucp.dev/schemas/shopping/types/card_payment_instrument.json) | Extends base with display: brand, last_digits, expiry, card art | +| Schema | Description | +| :----------------------------------------------------------------------------------------- | :--------------------------------------------------------------- | +| [`payment_instrument.json`](site:schemas/shopping/types/payment_instrument.json) | Base: id, handler_id, type, billing_address, credential, display | +| [`card_payment_instrument.json`](site:schemas/shopping/types/card_payment_instrument.json) | Extends base with display: brand, last_digits, expiry, card art | UCP provides base schemas for universal payment instruments like `card`. Spec authors **MAY** extend any of the base instruments to add handler-specific @@ -508,13 +508,13 @@ multiple instrument types for different payment flows. Each instrument schema defines its own `available_*` variant in `$defs` that specifies what constraints are valid for that instrument type. For example, -[`card_payment_instrument.json`](https://ucp.dev/schemas/shopping/types/card_payment_instrument.json) +[`card_payment_instrument.json`](site:schemas/shopping/types/card_payment_instrument.json) defines `available_card_payment_instrument` with a `brands` constraint. -| Schema | Constraints | -| :----------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------- | -| [`available_payment_instrument.json`](https://ucp.dev/schemas/shopping/types/available_payment_instrument.json) | Base: type, constraints (open object) | -| `card_payment_instrument.json#/$defs/available_card_payment_instrument` | Extends base with `constraints.brands` for card networks | +| Schema | Constraints | +| :----------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------- | +| [`available_payment_instrument.json`](site:schemas/shopping/types/available_payment_instrument.json) | Base: type, constraints (open object) | +| `card_payment_instrument.json#/$defs/available_card_payment_instrument` | Extends base with `constraints.brands` for card networks | Handlers reference these instrument-defined schemas when declaring `available_instruments`. The **instrument schema authors** define what @@ -534,7 +534,7 @@ constraints are meaningful (e.g., `brands` for cards), and **platforms/businesse "title": "Available Tokenizer Card", "description": "Card instrument availability with tokenizer-specific constraints.", "allOf": [ - { "$ref": "https://ucp.dev/schemas/shopping/types/card_payment_instrument.json#/$defs/available_card_payment_instrument" }, + { "$ref": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/types/card_payment_instrument.json#/$defs/available_card_payment_instrument" }, { "type": "object", "properties": { @@ -556,7 +556,7 @@ constraints are meaningful (e.g., `brands` for cards), and **platforms/businesse }, "allOf": [ - { "$ref": "https://ucp.dev/schemas/shopping/types/card_payment_instrument.json" } + { "$ref": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/types/card_payment_instrument.json" } ], "type": "object", "required": ["type"], @@ -585,7 +585,7 @@ constraints are meaningful (e.g., `brands` for cards), and **platforms/businesse "title": "Tokenizer Alt Instrument", "description": "Alternative payment instrument for com.example.tokenizer.", "allOf": [ - { "$ref": "https://ucp.dev/schemas/shopping/types/payment_instrument.json" } + { "$ref": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/types/payment_instrument.json" } ], "type": "object", "required": ["type"], @@ -611,10 +611,10 @@ constraints are meaningful (e.g., `brands` for cards), and **platforms/businesse **Base Credential Schemas:** -| Schema | Description | -| :------------------------------------------------------------------------------------------ | :---------------------------- | -| [`payment_credential.json`](https://ucp.dev/schemas/shopping/types/payment_credential.json) | Base: type discriminator only | -| [`token_credential.json`](https://ucp.dev/schemas/shopping/types/token_credential.json) | Token: type + token string | +| Schema | Description | +| :------------------------------------------------------------------------------- | :---------------------------- | +| [`payment_credential.json`](site:schemas/shopping/types/payment_credential.json) | Base: type discriminator only | +| [`token_credential.json`](site:schemas/shopping/types/token_credential.json) | Token: type + token string | UCP provides base schemas for universal payment credentials. Authors **MAY** extend these schemas to include handler-specific credential context. Handlers @@ -636,7 +636,7 @@ refresh credentials. "title": "Tokenizer Card Token", "description": "Card token credential for com.example.tokenizer.", "allOf": [ - { "$ref": "https://ucp.dev/schemas/shopping/types/token_credential.json" } + { "$ref": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/types/token_credential.json" } ], "type": "object", "required": ["type", "token", "expiry"], @@ -663,7 +663,7 @@ refresh credentials. "title": "Tokenizer Alt Token", "description": "Alt token credential for com.example.tokenizer, adding routing hints", "allOf": [ - { "$ref": "https://ucp.dev/schemas/shopping/types/token_credential.json" } + { "$ref": "https://ucp.dev/{{ ucp_version }}/schemas/shopping/types/token_credential.json" } ], "type": "object", "required": ["type", "token", "expiry"], diff --git a/docs/specification/payment-handler-template.md b/docs/specification/payment-handler-template.md index 01d9b37e6..baed124c0 100644 --- a/docs/specification/payment-handler-template.md +++ b/docs/specification/payment-handler-template.md @@ -136,7 +136,7 @@ for the full pattern. ```json { "ucp": { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "payment_handlers": { "{handler_name}": [ { @@ -210,7 +210,7 @@ Platforms advertise support for this handler in their UCP profile's ```json { "ucp": { - "version": "2026-01-11", + "version": "{{ ucp_version }}", "payment_handlers": { "{handler_name}": [ { @@ -303,8 +303,8 @@ Content-Type: application/json } ] }, - "risk_signals": { - // risk signal objects here + "signals": { + // Platform-observed signals (buyer connection and device) } } ``` diff --git a/docs/specification/playground.md b/docs/specification/playground.md index 1091c72cd..4ff27bbf9 100644 --- a/docs/specification/playground.md +++ b/docs/specification/playground.md @@ -466,24 +466,24 @@ const UcpData = { capabilities: { "dev.ucp.shopping.checkout": [ { - version: "2026-01-23", - spec: "https://ucp.dev/2026-01-23/specification/checkout", - schema: "https://ucp.dev/2026-01-23/schemas/shopping/checkout.json" + version: "{{ ucp_version }}", + spec: "https://ucp.dev/{{ ucp_version }}/specification/checkout", + schema: "https://ucp.dev/{{ ucp_version }}/schemas/shopping/checkout.json" } ], "dev.ucp.shopping.order": [ { - version: "2026-01-23", - spec: "https://ucp.dev/2026-01-23/specification/order", - schema: "https://ucp.dev/2026-01-23/schemas/shopping/order.json" + version: "{{ ucp_version }}", + spec: "https://ucp.dev/{{ ucp_version }}/specification/order", + schema: "https://ucp.dev/{{ ucp_version }}/schemas/shopping/order.json" } ], "dev.ucp.shopping.fulfillment": [ { extends: "dev.ucp.shopping.checkout", - version: "2026-01-23", - spec: "https://ucp.dev/2026-01-23/specification/fulfillment", - schema: "https://ucp.dev/2026-01-23/schemas/shopping/fulfillment.json", + version: "{{ ucp_version }}", + spec: "https://ucp.dev/{{ ucp_version }}/specification/fulfillment", + schema: "https://ucp.dev/{{ ucp_version }}/schemas/shopping/fulfillment.json", config: { allows_multi_destination: { shipping: false, @@ -499,25 +499,25 @@ const UcpData = { "dev.ucp.shopping.discount": [ { extends: "dev.ucp.shopping.checkout", - version: "2026-01-23", - spec: "https://ucp.dev/2026-01-23/specification/discount", - schema: "https://ucp.dev/2026-01-23/schemas/shopping/discount.json" + version: "{{ ucp_version }}", + spec: "https://ucp.dev/{{ ucp_version }}/specification/discount", + schema: "https://ucp.dev/{{ ucp_version }}/schemas/shopping/discount.json" } ], "dev.ucp.shopping.buyer_consent": [ { extends: "dev.ucp.shopping.checkout", - version: "2026-01-23", - spec: "https://ucp.dev/2026-01-23/specification/buyer-consent", - schema: "https://ucp.dev/2026-01-23/schemas/shopping/buyer_consent.json" + version: "{{ ucp_version }}", + spec: "https://ucp.dev/{{ ucp_version }}/specification/buyer-consent", + schema: "https://ucp.dev/{{ ucp_version }}/schemas/shopping/buyer_consent.json" } ], "dev.ucp.shopping.ap2_mandates": [ { extends: "dev.ucp.shopping.checkout", - version: "2026-01-23", - spec: "https://ucp.dev/2026-01-23/specification/ap2-mandates", - schema: "https://ucp.dev/2026-01-23/schemas/shopping/ap2_mandate.json" + version: "{{ ucp_version }}", + spec: "https://ucp.dev/{{ ucp_version }}/specification/ap2-mandates", + schema: "https://ucp.dev/{{ ucp_version }}/schemas/shopping/ap2_mandate.json" } ] }, diff --git a/docs/specification/reference.md b/docs/specification/reference.md index d7497afcb..c2b517d58 100644 --- a/docs/specification/reference.md +++ b/docs/specification/reference.md @@ -27,11 +27,27 @@ within the UCP. {{ auto_generate_schema_reference('types', 'reference', include_extensions=False) }} +### Selected Payment Instrument {: #payment-instrument-selected-payment-instrument } + +{{ extension_schema_fields('types/payment_instrument.json#/$defs/selected_payment_instrument', 'reference') }} + +### Pagination Request {: #pagination-request } + +{{ extension_schema_fields('types/pagination.json#/$defs/request', 'reference') }} + +### Pagination Response {: #pagination-response } + +{{ extension_schema_fields('types/pagination.json#/$defs/response', 'reference') }} + +### Error Code {: #error-code } + +{{ schema_fields('types/error_code', 'reference') }} + ## Extension Schemas {{ auto_generate_schema_reference('.', 'reference', include_capability=False) }} -## UCP Metadata +## UCP Metadata The following schemas define the structure of UCP metadata used in discovery and responses. @@ -48,13 +64,19 @@ The top-level structure of a business discovery document (`/.well-known/ucp`). {{ extension_schema_fields('ucp.json#/$defs/business_schema', 'reference') }} -### Checkout Response Metadata +### Checkout Response Metadata {: #ucp-response-checkout-schema } The `ucp` object included in checkout responses. {{ extension_schema_fields('ucp.json#/$defs/response_checkout_schema', 'reference') }} -### Order Response Metadata +### Cart Response Metadata {: #ucp-response-cart-schema } + +The `ucp` object included in cart responses. + +{{ extension_schema_fields('ucp.json#/$defs/response_cart_schema', 'reference') }} + +### Order Response Metadata {: #ucp-response-order-schema } The `ucp` object included in order responses or events. @@ -66,13 +88,13 @@ This object describes a single capability or extension. It appears in the `capabilities` array in discovery profiles and responses, with slightly different required fields in each context. -#### Capability (Discovery) +#### Capability (Discovery) {: #discovery } As seen in discovery profiles. {{ extension_schema_fields('capability.json#/$defs/platform_schema', 'reference') }} -#### Capability (Response) +#### Capability (Response) {: #response } As seen in response messages. diff --git a/docs/specification/signatures.md b/docs/specification/signatures.md index ec979cab2..9705b361c 100644 --- a/docs/specification/signatures.md +++ b/docs/specification/signatures.md @@ -25,7 +25,7 @@ This specification defines how to sign and verify UCP messages using [RFC 9421](https://www.rfc-editor.org/rfc/rfc9421) HTTP Message Signatures. For UCP's identity model, supported authentication mechanisms, and key discovery protocol, see -[Identity & Authentication](overview.md#identity--authentication). +[Identity & Authentication](overview.md#identity-authentication). HTTP Message Signatures protect against: diff --git a/docs/specification/tokenization-guide.md b/docs/specification/tokenization-guide.md index 9a4ec2e7f..c574b6fbf 100644 --- a/docs/specification/tokenization-guide.md +++ b/docs/specification/tokenization-guide.md @@ -16,7 +16,7 @@ # Tokenization Guide -**OpenAPI:** [Tokenization API](https://ucp.dev/handlers/tokenization/openapi.json) +**OpenAPI:** [Tokenization API](site:handlers/tokenization/openapi.json) ## Overview @@ -99,7 +99,7 @@ specific context: | `checkout_id` | Yes | The checkout session this token is valid for | | `identity` | Conditional | The participant identity to bind to; required when caller acts on behalf of another participant | -The tokenizer **MUST** verify binding matches on `/detokenize`. See [Binding Schema](https://ucp.dev/schemas/shopping/types/binding.json). +The tokenizer **MUST** verify binding matches on `/detokenize`. See [Binding Schema](site:schemas/shopping/types/binding.json). --- @@ -183,7 +183,7 @@ Authorization: Bearer {caller_access_token} binding target. Include it when acting on behalf of another participant (e.g., PSP detokenizing for business). -See the full [OpenAPI specification](https://ucp.dev/handlers/tokenization/openapi.json) for complete request/response schemas. +See the full [OpenAPI specification](site:handlers/tokenization/openapi.json) for complete request/response schemas. --- @@ -220,7 +220,7 @@ When publishing your handler, your specification document **MUST** include: ```markdown **Handler Name:** `com.acme.tokenization_payment` -**OpenAPI:** [Tokenization API](https://ucp.dev/handlers/tokenization/openapi.json) +**OpenAPI:** [Tokenization API](site:handlers/tokenization/openapi.json) | Environment | Base URL | | :---------- | :--------------------------------- | @@ -261,13 +261,13 @@ A tokenizer handler conforms to this pattern if it: ## References -| Resource | URL | -| :---------------------- | :-------------------------------------------------------------------- | -| Tokenization OpenAPI | `https://ucp.dev/handlers/tokenization/openapi.json` | -| Identity Schema | `https://ucp.dev/schemas/shopping/types/payment_identity.json` | -| Binding Schema | `https://ucp.dev/schemas/shopping/types/binding.json` | -| Token Credential Schema | `https://ucp.dev/schemas/shopping/types/token_credential.json` | -| Card Instrument Schema | `https://ucp.dev/schemas/shopping/types/card_payment_instrument.json` | +| Resource | URL | +| :---------------------- | :-------------------------------------------------------------------------------------------------------------- | +| Tokenization OpenAPI | [handlers/tokenization/openapi.json](site:handlers/tokenization/openapi.json) | +| Identity Schema | [schemas/shopping/types/payment_identity.json](site:schemas/shopping/types/payment_identity.json) | +| Binding Schema | [schemas/shopping/types/binding.json](site:schemas/shopping/types/binding.json) | +| Token Credential Schema | [schemas/shopping/types/token_credential.json](site:schemas/shopping/types/token_credential.json) | +| Card Instrument Schema | [schemas/shopping/types/card_payment_instrument.json](site:schemas/shopping/types/card_payment_instrument.json) | --- diff --git a/generate_ts_schema_types.js b/generate_ts_schema_types.js deleted file mode 100644 index 1b0120d9c..000000000 --- a/generate_ts_schema_types.js +++ /dev/null @@ -1,145 +0,0 @@ -const fs = require('node:fs'); -const path = require('node:path'); -const { compile } = require('json-schema-to-typescript'); - -const SOURCE_ROOT = path.resolve(__dirname, 'spec'); -const OUTPUT_FILE = path.resolve(__dirname, './generated/schema-types.ts'); -const WRAPPER_NAME = 'SCHEMA_WRAPPER'; - -/** - * Dynamically finds all JSON schemas and generates TypeScript types. - */ -async function generate() { - if (!fs.existsSync(path.dirname(OUTPUT_FILE))) { - fs.mkdirSync(path.dirname(OUTPUT_FILE), {recursive: true}); - } - - const properties = {}; - - // Add shopping schemas - const shoppingDir = path.join(SOURCE_ROOT, 'schemas/shopping'); - if (fs.existsSync(shoppingDir)) { - for (const file of fs.readdirSync(shoppingDir)) { - if (file.endsWith('.json')) { - properties[path.basename(file, '.json')] = { - $ref: path.join(shoppingDir, file) - }; - } - } - } - - // Add handler schemas - const handlersDir = path.join(SOURCE_ROOT, 'handlers'); - if (fs.existsSync(handlersDir)) { - for (const handler of fs.readdirSync(handlersDir)) { - const handlerPath = path.join(handlersDir, handler); - if (fs.statSync(handlerPath).isDirectory()) { - for (const file of fs.readdirSync(handlerPath)) { - if (file.endsWith('.json')) { - const name = - `${handler}_${path.basename(file, '.json')}`.replace(/-/g, '_'); - properties[name] = {$ref: path.join(handlerPath, file)}; - } - } - } - } - } - - console.log(`Found ${Object.keys(properties).length} schemas. Compiling...`); - - const wrappedSchema = { - title: WRAPPER_NAME, - type: 'object', - properties, - additionalProperties: false - }; - - try { - let ts = await compile(wrappedSchema, WRAPPER_NAME, { - cwd: SOURCE_ROOT, - $refOptions: { - resolve: { - file: { - order: 1, - canRead: true, - read: (file) => { - let filePath = file.url; - if (filePath.startsWith('file://')) { - try { - filePath = require('node:url').fileURLToPath(filePath); - } catch { - filePath = filePath.replace('file://', ''); - } - } - - const content = fs.readFileSync(filePath, 'utf8'); - const json = JSON.parse(content); - /** - * Cleans up the JSON object by removing properties that interfere - * with `json-schema-to-typescript`. - * This function mutates the input object. While acceptable here, - * be mindful of side effects. If this JSON object were used - * elsewhere, this could lead to unexpected behavior. - * @param {!any} obj The object to clean. - */ - function clean(obj) { - if (typeof obj !== 'object' || obj === null) return; - - // When $ref is present, other properties like title and - // description are technically ignored in older JSON Schema - // drafts. We remove them here to prevent - // json-schema-to-typescript from generating duplicate interface - // definitions or JSDoc comments that conflict with the - // referenced type. - if (obj.$ref) { - delete obj.description; - delete obj.title; - } - - for (const key in obj) { - clean(obj[key]); - } - } - - clean(json); - return json; - } - } - } - }, - bannerComment: ` -/* tslint:disable:enforce-comments-on-exported-symbols */ -/* eslint-disable */ -/* tslint:disable:enforce-name-casing */ -/** - * This file was automatically generated by json-schema-to-typescript. - * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, - * and run json-schema-to-typescript to regenerate this file. - */ -`, - style: {singleQuote: true, bracketSpacing: true}, - declareExternallyReferenced: true, - enableConstEnums: false, - unreachableDefinitions: true, - strictIndexSignatures: false - }); - - // Cleanup: Remove the wrapper interface and convert to 'export declare interface' - // We use \n} to match the closing brace at the start of a line to avoid matching nested braces - const wrapperRegex = new RegExp(`export interface ${WRAPPER_NAME}\\s*\\{[\\s\\S]*?\\n\\}\\s*`, 'g'); - ts = ts.replace(wrapperRegex, '') - .replace(/export interface/g, 'export declare interface'); - - // Replace (A | B)[] with Array - ts = ts.replace(/:\s*\(([^)]+)\)\[\]/g, ': Array<$1>'); - // Replace { ... }[] with Array<{ ... }> - ts = ts.replace(/:\s*(\{[^}]+\})\[\]/g, ': Array<$1>'); - - fs.writeFileSync(OUTPUT_FILE, ts.trim()); - console.log(`Success! Types written to ${OUTPUT_FILE}`); - } catch (err) { - console.error('Error generating types:', err); - } -} - -generate(); diff --git a/generated/schema-types.ts b/generated/schema-types.ts deleted file mode 100644 index 92f481cad..000000000 --- a/generated/schema-types.ts +++ /dev/null @@ -1,2039 +0,0 @@ -/* tslint:disable:enforce-comments-on-exported-symbols */ -/* eslint-disable */ -/* tslint:disable:enforce-name-casing */ -/** - * This file was automatically generated by json-schema-to-typescript. - * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file, - * and run json-schema-to-typescript to regenerate this file. - */ - -/** - * JWS Detached Content signature (RFC 7515 Appendix F) over the checkout response body (excluding ap2 field). Format: `..`. The header MUST contain 'alg' (ES256/ES384/ES512) and 'kid' claims. The signature covers both the header and JCS-canonicalized checkout payload. - * - * This interface was referenced by `AP2MandateExtensionCompleteRequest`'s JSON-Schema - * via the `definition` "merchant_authorization". - */ -export type MerchantAuthorizationCompleteRequest = string; -/** - * SD-JWT+kb credential in `ap2.checkout_mandate`. Proving user authorization for the checkout. Contains the full checkout including `ap2.merchant_authorization`. - * - * This interface was referenced by `AP2MandateExtensionCompleteRequest`'s JSON-Schema - * via the `definition` "checkout_mandate". - */ -export type CheckoutMandateCompleteRequest = string; -/** - * Checkout extended with AP2 mandate support. - * - * This interface was referenced by `AP2MandateExtensionCompleteRequest`'s JSON-Schema - * via the `definition` "checkout". - */ -export type CheckoutWithAP2MandateCompleteRequest = CheckoutCompleteRequest & { - ap2?: Ap2WithMerchantAuthorization & Ap2WithCheckoutMandate; - [k: string]: unknown; -}; -/** - * Matches a specific instrument type based on validation logic. - */ -export type PaymentInstrument = CardPaymentInstrument; -/** - * A basic card payment instrument with visible card details. Can be inherited by a handler's instrument schema to define handler-specific display details or more complex credential structures. - */ -export type CardPaymentInstrument = PaymentInstrumentBase & { - /** - * Indicates this is a card payment instrument. - */ - type: 'card'; - /** - * The card brand/network (e.g., visa, mastercard, amex). - */ - brand: string; - /** - * Last 4 digits of the card number. - */ - last_digits: string; - /** - * The month of the card's expiration date (1-12). - */ - expiry_month?: number; - /** - * The year of the card's expiration date. - */ - expiry_year?: number; - /** - * An optional rich text description of the card to display to the user (e.g., 'Visa ending in 1234, expires 12/2025'). - */ - rich_text_description?: string; - /** - * An optional URI to a rich image representing the card (e.g., card art provided by the issuer). - */ - rich_card_art?: string; - [k: string]: unknown; -}; -/** - * Container for sensitive payment data. Use the specific schema matching the 'type' field. - */ -export type PaymentCredential = TokenCredentialResponse | CardCredential; -/** - * Error codes specific to AP2 mandate verification. - * - * This interface was referenced by `AP2MandateExtensionCompleteRequest`'s JSON-Schema - * via the `definition` "error_code". - */ -export type AP2ErrorCodeCompleteRequest = - | 'mandate_required' - | 'agent_missing_key' - | 'mandate_invalid_signature' - | 'mandate_expired' - | 'mandate_scope_mismatch' - | 'merchant_authorization_invalid' - | 'merchant_authorization_missing'; -/** - * JWS Detached Content signature (RFC 7515 Appendix F) over the checkout response body (excluding ap2 field). Format: `..`. The header MUST contain 'alg' (ES256/ES384/ES512) and 'kid' claims. The signature covers both the header and JCS-canonicalized checkout payload. - * - * This interface was referenced by `AP2MandateExtensionCreateRequest`'s JSON-Schema - * via the `definition` "merchant_authorization". - */ -export type MerchantAuthorizationCreateRequest = string; -/** - * SD-JWT+kb credential in `ap2.checkout_mandate`. Proving user authorization for the checkout. Contains the full checkout including `ap2.merchant_authorization`. - * - * This interface was referenced by `AP2MandateExtensionCreateRequest`'s JSON-Schema - * via the `definition` "checkout_mandate". - */ -export type CheckoutMandateCreateRequest = string; -/** - * Checkout extended with AP2 mandate support. - * - * This interface was referenced by `AP2MandateExtensionCreateRequest`'s JSON-Schema - * via the `definition` "checkout". - */ -export type CheckoutWithAP2MandateCreateRequest = CheckoutCreateRequest & { - [k: string]: unknown; -}; -/** - * Error codes specific to AP2 mandate verification. - * - * This interface was referenced by `AP2MandateExtensionCreateRequest`'s JSON-Schema - * via the `definition` "error_code". - */ -export type AP2ErrorCodeCreateRequest = - | 'mandate_required' - | 'agent_missing_key' - | 'mandate_invalid_signature' - | 'mandate_expired' - | 'mandate_scope_mismatch' - | 'merchant_authorization_invalid' - | 'merchant_authorization_missing'; -/** - * JWS Detached Content signature (RFC 7515 Appendix F) over the checkout response body (excluding ap2 field). Format: `..`. The header MUST contain 'alg' (ES256/ES384/ES512) and 'kid' claims. The signature covers both the header and JCS-canonicalized checkout payload. - * - * This interface was referenced by `AP2MandateExtensionUpdateRequest`'s JSON-Schema - * via the `definition` "merchant_authorization". - */ -export type MerchantAuthorizationUpdateRequest = string; -/** - * SD-JWT+kb credential in `ap2.checkout_mandate`. Proving user authorization for the checkout. Contains the full checkout including `ap2.merchant_authorization`. - * - * This interface was referenced by `AP2MandateExtensionUpdateRequest`'s JSON-Schema - * via the `definition` "checkout_mandate". - */ -export type CheckoutMandateUpdateRequest = string; -/** - * Checkout extended with AP2 mandate support. - * - * This interface was referenced by `AP2MandateExtensionUpdateRequest`'s JSON-Schema - * via the `definition` "checkout". - */ -export type CheckoutWithAP2MandateUpdateRequest = CheckoutUpdateRequest & { - [k: string]: unknown; -}; -/** - * Error codes specific to AP2 mandate verification. - * - * This interface was referenced by `AP2MandateExtensionUpdateRequest`'s JSON-Schema - * via the `definition` "error_code". - */ -export type AP2ErrorCodeUpdateRequest = - | 'mandate_required' - | 'agent_missing_key' - | 'mandate_invalid_signature' - | 'mandate_expired' - | 'mandate_scope_mismatch' - | 'merchant_authorization_invalid' - | 'merchant_authorization_missing'; -/** - * JWS Detached Content signature (RFC 7515 Appendix F) over the checkout response body (excluding ap2 field). Format: `..`. The header MUST contain 'alg' (ES256/ES384/ES512) and 'kid' claims. The signature covers both the header and JCS-canonicalized checkout payload. - * - * This interface was referenced by `AP2MandateExtensionResponse`'s JSON-Schema - * via the `definition` "merchant_authorization". - */ -export type MerchantAuthorizationResponse = string; -/** - * SD-JWT+kb credential in `ap2.checkout_mandate`. Proving user authorization for the checkout. Contains the full checkout including `ap2.merchant_authorization`. - * - * This interface was referenced by `AP2MandateExtensionResponse`'s JSON-Schema - * via the `definition` "checkout_mandate". - */ -export type CheckoutMandateResponse = string; -/** - * Checkout extended with AP2 mandate support. - * - * This interface was referenced by `AP2MandateExtensionResponse`'s JSON-Schema - * via the `definition` "checkout". - */ -export type CheckoutWithAP2MandateResponse = CheckoutResponse & { - ap2?: Ap2WithMerchantAuthorization1 & Ap2WithCheckoutMandate1; - [k: string]: unknown; -}; -/** - * Capability reference in responses. Only name/version required to confirm active capabilities. - */ -export type CapabilityResponse = Base & { - [k: string]: unknown; -}; -/** - * Container for error, warning, or info messages. - */ -export type Message = MessageError | MessageWarning | MessageInfo; -/** - * Error codes specific to AP2 mandate verification. - * - * This interface was referenced by `AP2MandateExtensionResponse`'s JSON-Schema - * via the `definition` "error_code". - */ -export type AP2ErrorCodeResponse = - | 'mandate_required' - | 'agent_missing_key' - | 'mandate_invalid_signature' - | 'mandate_expired' - | 'mandate_scope_mismatch' - | 'merchant_authorization_invalid' - | 'merchant_authorization_missing'; -/** - * Buyer object extended with consent tracking. - * - * This interface was referenced by `BuyerConsentExtensionCompleteRequest`'s JSON-Schema - * via the `definition` "buyer". - */ -export type BuyerWithConsentCompleteRequest = Buyer & { - consent?: Consent; - [k: string]: unknown; -}; -/** - * Checkout extended with consent tracking via buyer object. - * - * This interface was referenced by `BuyerConsentExtensionCompleteRequest`'s JSON-Schema - * via the `definition` "checkout". - */ -export type CheckoutWithBuyerConsentCompleteRequest = CheckoutCompleteRequest & { - [k: string]: unknown; -}; -/** - * Buyer object extended with consent tracking. - * - * This interface was referenced by `BuyerConsentExtensionCreateRequest`'s JSON-Schema - * via the `definition` "buyer". - */ -export type BuyerWithConsentCreateRequest = Buyer & { - consent?: Consent1; - [k: string]: unknown; -}; -/** - * Checkout extended with consent tracking via buyer object. - * - * This interface was referenced by `BuyerConsentExtensionCreateRequest`'s JSON-Schema - * via the `definition` "checkout". - */ -export type CheckoutWithBuyerConsentCreateRequest = CheckoutCreateRequest & { - buyer?: BuyerWithConsentCreateRequest; - [k: string]: unknown; -}; -/** - * Buyer object extended with consent tracking. - * - * This interface was referenced by `BuyerConsentExtensionUpdateRequest`'s JSON-Schema - * via the `definition` "buyer". - */ -export type BuyerWithConsentUpdateRequest = Buyer & { - consent?: Consent2; - [k: string]: unknown; -}; -/** - * Checkout extended with consent tracking via buyer object. - * - * This interface was referenced by `BuyerConsentExtensionUpdateRequest`'s JSON-Schema - * via the `definition` "checkout". - */ -export type CheckoutWithBuyerConsentUpdateRequest = CheckoutUpdateRequest & { - buyer?: BuyerWithConsentUpdateRequest; - [k: string]: unknown; -}; -/** - * Buyer object extended with consent tracking. - * - * This interface was referenced by `BuyerConsentExtensionResponse`'s JSON-Schema - * via the `definition` "buyer". - */ -export type BuyerWithConsentResponse = Buyer & { - consent?: Consent3; - [k: string]: unknown; -}; -/** - * Checkout extended with consent tracking via buyer object. - * - * This interface was referenced by `BuyerConsentExtensionResponse`'s JSON-Schema - * via the `definition` "checkout". - */ -export type CheckoutWithBuyerConsentResponse = CheckoutResponse & { - buyer?: BuyerWithConsentResponse; - [k: string]: unknown; -}; -/** - * Checkout extended with discount capability. - * - * This interface was referenced by `DiscountExtensionCompleteRequest`'s JSON-Schema - * via the `definition` "checkout". - */ -export type CheckoutWithDiscountCompleteRequest = CheckoutCompleteRequest & { - [k: string]: unknown; -}; -/** - * Checkout extended with discount capability. - * - * This interface was referenced by `DiscountExtensionCreateRequest`'s JSON-Schema - * via the `definition` "checkout". - */ -export type CheckoutWithDiscountCreateRequest = CheckoutCreateRequest & { - discounts?: DiscountsObject; - [k: string]: unknown; -}; -/** - * Checkout extended with discount capability. - * - * This interface was referenced by `DiscountExtensionUpdateRequest`'s JSON-Schema - * via the `definition` "checkout". - */ -export type CheckoutWithDiscountUpdateRequest = CheckoutUpdateRequest & { - discounts?: DiscountsObject1; - [k: string]: unknown; -}; -/** - * Checkout extended with discount capability. - * - * This interface was referenced by `DiscountExtensionResponse`'s JSON-Schema - * via the `definition` "checkout". - */ -export type CheckoutWithDiscountResponse = CheckoutResponse & { - discounts?: DiscountsObject2; - [k: string]: unknown; -}; -/** - * A destination for fulfillment. - */ -export type FulfillmentDestinationRequest = ShippingDestinationRequest | RetailLocationRequest; -/** - * Shipping destination. - */ -export type ShippingDestinationRequest = PostalAddress & { - /** - * ID specific to this shipping destination. - */ - id?: string; - [k: string]: unknown; -}; -/** - * Checkout extended with hierarchical fulfillment. - * - * This interface was referenced by `FulfillmentExtensionCompleteRequest`'s JSON-Schema - * via the `definition` "checkout". - */ -export type CheckoutWithFulfillmentCompleteRequest = CheckoutCompleteRequest & { - [k: string]: unknown; -}; -/** - * Checkout extended with hierarchical fulfillment. - * - * This interface was referenced by `FulfillmentExtensionCreateRequest`'s JSON-Schema - * via the `definition` "checkout". - */ -export type CheckoutWithFulfillmentCreateRequest = CheckoutCreateRequest & { - fulfillment?: FulfillmentRequest; - [k: string]: unknown; -}; -/** - * Checkout extended with hierarchical fulfillment. - * - * This interface was referenced by `FulfillmentExtensionUpdateRequest`'s JSON-Schema - * via the `definition` "checkout". - */ -export type CheckoutWithFulfillmentUpdateRequest = CheckoutUpdateRequest & { - fulfillment?: FulfillmentRequest; - [k: string]: unknown; -}; -/** - * A destination for fulfillment. - */ -export type FulfillmentDestinationResponse = ShippingDestinationResponse | RetailLocationResponse; -/** - * Shipping destination. - */ -export type ShippingDestinationResponse = PostalAddress & { - /** - * ID specific to this shipping destination. - */ - id: string; - [k: string]: unknown; -}; -/** - * Checkout extended with hierarchical fulfillment. - * - * This interface was referenced by `FulfillmentExtensionResponse`'s JSON-Schema - * via the `definition` "checkout". - */ -export type CheckoutWithFulfillmentResponse = CheckoutResponse & { - fulfillment?: FulfillmentResponse; - [k: string]: unknown; -}; - -/** - * Extends Checkout with cryptographic mandate support for non-repudiable authorization per the AP2 protocol. Uses embedded signature model with ap2 namespace. - */ -export declare interface AP2MandateExtensionCompleteRequest { - [k: string]: unknown; -} -/** - * AP2 extension data including merchant authorization. - * - * This interface was referenced by `AP2MandateExtensionCompleteRequest`'s JSON-Schema - * via the `definition` "ap2_with_merchant_authorization". - */ -export declare interface Ap2WithMerchantAuthorization { - [k: string]: unknown; -} -/** - * AP2 extension data including checkout mandate. - * - * This interface was referenced by `AP2MandateExtensionCompleteRequest`'s JSON-Schema - * via the `definition` "ap2_with_checkout_mandate". - */ -export declare interface Ap2WithCheckoutMandate { - checkout_mandate?: CheckoutMandateCompleteRequest; - [k: string]: unknown; -} -/** - * Base checkout schema. Extensions compose onto this using allOf. - */ -export declare interface CheckoutCompleteRequest { - payment: PaymentCompleteRequest; - [k: string]: unknown; -} -/** - * Payment configuration containing handlers. - */ -export declare interface PaymentCompleteRequest { - /** - * The id of the currently selected payment instrument from the instruments array. Set by the agent when submitting payment, and echoed back by the merchant in finalized state. - */ - selected_instrument_id?: string; - /** - * The payment instruments available for this payment. Each instrument is associated with a specific handler via the handler_id field. Handlers can extend the base payment_instrument schema to add handler-specific fields. - */ - instruments?: PaymentInstrument[]; - [k: string]: unknown; -} -/** - * The base definition for any payment instrument. It links the instrument to a specific Merchant configuration (handler_id) and defines common fields like billing address. - */ -export declare interface PaymentInstrumentBase { - /** - * A unique identifier for this instrument instance, assigned by the Agent. Used to reference this specific instrument in the 'payment.selected_instrument_id' field. - */ - id: string; - /** - * The unique identifier for the handler instance that produced this instrument. This corresponds to the 'id' field in the Payment Handler definition. - */ - handler_id: string; - /** - * The broad category of the instrument (e.g., 'card', 'tokenized_card'). Specific schemas will constrain this to a constant value. - */ - type: string; - billing_address?: PostalAddress; - credential?: PaymentCredential; - [k: string]: unknown; -} -export declare interface PostalAddress { - /** - * An address extension such as an apartment number, C/O or alternative name. - */ - extended_address?: string; - /** - * The street address. - */ - street_address?: string; - /** - * The locality in which the street address is, and which is in the region. For example, Mountain View. - */ - address_locality?: string; - /** - * The region in which the locality is, and which is in the country. Required for applicable countries (i.e. state in US, province in CA). For example, California or another appropriate first-level Administrative division. - */ - address_region?: string; - /** - * The country. Recommended to be in 2-letter ISO 3166-1 alpha-2 format, for example "US". For backward compatibility, a 3-letter ISO 3166-1 alpha-3 country code such as "SGP" or a full country name such as "Singapore" can also be used. - */ - address_country?: string; - /** - * The postal code. For example, 94043. - */ - postal_code?: string; - /** - * Optional. First name of the contact associated with the address. - */ - first_name?: string; - /** - * Optional. Last name of the contact associated with the address. - */ - last_name?: string; - /** - * Optional. Phone number of the contact associated with the address. - */ - phone_number?: string; - [k: string]: unknown; -} -/** - * Base token credential schema. Concrete payment handlers may extend this schema with additional fields and define their own constraints. - */ -export declare interface TokenCredentialResponse { - /** - * The specific type of token produced by the handler (e.g., 'stripe_token'). - */ - type: string; - [k: string]: unknown; -} -/** - * A card credential containing sensitive payment card details including raw Primary Account Numbers (PANs). This credential type MUST NOT be used for checkout, only with payment handlers that tokenize or encrypt credentials. CRITICAL: Both parties handling CardCredential (sender and receiver) MUST be PCI DSS compliant. Transmission MUST use HTTPS/TLS with strong cipher suites. - */ -export declare interface CardCredential { - /** - * The credential type identifier for card credentials. - */ - type: 'card'; - /** - * The type of card number. Network tokens are preferred with fallback to FPAN. See PCI Scope for more details. - */ - card_number_type: 'fpan' | 'network_token' | 'dpan'; - /** - * Card number. - */ - number?: string; - /** - * The month of the card's expiration date (1-12). - */ - expiry_month?: number; - /** - * The year of the card's expiration date. - */ - expiry_year?: number; - /** - * Cardholder name. - */ - name?: string; - /** - * Card CVC number. - */ - cvc?: string; - /** - * Cryptogram provided with network tokens. - */ - cryptogram?: string; - /** - * Electronic Commerce Indicator / Security Level Indicator provided with network tokens. - */ - eci_value?: string; - [k: string]: unknown; -} -/** - * Extends Checkout with cryptographic mandate support for non-repudiable authorization per the AP2 protocol. Uses embedded signature model with ap2 namespace. - */ -export declare interface AP2MandateExtensionCreateRequest { - [k: string]: unknown; -} -/** - * Base checkout schema. Extensions compose onto this using allOf. - */ -export declare interface CheckoutCreateRequest { - /** - * List of line items being checked out. - */ - line_items: LineItemCreateRequest[]; - buyer?: Buyer; - /** - * ISO 4217 currency code. - */ - currency: string; - payment?: PaymentCreateRequest; - [k: string]: unknown; -} -/** - * Line item object. Expected to use the currency of the parent object. - */ -export declare interface LineItemCreateRequest { - item: ItemCreateRequest; - /** - * Quantity of the item being purchased. - */ - quantity: number; - [k: string]: unknown; -} -export declare interface ItemCreateRequest { - /** - * Should be recognized by both the Platform, and the Business. For Google it should match the id provided in the "id" field in the product feed. - */ - id: string; - [k: string]: unknown; -} -export declare interface Buyer { - /** - * First name of the buyer. - */ - first_name?: string; - /** - * Last name of the buyer. - */ - last_name?: string; - /** - * Email of the buyer. - */ - email?: string; - /** - * E.164 standard. - */ - phone_number?: string; - [k: string]: unknown; -} -/** - * Payment configuration containing handlers. - */ -export declare interface PaymentCreateRequest { - /** - * The id of the currently selected payment instrument from the instruments array. Set by the agent when submitting payment, and echoed back by the merchant in finalized state. - */ - selected_instrument_id?: string; - /** - * The payment instruments available for this payment. Each instrument is associated with a specific handler via the handler_id field. Handlers can extend the base payment_instrument schema to add handler-specific fields. - */ - instruments?: PaymentInstrument[]; - [k: string]: unknown; -} -/** - * Extends Checkout with cryptographic mandate support for non-repudiable authorization per the AP2 protocol. Uses embedded signature model with ap2 namespace. - */ -export declare interface AP2MandateExtensionUpdateRequest { - [k: string]: unknown; -} -/** - * Base checkout schema. Extensions compose onto this using allOf. - */ -export declare interface CheckoutUpdateRequest { - /** - * Unique identifier of the checkout session. - */ - id: string; - /** - * List of line items being checked out. - */ - line_items: LineItemUpdateRequest[]; - buyer?: Buyer; - /** - * ISO 4217 currency code. - */ - currency: string; - payment?: PaymentUpdateRequest; - [k: string]: unknown; -} -/** - * Line item object. Expected to use the currency of the parent object. - */ -export declare interface LineItemUpdateRequest { - id?: string; - item: ItemUpdateRequest; - /** - * Quantity of the item being purchased. - */ - quantity: number; - /** - * Parent line item identifier for any nested structures. - */ - parent_id?: string; - [k: string]: unknown; -} -export declare interface ItemUpdateRequest { - /** - * Should be recognized by both the Platform, and the Business. For Google it should match the id provided in the "id" field in the product feed. - */ - id: string; - [k: string]: unknown; -} -/** - * Payment configuration containing handlers. - */ -export declare interface PaymentUpdateRequest { - /** - * The id of the currently selected payment instrument from the instruments array. Set by the agent when submitting payment, and echoed back by the merchant in finalized state. - */ - selected_instrument_id?: string; - /** - * The payment instruments available for this payment. Each instrument is associated with a specific handler via the handler_id field. Handlers can extend the base payment_instrument schema to add handler-specific fields. - */ - instruments?: PaymentInstrument[]; - [k: string]: unknown; -} -/** - * Extends Checkout with cryptographic mandate support for non-repudiable authorization per the AP2 protocol. Uses embedded signature model with ap2 namespace. - */ -export declare interface AP2MandateExtensionResponse { - [k: string]: unknown; -} -/** - * AP2 extension data including merchant authorization. - * - * This interface was referenced by `AP2MandateExtensionResponse`'s JSON-Schema - * via the `definition` "ap2_with_merchant_authorization". - */ -export declare interface Ap2WithMerchantAuthorization1 { - merchant_authorization?: MerchantAuthorizationResponse; - [k: string]: unknown; -} -/** - * AP2 extension data including checkout mandate. - * - * This interface was referenced by `AP2MandateExtensionResponse`'s JSON-Schema - * via the `definition` "ap2_with_checkout_mandate". - */ -export declare interface Ap2WithCheckoutMandate1 { - checkout_mandate?: CheckoutMandateResponse; - [k: string]: unknown; -} -/** - * Base checkout schema. Extensions compose onto this using allOf. - */ -export declare interface CheckoutResponse { - ucp: UCPCheckoutResponse; - /** - * Unique identifier of the checkout session. - */ - id: string; - /** - * List of line items being checked out. - */ - line_items: LineItemResponse[]; - buyer?: Buyer; - /** - * Checkout state indicating the current phase and required action. See Checkout Status lifecycle documentation for state transition details. - */ - status: - | 'incomplete' - | 'requires_escalation' - | 'ready_for_complete' - | 'complete_in_progress' - | 'completed' - | 'canceled'; - /** - * ISO 4217 currency code. - */ - currency: string; - /** - * Different cart totals. - */ - totals: TotalResponse[]; - /** - * List of messages with error and info about the checkout session state. - */ - messages?: Message[]; - /** - * Links to be displayed by the platform (Privacy Policy, TOS). Mandatory for legal compliance. - */ - links: Link[]; - /** - * RFC 3339 expiry timestamp. Default TTL is 6 hours from creation if not sent. - */ - expires_at?: string; - /** - * URL for checkout handoff and session recovery. MUST be provided when status is requires_escalation. See specification for format and availability requirements. - */ - continue_url?: string; - payment: PaymentResponse; - order?: OrderConfirmation; - [k: string]: unknown; -} -/** - * UCP metadata for checkout responses. - */ -export declare interface UCPCheckoutResponse { - /** - * UCP protocol version in YYYY-MM-DD format. - */ - version: string; - /** - * Active capabilities for this response. - */ - capabilities: CapabilityResponse[]; - [k: string]: unknown; -} -export declare interface Base { - /** - * Stable capability identifier in reverse-domain notation (e.g., dev.ucp.shopping.checkout). Used in capability negotiation. - */ - name?: string; - /** - * UCP protocol version in YYYY-MM-DD format. - */ - version?: string; - /** - * URL to human-readable specification document. - */ - spec?: string; - /** - * URL to JSON Schema for this capability's payload. - */ - schema?: string; - /** - * Parent capability this extends. Present for extensions, absent for root capabilities. - */ - extends?: string; - /** - * Capability-specific configuration (structure defined by each capability). - */ - config?: { - [k: string]: unknown; - }; - [k: string]: unknown; -} -/** - * Line item object. Expected to use the currency of the parent object. - */ -export declare interface LineItemResponse { - id: string; - item: ItemResponse; - /** - * Quantity of the item being purchased. - */ - quantity: number; - /** - * Line item totals breakdown. - */ - totals: TotalResponse[]; - /** - * Parent line item identifier for any nested structures. - */ - parent_id?: string; - [k: string]: unknown; -} -export declare interface ItemResponse { - /** - * Should be recognized by both the Platform, and the Business. For Google it should match the id provided in the "id" field in the product feed. - */ - id: string; - /** - * Product title. - */ - title: string; - /** - * Unit price in minor (cents) currency units. - */ - price: number; - /** - * Product image URI. - */ - image_url?: string; - [k: string]: unknown; -} -export declare interface TotalResponse { - /** - * Type of total categorization. - */ - type: 'items_discount' | 'subtotal' | 'discount' | 'fulfillment' | 'tax' | 'fee' | 'total'; - /** - * Text to display against the amount. Should reflect appropriate method (e.g., 'Shipping', 'Delivery'). - */ - display_text?: string; - /** - * If type == total, sums subtotal - discount + fulfillment + tax + fee. Should be >= 0. Amount in minor (cents) currency units. - */ - amount: number; - [k: string]: unknown; -} -export declare interface MessageError { - /** - * Message type discriminator. - */ - type: 'error'; - /** - * Error code. Possible values include: missing, invalid, out_of_stock, payment_declined, requires_sign_in, requires_3ds, requires_identity_linking. Freeform codes also allowed. - */ - code: string; - /** - * RFC 9535 JSONPath to the component the message refers to (e.g., $.items[1]). - */ - path?: string; - /** - * Content format, default = plain. - */ - content_type?: 'plain' | 'markdown'; - /** - * Human-readable message. - */ - content: string; - /** - * Declares who resolves this error. 'recoverable': agent can fix via API. 'requires_buyer_input': merchant requires information their API doesn't support collecting programmatically (checkout incomplete). 'requires_buyer_review': buyer must authorize before order placement due to policy, regulatory, or entitlement rules (checkout complete). Errors with 'requires_*' severity contribute to 'status: requires_escalation'. - */ - severity: 'recoverable' | 'requires_buyer_input' | 'requires_buyer_review'; - [k: string]: unknown; -} -export declare interface MessageWarning { - /** - * Message type discriminator. - */ - type: 'warning'; - /** - * JSONPath (RFC 9535) to related field (e.g., $.line_items[0]). - */ - path?: string; - /** - * Warning code. Machine-readable identifier for the warning type (e.g., final_sale, prop65, fulfillment_changed, age_restricted, etc.). - */ - code: string; - /** - * Human-readable warning message that MUST be displayed. - */ - content: string; - /** - * Content format, default = plain. - */ - content_type?: 'plain' | 'markdown'; - [k: string]: unknown; -} -export declare interface MessageInfo { - /** - * Message type discriminator. - */ - type: 'info'; - /** - * RFC 9535 JSONPath to the component the message refers to. - */ - path?: string; - /** - * Info code for programmatic handling. - */ - code?: string; - /** - * Content format, default = plain. - */ - content_type?: 'plain' | 'markdown'; - /** - * Human-readable message. - */ - content: string; - [k: string]: unknown; -} -export declare interface Link { - /** - * Type of link. Well-known values: `privacy_policy`, `terms_of_service`, `refund_policy`, `shipping_policy`, `faq`. Consumers SHOULD handle unknown values gracefully by displaying them using the `title` field or omitting the link. - */ - type: string; - /** - * The actual URL pointing to the content to be displayed. - */ - url: string; - /** - * Optional display text for the link. When provided, use this instead of generating from type. - */ - title?: string; - [k: string]: unknown; -} -/** - * Payment configuration containing handlers. - */ -export declare interface PaymentResponse { - /** - * Processing configurations that define how payment instruments can be collected. Each handler specifies a tokenization or payment collection strategy. - */ - handlers: PaymentHandlerResponse[]; - /** - * The id of the currently selected payment instrument from the instruments array. Set by the agent when submitting payment, and echoed back by the merchant in finalized state. - */ - selected_instrument_id?: string; - /** - * The payment instruments available for this payment. Each instrument is associated with a specific handler via the handler_id field. Handlers can extend the base payment_instrument schema to add handler-specific fields. - */ - instruments?: PaymentInstrument[]; - [k: string]: unknown; -} -export declare interface PaymentHandlerResponse { - /** - * The unique identifier for this handler instance within the payment.handlers. Used by payment instruments to reference which handler produced them. - */ - id: string; - /** - * The specification name using reverse-DNS format. For example, dev.ucp.delegate_payment. - */ - name: string; - /** - * UCP protocol version in YYYY-MM-DD format. - */ - version: string; - /** - * A URI pointing to the technical specification or schema that defines how this handler operates. - */ - spec: string; - /** - * A URI pointing to a JSON Schema used to validate the structure of the config object. - */ - config_schema: string; - instrument_schemas: string[]; - /** - * A dictionary containing provider-specific configuration details, such as merchant IDs, supported networks, or gateway credentials. - */ - config: { - [k: string]: unknown; - }; - [k: string]: unknown; -} -/** - * Order details available at the time of checkout completion. - */ -export declare interface OrderConfirmation { - /** - * Unique order identifier. - */ - id: string; - /** - * Permalink to access the order on merchant site. - */ - permalink_url: string; - [k: string]: unknown; -} -/** - * Extends Checkout with buyer consent tracking for privacy compliance via the buyer object. - */ -export declare interface BuyerConsentExtensionCompleteRequest { - [k: string]: unknown; -} -/** - * User consent states for data processing - * - * This interface was referenced by `BuyerConsentExtensionCompleteRequest`'s JSON-Schema - * via the `definition` "consent". - */ -export declare interface Consent { - /** - * Consent for analytics and performance tracking. - */ - analytics?: boolean; - /** - * Consent for storing user preferences. - */ - preferences?: boolean; - /** - * Consent for marketing communications. - */ - marketing?: boolean; - /** - * Consent for selling data to third parties (CCPA). - */ - sale_of_data?: boolean; - [k: string]: unknown; -} -/** - * Extends Checkout with buyer consent tracking for privacy compliance via the buyer object. - */ -export declare interface BuyerConsentExtensionCreateRequest { - [k: string]: unknown; -} -/** - * User consent states for data processing - * - * This interface was referenced by `BuyerConsentExtensionCreateRequest`'s JSON-Schema - * via the `definition` "consent". - */ -export declare interface Consent1 { - /** - * Consent for analytics and performance tracking. - */ - analytics?: boolean; - /** - * Consent for storing user preferences. - */ - preferences?: boolean; - /** - * Consent for marketing communications. - */ - marketing?: boolean; - /** - * Consent for selling data to third parties (CCPA). - */ - sale_of_data?: boolean; - [k: string]: unknown; -} -/** - * Extends Checkout with buyer consent tracking for privacy compliance via the buyer object. - */ -export declare interface BuyerConsentExtensionUpdateRequest { - [k: string]: unknown; -} -/** - * User consent states for data processing - * - * This interface was referenced by `BuyerConsentExtensionUpdateRequest`'s JSON-Schema - * via the `definition` "consent". - */ -export declare interface Consent2 { - /** - * Consent for analytics and performance tracking. - */ - analytics?: boolean; - /** - * Consent for storing user preferences. - */ - preferences?: boolean; - /** - * Consent for marketing communications. - */ - marketing?: boolean; - /** - * Consent for selling data to third parties (CCPA). - */ - sale_of_data?: boolean; - [k: string]: unknown; -} -/** - * Extends Checkout with buyer consent tracking for privacy compliance via the buyer object. - */ -export declare interface BuyerConsentExtensionResponse { - [k: string]: unknown; -} -/** - * User consent states for data processing - * - * This interface was referenced by `BuyerConsentExtensionResponse`'s JSON-Schema - * via the `definition` "consent". - */ -export declare interface Consent3 { - /** - * Consent for analytics and performance tracking. - */ - analytics?: boolean; - /** - * Consent for storing user preferences. - */ - preferences?: boolean; - /** - * Consent for marketing communications. - */ - marketing?: boolean; - /** - * Consent for selling data to third parties (CCPA). - */ - sale_of_data?: boolean; - [k: string]: unknown; -} -/** - * Extends Checkout with discount code support, enabling agents to apply promotional, loyalty, referral, and other discount codes. - */ -export declare interface DiscountExtensionCompleteRequest { - [k: string]: unknown; -} -/** - * Breakdown of how a discount amount was allocated to a specific target. - * - * This interface was referenced by `DiscountExtensionCompleteRequest`'s JSON-Schema - * via the `definition` "allocation". - */ -export declare interface Allocation { - /** - * JSONPath to the allocation target (e.g., '$.line_items[0]', '$.totals.shipping'). - */ - path: string; - /** - * Amount allocated to this target in minor (cents) currency units. - */ - amount: number; - [k: string]: unknown; -} -/** - * A discount that was successfully applied. - * - * This interface was referenced by `DiscountExtensionCompleteRequest`'s JSON-Schema - * via the `definition` "applied_discount". - */ -export declare interface AppliedDiscount { - /** - * The discount code. Omitted for automatic discounts. - */ - code?: string; - /** - * Human-readable discount name (e.g., 'Summer Sale 20% Off'). - */ - title: string; - /** - * Total discount amount in minor (cents) currency units. - */ - amount: number; - /** - * True if applied automatically by merchant rules (no code required). - */ - automatic?: boolean; - /** - * Allocation method. 'each' = applied independently per item. 'across' = split proportionally by value. - */ - method?: 'each' | 'across'; - /** - * Stacking order for discount calculation. Lower numbers applied first (1 = first). - */ - priority?: number; - /** - * Breakdown of where this discount was allocated. Sum of allocation amounts equals total amount. - */ - allocations?: Allocation[]; - [k: string]: unknown; -} -/** - * Extends Checkout with discount code support, enabling agents to apply promotional, loyalty, referral, and other discount codes. - */ -export declare interface DiscountExtensionCreateRequest { - [k: string]: unknown; -} -/** - * Breakdown of how a discount amount was allocated to a specific target. - * - * This interface was referenced by `DiscountExtensionCreateRequest`'s JSON-Schema - * via the `definition` "allocation". - */ -export declare interface Allocation1 { - /** - * JSONPath to the allocation target (e.g., '$.line_items[0]', '$.totals.shipping'). - */ - path: string; - /** - * Amount allocated to this target in minor (cents) currency units. - */ - amount: number; - [k: string]: unknown; -} -/** - * A discount that was successfully applied. - * - * This interface was referenced by `DiscountExtensionCreateRequest`'s JSON-Schema - * via the `definition` "applied_discount". - */ -export declare interface AppliedDiscount1 { - /** - * The discount code. Omitted for automatic discounts. - */ - code?: string; - /** - * Human-readable discount name (e.g., 'Summer Sale 20% Off'). - */ - title: string; - /** - * Total discount amount in minor (cents) currency units. - */ - amount: number; - /** - * True if applied automatically by merchant rules (no code required). - */ - automatic?: boolean; - /** - * Allocation method. 'each' = applied independently per item. 'across' = split proportionally by value. - */ - method?: 'each' | 'across'; - /** - * Stacking order for discount calculation. Lower numbers applied first (1 = first). - */ - priority?: number; - /** - * Breakdown of where this discount was allocated. Sum of allocation amounts equals total amount. - */ - allocations?: Allocation1[]; - [k: string]: unknown; -} -/** - * Discount codes input and applied discounts output. - * - * This interface was referenced by `DiscountExtensionCreateRequest`'s JSON-Schema - * via the `definition` "discounts_object". - */ -export declare interface DiscountsObject { - /** - * Discount codes to apply. Case-insensitive. Replaces previously submitted codes. Send empty array to clear. - */ - codes?: string[]; - /** - * Discounts successfully applied (code-based and automatic). - */ - applied?: AppliedDiscount1[]; - [k: string]: unknown; -} -/** - * Extends Checkout with discount code support, enabling agents to apply promotional, loyalty, referral, and other discount codes. - */ -export declare interface DiscountExtensionUpdateRequest { - [k: string]: unknown; -} -/** - * Breakdown of how a discount amount was allocated to a specific target. - * - * This interface was referenced by `DiscountExtensionUpdateRequest`'s JSON-Schema - * via the `definition` "allocation". - */ -export declare interface Allocation2 { - /** - * JSONPath to the allocation target (e.g., '$.line_items[0]', '$.totals.shipping'). - */ - path: string; - /** - * Amount allocated to this target in minor (cents) currency units. - */ - amount: number; - [k: string]: unknown; -} -/** - * A discount that was successfully applied. - * - * This interface was referenced by `DiscountExtensionUpdateRequest`'s JSON-Schema - * via the `definition` "applied_discount". - */ -export declare interface AppliedDiscount2 { - /** - * The discount code. Omitted for automatic discounts. - */ - code?: string; - /** - * Human-readable discount name (e.g., 'Summer Sale 20% Off'). - */ - title: string; - /** - * Total discount amount in minor (cents) currency units. - */ - amount: number; - /** - * True if applied automatically by merchant rules (no code required). - */ - automatic?: boolean; - /** - * Allocation method. 'each' = applied independently per item. 'across' = split proportionally by value. - */ - method?: 'each' | 'across'; - /** - * Stacking order for discount calculation. Lower numbers applied first (1 = first). - */ - priority?: number; - /** - * Breakdown of where this discount was allocated. Sum of allocation amounts equals total amount. - */ - allocations?: Allocation2[]; - [k: string]: unknown; -} -/** - * Discount codes input and applied discounts output. - * - * This interface was referenced by `DiscountExtensionUpdateRequest`'s JSON-Schema - * via the `definition` "discounts_object". - */ -export declare interface DiscountsObject1 { - /** - * Discount codes to apply. Case-insensitive. Replaces previously submitted codes. Send empty array to clear. - */ - codes?: string[]; - /** - * Discounts successfully applied (code-based and automatic). - */ - applied?: AppliedDiscount2[]; - [k: string]: unknown; -} -/** - * Extends Checkout with discount code support, enabling agents to apply promotional, loyalty, referral, and other discount codes. - */ -export declare interface DiscountExtensionResponse { - [k: string]: unknown; -} -/** - * Breakdown of how a discount amount was allocated to a specific target. - * - * This interface was referenced by `DiscountExtensionResponse`'s JSON-Schema - * via the `definition` "allocation". - */ -export declare interface Allocation3 { - /** - * JSONPath to the allocation target (e.g., '$.line_items[0]', '$.totals.shipping'). - */ - path: string; - /** - * Amount allocated to this target in minor (cents) currency units. - */ - amount: number; - [k: string]: unknown; -} -/** - * A discount that was successfully applied. - * - * This interface was referenced by `DiscountExtensionResponse`'s JSON-Schema - * via the `definition` "applied_discount". - */ -export declare interface AppliedDiscount3 { - /** - * The discount code. Omitted for automatic discounts. - */ - code?: string; - /** - * Human-readable discount name (e.g., 'Summer Sale 20% Off'). - */ - title: string; - /** - * Total discount amount in minor (cents) currency units. - */ - amount: number; - /** - * True if applied automatically by merchant rules (no code required). - */ - automatic?: boolean; - /** - * Allocation method. 'each' = applied independently per item. 'across' = split proportionally by value. - */ - method?: 'each' | 'across'; - /** - * Stacking order for discount calculation. Lower numbers applied first (1 = first). - */ - priority?: number; - /** - * Breakdown of where this discount was allocated. Sum of allocation amounts equals total amount. - */ - allocations?: Allocation3[]; - [k: string]: unknown; -} -/** - * Discount codes input and applied discounts output. - * - * This interface was referenced by `DiscountExtensionResponse`'s JSON-Schema - * via the `definition` "discounts_object". - */ -export declare interface DiscountsObject2 { - /** - * Discount codes to apply. Case-insensitive. Replaces previously submitted codes. Send empty array to clear. - */ - codes?: string[]; - /** - * Discounts successfully applied (code-based and automatic). - */ - applied?: AppliedDiscount3[]; - [k: string]: unknown; -} -/** - * Extends Checkout with fulfillment support using methods, destinations, and groups. - */ -export declare interface FulfillmentExtensionCompleteRequest { - [k: string]: unknown; -} -/** - * A fulfillment option within a group (e.g., Standard Shipping $5, Express $15). - * - * This interface was referenced by `FulfillmentExtensionCompleteRequest`'s JSON-Schema - * via the `definition` "fulfillment_option". - * - * This interface was referenced by `FulfillmentExtensionCreateRequest`'s JSON-Schema - * via the `definition` "fulfillment_option". - * - * This interface was referenced by `FulfillmentExtensionUpdateRequest`'s JSON-Schema - * via the `definition` "fulfillment_option". - */ -export declare interface FulfillmentOptionRequest { - [k: string]: unknown; -} -/** - * A merchant-generated package/group of line items with fulfillment options. - * - * This interface was referenced by `FulfillmentExtensionCompleteRequest`'s JSON-Schema - * via the `definition` "fulfillment_group". - */ -export declare interface FulfillmentGroupCompleteRequest { - /** - * Group identifier for referencing merchant-generated groups in updates. - */ - id: string; - /** - * ID of the selected fulfillment option for this group. - */ - selected_option_id?: string | null; - [k: string]: unknown; -} -/** - * A fulfillment method (shipping or pickup) with destinations and groups. - * - * This interface was referenced by `FulfillmentExtensionCompleteRequest`'s JSON-Schema - * via the `definition` "fulfillment_method". - */ -export declare interface FulfillmentMethodCompleteRequest { - /** - * Unique fulfillment method identifier. - */ - id: string; - /** - * Fulfillment method type. - */ - type: 'shipping' | 'pickup'; - /** - * Line item IDs fulfilled via this method. - */ - line_item_ids: string[]; - /** - * Available destinations. For shipping: addresses. For pickup: retail locations. - */ - destinations?: FulfillmentDestinationRequest[]; - /** - * ID of the selected destination. - */ - selected_destination_id?: string | null; - /** - * Fulfillment groups for selecting options. Agent sets selected_option_id on groups to choose shipping method. - */ - groups?: FulfillmentGroupCompleteRequest[]; - [k: string]: unknown; -} -/** - * A pickup location (retail store, locker, etc.). - */ -export declare interface RetailLocationRequest { - /** - * Location name (e.g., store name). - */ - name: string; - address?: PostalAddress; - [k: string]: unknown; -} -/** - * Inventory availability hint for a fulfillment method type. - * - * This interface was referenced by `FulfillmentExtensionCompleteRequest`'s JSON-Schema - * via the `definition` "fulfillment_available_method". - * - * This interface was referenced by `FulfillmentExtensionCreateRequest`'s JSON-Schema - * via the `definition` "fulfillment_available_method". - * - * This interface was referenced by `FulfillmentExtensionUpdateRequest`'s JSON-Schema - * via the `definition` "fulfillment_available_method". - */ -export declare interface FulfillmentAvailableMethodRequest { - [k: string]: unknown; -} -/** - * Container for fulfillment methods and availability. - * - * This interface was referenced by `FulfillmentExtensionCompleteRequest`'s JSON-Schema - * via the `definition` "fulfillment". - * - * This interface was referenced by `FulfillmentExtensionCreateRequest`'s JSON-Schema - * via the `definition` "fulfillment". - * - * This interface was referenced by `FulfillmentExtensionUpdateRequest`'s JSON-Schema - * via the `definition` "fulfillment". - */ -export declare interface FulfillmentRequest { - /** - * Fulfillment methods for cart items. - */ - methods?: FulfillmentMethodCreateRequest[]; - [k: string]: unknown; -} -/** - * A fulfillment method (shipping or pickup) with destinations and groups. - * - * This interface was referenced by `FulfillmentExtensionCreateRequest`'s JSON-Schema - * via the `definition` "fulfillment_method". - */ -export declare interface FulfillmentMethodCreateRequest { - /** - * Fulfillment method type. - */ - type: 'shipping' | 'pickup'; - /** - * Line item IDs fulfilled via this method. - */ - line_item_ids?: string[]; - /** - * Available destinations. For shipping: addresses. For pickup: retail locations. - */ - destinations?: FulfillmentDestinationRequest[]; - /** - * ID of the selected destination. - */ - selected_destination_id?: string | null; - /** - * Fulfillment groups for selecting options. Agent sets selected_option_id on groups to choose shipping method. - */ - groups?: FulfillmentGroupCreateRequest[]; - [k: string]: unknown; -} -/** - * A merchant-generated package/group of line items with fulfillment options. - * - * This interface was referenced by `FulfillmentExtensionCreateRequest`'s JSON-Schema - * via the `definition` "fulfillment_group". - */ -export declare interface FulfillmentGroupCreateRequest { - /** - * ID of the selected fulfillment option for this group. - */ - selected_option_id?: string | null; - [k: string]: unknown; -} -/** - * Extends Checkout with fulfillment support using methods, destinations, and groups. - */ -export declare interface FulfillmentExtensionCreateRequest { - [k: string]: unknown; -} -/** - * Extends Checkout with fulfillment support using methods, destinations, and groups. - */ -export declare interface FulfillmentExtensionUpdateRequest { - [k: string]: unknown; -} -/** - * A merchant-generated package/group of line items with fulfillment options. - * - * This interface was referenced by `FulfillmentExtensionUpdateRequest`'s JSON-Schema - * via the `definition` "fulfillment_group". - */ -export declare interface FulfillmentGroupUpdateRequest { - /** - * Group identifier for referencing merchant-generated groups in updates. - */ - id: string; - /** - * ID of the selected fulfillment option for this group. - */ - selected_option_id?: string | null; - [k: string]: unknown; -} -/** - * A fulfillment method (shipping or pickup) with destinations and groups. - * - * This interface was referenced by `FulfillmentExtensionUpdateRequest`'s JSON-Schema - * via the `definition` "fulfillment_method". - */ -export declare interface FulfillmentMethodUpdateRequest { - /** - * Unique fulfillment method identifier. - */ - id: string; - /** - * Line item IDs fulfilled via this method. - */ - line_item_ids: string[]; - /** - * Available destinations. For shipping: addresses. For pickup: retail locations. - */ - destinations?: FulfillmentDestinationRequest[]; - /** - * ID of the selected destination. - */ - selected_destination_id?: string | null; - /** - * Fulfillment groups for selecting options. Agent sets selected_option_id on groups to choose shipping method. - */ - groups?: FulfillmentGroupUpdateRequest[]; - [k: string]: unknown; -} -/** - * Extends Checkout with fulfillment support using methods, destinations, and groups. - */ -export declare interface FulfillmentExtensionResponse { - [k: string]: unknown; -} -/** - * A fulfillment option within a group (e.g., Standard Shipping $5, Express $15). - * - * This interface was referenced by `FulfillmentExtensionResponse`'s JSON-Schema - * via the `definition` "fulfillment_option". - */ -export declare interface FulfillmentOptionResponse { - /** - * Unique fulfillment option identifier. - */ - id: string; - /** - * Short label (e.g., 'Express Shipping', 'Curbside Pickup'). - */ - title: string; - /** - * Complete context for buyer decision (e.g., 'Arrives Dec 12-15 via FedEx'). - */ - description?: string; - /** - * Carrier name (for shipping). - */ - carrier?: string; - /** - * Earliest fulfillment date. - */ - earliest_fulfillment_time?: string; - /** - * Latest fulfillment date. - */ - latest_fulfillment_time?: string; - /** - * Fulfillment option totals breakdown. - */ - totals: TotalResponse[]; - [k: string]: unknown; -} -/** - * A merchant-generated package/group of line items with fulfillment options. - * - * This interface was referenced by `FulfillmentExtensionResponse`'s JSON-Schema - * via the `definition` "fulfillment_group". - */ -export declare interface FulfillmentGroupResponse { - /** - * Group identifier for referencing merchant-generated groups in updates. - */ - id: string; - /** - * Line item IDs included in this group/package. - */ - line_item_ids: string[]; - /** - * Available fulfillment options for this group. - */ - options?: FulfillmentOptionResponse[]; - /** - * ID of the selected fulfillment option for this group. - */ - selected_option_id?: string | null; - [k: string]: unknown; -} -/** - * A fulfillment method (shipping or pickup) with destinations and groups. - * - * This interface was referenced by `FulfillmentExtensionResponse`'s JSON-Schema - * via the `definition` "fulfillment_method". - */ -export declare interface FulfillmentMethodResponse { - /** - * Unique fulfillment method identifier. - */ - id: string; - /** - * Fulfillment method type. - */ - type: 'shipping' | 'pickup'; - /** - * Line item IDs fulfilled via this method. - */ - line_item_ids: string[]; - /** - * Available destinations. For shipping: addresses. For pickup: retail locations. - */ - destinations?: FulfillmentDestinationResponse[]; - /** - * ID of the selected destination. - */ - selected_destination_id?: string | null; - /** - * Fulfillment groups for selecting options. Agent sets selected_option_id on groups to choose shipping method. - */ - groups?: FulfillmentGroupResponse[]; - [k: string]: unknown; -} -/** - * A pickup location (retail store, locker, etc.). - */ -export declare interface RetailLocationResponse { - /** - * Unique location identifier. - */ - id: string; - /** - * Location name (e.g., store name). - */ - name: string; - address?: PostalAddress; - [k: string]: unknown; -} -/** - * Inventory availability hint for a fulfillment method type. - * - * This interface was referenced by `FulfillmentExtensionResponse`'s JSON-Schema - * via the `definition` "fulfillment_available_method". - */ -export declare interface FulfillmentAvailableMethodResponse { - /** - * Fulfillment method type this availability applies to. - */ - type: 'shipping' | 'pickup'; - /** - * Line items available for this fulfillment method. - */ - line_item_ids: string[]; - /** - * 'now' for immediate availability, or ISO 8601 date for future (preorders, transfers). - */ - fulfillable_on?: string | null; - /** - * Human-readable availability info (e.g., 'Available for pickup at Downtown Store today'). - */ - description?: string; - [k: string]: unknown; -} -/** - * Container for fulfillment methods and availability. - * - * This interface was referenced by `FulfillmentExtensionResponse`'s JSON-Schema - * via the `definition` "fulfillment". - */ -export declare interface FulfillmentResponse { - /** - * Fulfillment methods for cart items. - */ - methods?: FulfillmentMethodResponse[]; - /** - * Inventory availability hints. - */ - available_methods?: FulfillmentAvailableMethodResponse[]; - [k: string]: unknown; -} -/** - * Order schema with immutable line items, buyer-facing fulfillment expectations, and append-only event logs. - */ -export declare interface Order { - ucp: UCPOrderResponse; - /** - * Unique order identifier. - */ - id: string; - /** - * Associated checkout ID for reconciliation. - */ - checkout_id: string; - /** - * Permalink to access the order on merchant site. - */ - permalink_url: string; - /** - * Immutable line items — source of truth for what was ordered. - */ - line_items: OrderLineItem[]; - /** - * Fulfillment data: buyer expectations and what actually happened. - */ - fulfillment: { - /** - * Buyer-facing groups representing when/how items will be delivered. Can be split, merged, or adjusted post-order. - */ - expectations?: Expectation[]; - /** - * Append-only event log of actual shipments. Each event references line items by ID. - */ - events?: FulfillmentEvent[]; - [k: string]: unknown; - }; - /** - * Append-only event log of money movements (refunds, returns, credits, disputes, cancellations, etc.) that exist independently of fulfillment. - */ - adjustments?: Adjustment[]; - /** - * Different totals for the order. - */ - totals: TotalResponse[]; - [k: string]: unknown; -} -/** - * UCP metadata for order responses. No payment handlers needed post-purchase. - */ -export declare interface UCPOrderResponse { - /** - * UCP protocol version in YYYY-MM-DD format. - */ - version: string; - /** - * Active capabilities for this response. - */ - capabilities: CapabilityResponse[]; - [k: string]: unknown; -} -export declare interface OrderLineItem { - /** - * Line item identifier. - */ - id: string; - item: ItemResponse; - /** - * Quantity tracking. Both total and fulfilled are derived from events. - */ - quantity: { - /** - * Current total quantity. - */ - total: number; - /** - * Quantity fulfilled (sum from fulfillment events). - */ - fulfilled: number; - [k: string]: unknown; - }; - /** - * Line item totals breakdown. - */ - totals: TotalResponse[]; - /** - * Derived status: fulfilled if quantity.fulfilled == quantity.total, partial if quantity.fulfilled > 0, otherwise processing. - */ - status: 'processing' | 'partial' | 'fulfilled'; - /** - * Parent line item identifier for any nested structures. - */ - parent_id?: string; - [k: string]: unknown; -} -/** - * Buyer-facing fulfillment expectation representing logical groupings of items (e.g., 'package'). Can be split, merged, or adjusted post-order to set buyer expectations for when/how items arrive. - */ -export declare interface Expectation { - /** - * Expectation identifier. - */ - id: string; - /** - * Which line items and quantities are in this expectation. - */ - line_items: Array<{ - /** - * Line item ID reference. - */ - id: string; - /** - * Quantity of this item in this expectation. - */ - quantity: number; - [k: string]: unknown; - }>; - /** - * Delivery method type (shipping, pickup, digital). - */ - method_type: 'shipping' | 'pickup' | 'digital'; - destination: PostalAddress; - /** - * Human-readable delivery description (e.g., 'Arrives in 5-8 business days'). - */ - description?: string; - /** - * When this expectation can be fulfilled: 'now' or ISO 8601 timestamp for future date (backorder, pre-order). - */ - fulfillable_on?: string; - [k: string]: unknown; -} -/** - * Append-only fulfillment event representing an actual shipment. References line items by ID. - */ -export declare interface FulfillmentEvent { - /** - * Fulfillment event identifier. - */ - id: string; - /** - * RFC 3339 timestamp when this fulfillment event occurred. - */ - occurred_at: string; - /** - * Fulfillment event type. Common values include: processing (preparing to ship), shipped (handed to carrier), in_transit (in delivery network), delivered (received by buyer), failed_attempt (delivery attempt failed), canceled (fulfillment canceled), undeliverable (cannot be delivered), returned_to_sender (returned to merchant). - */ - type: string; - /** - * Which line items and quantities are fulfilled in this event. - */ - line_items: Array<{ - /** - * Line item ID reference. - */ - id: string; - /** - * Quantity fulfilled in this event. - */ - quantity: number; - [k: string]: unknown; - }>; - /** - * Carrier tracking number (required if type != processing). - */ - tracking_number?: string; - /** - * URL to track this shipment (required if type != processing). - */ - tracking_url?: string; - /** - * Carrier name (e.g., 'FedEx', 'USPS'). - */ - carrier?: string; - /** - * Human-readable description of the shipment status or delivery information (e.g., 'Delivered to front door', 'Out for delivery'). - */ - description?: string; - [k: string]: unknown; -} -/** - * Append-only event that exists independently of fulfillment. Typically represents money movements but can be any post-order change. Polymorphic type that can optionally reference line items. - */ -export declare interface Adjustment { - /** - * Adjustment event identifier. - */ - id: string; - /** - * Type of adjustment (open string). Typically money-related like: refund, return, credit, price_adjustment, dispute, cancellation. Can be any value that makes sense for the merchant's business. - */ - type: string; - /** - * RFC 3339 timestamp when this adjustment occurred. - */ - occurred_at: string; - /** - * Adjustment status. - */ - status: 'pending' | 'completed' | 'failed'; - /** - * Which line items and quantities are affected (optional). - */ - line_items?: Array<{ - /** - * Line item ID reference. - */ - id: string; - /** - * Quantity affected by this adjustment. - */ - quantity: number; - [k: string]: unknown; - }>; - /** - * Amount in minor units (cents) for refunds, credits, price adjustments (optional). - */ - amount?: number; - /** - * Human-readable reason or description (e.g., 'Defective item', 'Customer requested'). - */ - description?: string; - [k: string]: unknown; -} -/** - * Platform's order capability configuration. - * - * This interface was referenced by `Order`'s JSON-Schema - * via the `definition` "platform_config". - */ -export declare interface PlatformOrderConfig { - /** - * URL where merchant sends order lifecycle events (webhooks). - */ - webhook_url: string; - [k: string]: unknown; -} diff --git a/hooks.py b/hooks.py index 963476ca6..e24eecd89 100644 --- a/hooks.py +++ b/hooks.py @@ -168,6 +168,24 @@ def on_config(config): # --- Adjust Nav (Config Phase) --- # Modifying config['nav'] prevents validation errors for missing files. if "nav" in config: + # Support subpath deployments for absolute nav links + def rewrite_nav(nav_list): + rewritten = [] + for item in nav_list: + if isinstance(item, dict): + for k, v in item.items(): + if isinstance(v, list): + item[k] = rewrite_nav(v) + elif isinstance(v, str) and v.startswith("/latest/"): + # Rewrite absolute /latest/... links to respect base_path + item[k] = f"{base_path}{v[1:]}" + elif isinstance(item, str) and item.startswith("/latest/"): + item = f"{base_path}{item[1:]}" + rewritten.append(item) + return rewritten + + config["nav"] = rewrite_nav(config["nav"]) + new_nav = [] for item in config["nav"]: # Nav items are usually dicts {Title: path/content} or strings @@ -281,20 +299,30 @@ def replace_link(match): # Rewrite relative links to assets/ to absolute URLs # pointing to served assets folder. - target_base = f"{base_path}assets/" + markdown = _root_pages_asset_link_rewrite(markdown, base_path) - def replace_asset_link(match): - path = match.group(1) - output = f"{target_base}{path}" - log.info(f"on_page_markdown::replace_asset_link: {path} -> {output}") - return output + return markdown - # Pattern matches: ( prefix assets/ path ) - # We capture the path AFTER assets/ - # Matches: (../assets/foo.img) or (assets/foo.img) - pattern = r"\"(?:(?:\.\./)+|\./)?assets/([^)\"]+)\"" - markdown = re.sub(pattern, replace_asset_link, markdown) +def _root_pages_asset_link_rewrite(markdown, base_path): + """Rewrite asset references in the root/overview to absolute links. + + Uses regex to find and replace asset links with root based links. + """ + # Targeting the assets + target_base = f"{base_path}assets/" + + def replace_link(match): + path = match.group(1) + # Including quotes back into the rendered new URL + output = f'"{target_base}{path}"' + return output + + # Pattern matches: ( prefix assets/ path ) + # We capture the path AFTER assets/ + # Matches: (../assets/foo.img) or (assets/foo.img) excluding quotes + pattern = r"\"(?:(?:\.\./)+|\./)?assets/([^)\"]+)\"" + markdown = re.sub(pattern, replace_link, markdown) return markdown diff --git a/main.py b/main.py index 6441f7c47..642305daa 100644 --- a/main.py +++ b/main.py @@ -281,7 +281,8 @@ def create_link(ref_string, spec_file_name, context=None): Args: ---- - ref_string: e.g., "types/line_item.create_req.json" + ref_string: e.g., "types/line_item.create_req.json" or + "types/pagination.json#/$defs/response" spec_file_name: e.g., "checkout" context: Optional dict with 'io_type' (request/response) for polymorphic type handling. @@ -300,7 +301,26 @@ def create_link(ref_string, spec_file_name, context=None): ): spec_file_name = "checkout" - filename = Path(ref_string).name + # Extract fragment identifier if present (e.g., #/$defs/response) + # This handles cases like "types/pagination.json#/$defs/response" + fragment = None + ref_path = ref_string + if "#/$defs/" in ref_string: + ref_path, fragment = ref_string.split("#/$defs/", 1) + + # Redirect all types/ references to the reference specification + if ref_string.startswith("types/"): + spec_file_name = "reference" + + # Redirect sibling refs that are types (e.g. "item.json" in + # types/order_line_item.json) + elif "/" not in ref_string and ref_string.endswith(".json"): + type_path = Path("source/schemas/shopping/types") / ref_string + shopping_path = Path("source/schemas/shopping") / ref_string + if type_path.exists() and not shopping_path.exists(): + spec_file_name = "reference" + + filename = Path(ref_path).name # Check if this reference comes from the core UCP schema is_ucp = "ucp.json" in ref_string @@ -312,9 +332,20 @@ def create_link(ref_string, spec_file_name, context=None): # 2. Generate Link Text (Visual) # e.g. "checkout_response" -> "Checkout Response" - link_text = ( - raw_name.replace("_", " ").replace(".", " ").replace("-", " ").title() - ) + # e.g. "pagination" + fragment "response" -> "Pagination Response" + if fragment: + base_text = ( + raw_name.replace("_", " ").replace(".", " ").replace("-", " ").title() + ) + fragment_text = ( + fragment.replace("_", " ").replace(".", " ").replace("-", " ").title() + ) + link_text = f"{base_text} {fragment_text}" + else: + link_text = ( + raw_name.replace("_", " ").replace(".", " ").replace("-", " ").title() + ) + if link_text.endswith("Resp"): link_text = link_text.replace("Resp", "Response") elif link_text.endswith("Req"): @@ -327,14 +358,20 @@ def create_link(ref_string, spec_file_name, context=None): # 3. Generate Anchor (Target) # We want "types/line_item.create_req.json" -> "#line-item-create_request" # This matches the pattern: "Line Item" H3 -> "Create Request" H4 - - # 3. Generate Anchor (Target) parts = raw_name.split(".") base_entity = parts[0] anchor_name = base_entity.replace("_", "-") - if len(parts) > 1: + # Handle fragment in anchor + # e.g., pagination#/$defs/response -> pagination-response + if fragment: + fragment_anchor = fragment.replace("_", "-") + if anchor_name: # External ref: base-fragment + anchor_name = f"{anchor_name}-{fragment_anchor}" + else: # Internal ref like #/$defs/context: just use fragment + anchor_name = fragment_anchor + elif len(parts) > 1: variant = parts[1] variant_expanded = ( variant.replace("create_req", "create-request") @@ -348,12 +385,12 @@ def create_link(ref_string, spec_file_name, context=None): elif raw_name.endswith("_req"): anchor_name = raw_name.replace("_", "-").replace("-req", "-request") elif context and context.get("io_type") == "response": - # For polymorphic types in response mode, append -response to match - # markdown headings like "Line Item Response" (h4 under "Line Item" h3) - if _is_polymorphic_type(ref_string): - anchor_name = f"{anchor_name}-response" - if not link_text.endswith("Response"): - link_text = f"{link_text} Response" + # For polymorphic types in response mode, keep the base anchor name to + # match markdown headings like "Line Item" instead of "Line Item Response" + if _is_polymorphic_type(ref_string) and not link_text.endswith( + "Response" + ): + link_text = f"{link_text} Response" # FIX: Ensure anchor starts with ucp- for UCP definitions if is_ucp and not anchor_name.startswith("ucp-"): @@ -413,7 +450,8 @@ def _render_table_from_ref( return f"_See [{properties_ref}]({properties_ref})_" # ucp-schema failed or schema not found - fail loudly raise RuntimeError( - f"Failed to resolve '{ref_entity_name}'{get_error_context()}. " + f"Failed to resolve ref_entity_name='{ref_entity_name}' " + f"from properties_ref='{properties_ref}' {get_error_context()}. " f"Ensure ucp-schema is installed: `cargo install ucp-schema`" ) @@ -721,7 +759,7 @@ def _read_schema_from_defs( if "allOf" in embedded_schema_data: new_all_of = [] for item in embedded_schema_data["allOf"]: - if "$ref" in item and item["$ref"].startswith("#/"): + if "$ref" in item and item["$ref"].startswith("#"): resolved = _resolve_json_pointer(item["$ref"], bundled) new_all_of.append(resolved if resolved else item) else: @@ -886,16 +924,6 @@ def auto_generate_schema_reference( if not is_extension and not include_capability: continue - # If a schema has no structural elements worth documenting here, - # skip it. - if ( - not schema_data.get("properties") - and not schema_data.get("allOf") - and not schema_data.get("oneOf") - and not schema_data.get("$ref") - and not schema_data.get("$defs") - ): - continue schema_title = schema_data.get( "title", entity_name_base.replace("_", " ").title() ) diff --git a/mkdocs.yml b/mkdocs.yml index b9567b1d6..563de73a4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -30,7 +30,7 @@ nav: - Overview: - Home: index.md - Core Concepts: documentation/core-concepts.md - - Specification: /latest/specification/overview + - Specification: /latest/specification/overview/ - UCP and AP2: documentation/ucp-and-ap2.md - Roadmap: documentation/roadmap.md - Schema Authoring: documentation/schema-authoring.md @@ -51,6 +51,12 @@ nav: - Overview: specification/cart.md - HTTP/REST Binding: specification/cart-rest.md - MCP Binding: specification/cart-mcp.md + - Catalog Capability: + - Overview: specification/catalog/index.md + - Search: specification/catalog/search.md + - Lookup: specification/catalog/lookup.md + - HTTP/REST Binding: specification/catalog/rest.md + - MCP Binding: specification/catalog/mcp.md - Order Capability: specification/order.md - Identity Linking Capability: specification/identity-linking.md - Payment Handlers: @@ -219,6 +225,13 @@ plugins: - specification/cart.md - specification/cart-rest.md - specification/cart-mcp.md + Catalog Capability: + - specification/catalog/index.md + - specification/catalog/search.md + - specification/catalog/lookup.md + - specification/catalog/rest.md + - specification/catalog/mcp.md + Other Capabilities: - specification/order.md - specification/identity-linking.md - specification/payment-handler-guide.md diff --git a/pyproject.toml b/pyproject.toml index c257e351f..44bfe0911 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ dev = [ "mike==2.1.3", "mkdocs-llmstxt==0.5.0", "mkdocs-macros-plugin==1.4.0", - "mkdocs-material[imaging]>=9.0.0", + "mkdocs-material[imaging]>=9.5.0", "mkdocs-redirects==1.2.2", "mkdocs-site-urls==0.3.1", "pyyaml>=6.0.3", diff --git a/scripts/build_local.sh b/scripts/build_local.sh index b9dade681..b53a93928 100755 --- a/scripts/build_local.sh +++ b/scripts/build_local.sh @@ -52,61 +52,66 @@ fi echo "Using Mike: $(which mike)" +MAIN_ONLY=false +if [[ "$1" == "--main-only" ]]; then + MAIN_ONLY=true + echo "Running in MAIN_ONLY mode. Skipping release branches." +fi + echo "=== Setup ===" rm -rf "$OUTPUT_DIR" echo "=== Syncing Release Branches ===" -git fetch origin -# Sync local gh-pages with remote to avoid divergence errors -git branch -f gh-pages origin/gh-pages 2>/dev/null || true +if [ "$MAIN_ONLY" = false ]; then + git fetch origin + # Sync local gh-pages with remote to avoid divergence errors + git branch -f gh-pages origin/gh-pages 2>/dev/null || true -# Find all release branches (format: release/YYYY-MM-DD) -RELEASE_BRANCHES=$(git branch -r | grep "origin/release/[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}" | sed 's/ *origin\///') + # Find all release branches (both local and remote, format: release/YYYY-MM-DD) + RELEASE_BRANCHES=$(git branch -a | grep -E "(remotes/origin/)?release/[0-9]{4}-[0-9]{2}-[0-9]{2}" | sed -E 's|.*(release/[0-9]{4}-[0-9]{2}-[0-9]{2}).*|\1|' | sort -u) -echo "Found branches: $RELEASE_BRANCHES" + echo "Found branches: $RELEASE_BRANCHES" +fi # List of folders we want to extract later EXTRACT_LIST="draft latest versions.json" -for branch in $RELEASE_BRANCHES; do - version=$(echo "$branch" | sed 's/release\///') - echo ">>> Rebuilding Version: $version (from $branch)" - EXTRACT_LIST="$EXTRACT_LIST $version" +if [ "$MAIN_ONLY" = false ]; then + for branch in $RELEASE_BRANCHES; do + version=$(echo "$branch" | sed 's/release\///') - rm -rf "$WORKTREE_DIR" - git worktree prune - git worktree add -f "$WORKTREE_DIR" "origin/$branch" + if git show-ref --verify --quiet "refs/heads/$branch"; then + TREE_REF="$branch" + echo ">>> Rebuilding Version: $version (from local $branch)" + else + TREE_REF="origin/$branch" + echo ">>> Rebuilding Version: $version (from origin/$branch)" + fi - pushd "$WORKTREE_DIR" >/dev/null + EXTRACT_LIST="$EXTRACT_LIST $version" - # Copy latest hooks from project root (main) to ensure consistent build logic - cp "$PROJECT_ROOT/hooks.py" . + rm -rf "$WORKTREE_DIR" + git worktree prune + git worktree add -f "$WORKTREE_DIR" "$TREE_REF" - # Deploy - # mike will now use the mkdocs in PATH (which is the root venv) - export DOCS_MODE=spec - export UCP_BUILD_VERSION="$version" - mike deploy "$version" + pushd "$WORKTREE_DIR" >/dev/null - popd >/dev/null - git worktree remove -f "$WORKTREE_DIR" -done + # Deploy + # mike will now use the mkdocs in PATH (which is the root venv) + export DOCS_MODE=spec + export UCP_BUILD_VERSION="$version" + mike deploy "$version" + + popd >/dev/null + git worktree remove -f "$WORKTREE_DIR" + done +fi echo ">>> Building Current Version (Draft & Latest)" export DOCS_MODE=spec export UCP_BUILD_VERSION="draft" mike deploy draft -LATEST_VERSION=$(echo "$RELEASE_BRANCHES" | sed 's/release\///' | sort -r | head -n 1) - -if [ -n "$LATEST_VERSION" ]; then - echo "Aliasing 'latest' to $LATEST_VERSION" - mike alias "$LATEST_VERSION" latest --update-aliases -else - echo "No release found, aliasing latest to draft" - mike alias draft latest --update-aliases -fi - echo ">>> Building Root Site" # Build root site FIRST so we establish the base (index.html, etc.) # Run in sub-shell or unset variable to be safe, though later steps don't use it diff --git a/scripts/check_links.py b/scripts/check_links.py new file mode 100644 index 000000000..f777edf62 --- /dev/null +++ b/scripts/check_links.py @@ -0,0 +1,262 @@ +"""Script to check for broken internal links and anchors in the built site.""" + +import os +import sys +import re +from html.parser import HTMLParser +from urllib.parse import urlparse, unquote +from pathlib import Path +from collections import defaultdict + +# Configuration +ROOT_DIR = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("local_preview") +SITE_URL = os.environ.get("SITE_URL", "https://ucp.dev/") + +# Ensure trailing slash for site url to match correctly +if not SITE_URL.endswith("/"): + SITE_URL += "/" +SITE_BASE_PATH = urlparse(SITE_URL).path +if SITE_BASE_PATH == "": + SITE_BASE_PATH = "/" + + +class LinkParser(HTMLParser): + """Parses HTML to extract links and id attributes.""" + + def __init__(self): + """Initialize the LinkParser.""" + super().__init__() + self.links = [] + self.ids = set() + self.is_ignoring_links = False + + def handle_comment(self, data): + """Detect comments instructing to ignore links.""" + if "ignore-link-begin" in data: + self.is_ignoring_links = True + elif "ignore-link-end" in data: + self.is_ignoring_links = False + + def handle_starttag(self, tag, attrs): + """Extract href from anchor tags and id/name attributes from all tags.""" + attrs_dict = dict(attrs) + if tag == "a" and "href" in attrs_dict: + href = attrs_dict["href"] + if ( + not self.is_ignoring_links + and not href.endswith("...") + and not href.endswith("*") + ): + self.links.append(href) + + # Collect IDs for anchor validation + if "id" in attrs_dict: + self.ids.add(attrs_dict["id"]) + if "name" in attrs_dict: # Old style anchors + self.ids.add(attrs_dict["name"]) + + def handle_data(self, data): + """Extract bare ucp.dev URLs from text content.""" + if self.is_ignoring_links: + return + + # Find anything that looks like https://ucp.dev/... in the text + urls = re.findall(r"https://ucp\.dev[^\s\"\'<>]*", data) + for url in urls: + if url.endswith("...") or url.endswith("*"): + continue + if url not in self.links: + self.links.append(url) + + +def check_links(): + """Scan the built documentation site for broken links and anchors.""" + if not ROOT_DIR.exists(): + print( + f"Error: {ROOT_DIR} does not exist. Run build_local.sh (local) " + "or mkdocs build (CI) first." + ) + sys.exit(1) + + ignore_patterns = [] + if Path(".linkignore").exists(): + try: + with Path.open(".linkignore", "r", encoding="utf-8") as f: + for line in f: + line = line.strip() + if line and not line.startswith("#"): + try: + ignore_patterns.append(re.compile(line)) + except re.error as e: + print(f"Warning: Invalid regex in .linkignore '{line}': {e}") + except Exception as e: + print(f"Warning: Could not read .linkignore: {e}") + + print(f"Scanning {ROOT_DIR} for broken links (Site URL: {SITE_URL})...") + + html_files = list(ROOT_DIR.rglob("*.html")) + file_cache = {} # Cache parsed IDs for each file to avoid re-parsing + # Structure: errors_by_version[version][file_path] = [list of error details] + errors_by_version = defaultdict(lambda: defaultdict(list)) + + def get_file_ids(path): + if path in file_cache: + return file_cache[path] + + if not path.exists(): + return None + + try: + content = path.read_text(encoding="utf-8") + parser = LinkParser() + parser.feed(content) + file_cache[path] = parser.ids + return parser.ids + except Exception: + # print(f"Failed to parse {path}: {e}") # Reduce noise + return None + + for file_path in html_files: + try: + rel_path = file_path.relative_to(ROOT_DIR) + first_part = rel_path.parts[0] + + # Heuristic for version detection + is_version = False + if first_part in ["draft", "latest"] or re.match( + r"^\d{4}-\d{2}-\d{2}$", first_part + ): + is_version = True + + version = first_part if is_version else "root" + except Exception: + version = "unknown" + + try: + content = file_path.read_text(encoding="utf-8") + except Exception as e: + errors_by_version[version][str(file_path)].append( + f" Could not read file: {e}" + ) + continue + + parser = LinkParser() + parser.feed(content) + file_cache[file_path] = parser.ids + + for link in parser.links: + original_link = link + + should_ignore = False + for pattern in ignore_patterns: + if pattern.search(original_link): + should_ignore = True + break + if should_ignore: + continue + + # Ignore external links + if link.startswith(("mailto:", "tel:", "javascript:", "data:")): + continue + + parsed = urlparse(link) + if parsed.scheme and parsed.scheme in ("http", "https"): + if not link.startswith(SITE_URL): + continue # External link + # Internal absolute URL (e.g. https://ucp.dev/foo) -> /foo + link = link[len(SITE_URL) - 1 :] # Keep the leading slash + + path_part = parsed.path + anchor_part = parsed.fragment + path_part = unquote(path_part) + + # If the path starts with the SITE_BASE_PATH (e.g. /ucp/), strip it + # so it resolves correctly against the local ROOT_DIR. + if SITE_BASE_PATH != "/" and path_part.startswith(SITE_BASE_PATH): + path_part = "/" + path_part[len(SITE_BASE_PATH) :] + + target_file = None + + # Resolve Target File + if not path_part: + target_file = file_path + elif path_part.startswith("/"): + # Absolute path from root + rel_path = path_part[1:] + parts = rel_path.split("/", 1) + + # If the path starts with a version identifier (latest, draft, or date) + # and if that directory does NOT exist at the root, we are likely + # scanning a single isolated build. In this case, strip the prefix to + # test against the flat structure. + if ( + len(parts) > 1 + and ( + parts[0] in ["latest", "draft"] + or re.match(r"^\d{4}-\d{2}-\d{2}$", parts[0]) + ) + and not (ROOT_DIR / parts[0]).exists() + ): + rel_path = parts[1] + + target_file = ROOT_DIR / rel_path + else: + # Relative path + target_file = file_path.parent / path_part + + # Handle directory targets + if target_file.is_dir() or path_part.endswith("/"): + target_file = target_file / "index.html" + + # Check Existence + if not target_file.exists(): + # Allow for cases where /foo points to /foo.html + if not path_part.endswith("/") and not target_file.name.endswith( + ".html" + ): + candidate = target_file.with_name(target_file.name + ".html") + if candidate.exists(): + target_file = candidate + else: + errors_by_version[version][str(file_path)].append( + f" Link: {original_link}\n Target: {target_file} (Not Found)" + ) + continue + else: + errors_by_version[version][str(file_path)].append( + f" Link: {original_link}\n Target: {target_file} (Not Found)" + ) + continue + + # Check Anchor + if anchor_part and not target_file.name.endswith(".json"): + ids = get_file_ids(target_file) + if ids is None: + continue + + if anchor_part not in ids: + errors_by_version[version][str(file_path)].append( + f" Link: {original_link}\n" + f" Target: {target_file}#{anchor_part} (Anchor not found)" + ) + + if errors_by_version: + total_errors = sum( + sum(len(errs) for errs in files.values()) + for files in errors_by_version.values() + ) + print(f"\nFound {total_errors} broken links:") + + for version in sorted(errors_by_version.keys()): + print(f"\n=== Version: {version} ===") + for file_path, errors in sorted(errors_by_version[version].items()): + print(f"Issues in {file_path}:") + for e in errors: + print(e) + sys.exit(1) + else: + print("All internal links validated successfully.") + + +if __name__ == "__main__": + check_links() diff --git a/source/handlers/tokenization/openapi.json b/source/handlers/tokenization/openapi.json index 4e54ebc6d..3a5f29859 100644 --- a/source/handlers/tokenization/openapi.json +++ b/source/handlers/tokenization/openapi.json @@ -2,7 +2,7 @@ "openapi": "3.1.0", "info": { "title": "Tokenization API", - "version": "2026-01-11", + "version": "draft", "description": "Shared API for tokenization payment handlers. Tokenizer services implement these endpoints to enable secure credential exchange. See the Tokenization Guide for implementation details." }, "paths": { diff --git a/source/schemas/common/identity_linking.json b/source/schemas/common/identity_linking.json new file mode 100644 index 000000000..d58007dad --- /dev/null +++ b/source/schemas/common/identity_linking.json @@ -0,0 +1,86 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/{{ ucp_version }}/schemas/common/identity_linking.json", + "name": "dev.ucp.common.identity_linking", + "version": "{{ ucp_version }}", + "title": "Identity Linking Capability", + "description": "Schema for authenticating and establishing verified connections between platforms and businesses.", + "$comment": "This schema also owns the 'identity_scopes' annotation convention. Any UCP capability schema MAY declare a top-level 'identity_scopes' array listing the OAuth 2.0 scopes required to operate that capability. During UCP discovery, platforms MUST collect identity_scopes from all capabilities in the finalized intersection and use the union as the authorization scope set. Absence of identity_scopes means the capability requires no dedicated scope. The canonical shape and pattern for this annotation is defined in $defs/identity_scopes.", + + "$defs": { + "platform_schema": { + "allOf": [{ "$ref": "../capability.json#/$defs/platform_schema" }] + }, + + "business_schema": { + "allOf": [ + { "$ref": "../capability.json#/$defs/business_schema" }, + { + "type": "object", + "properties": { + "config": { + "type": "object", + "properties": { + "supported_mechanisms": { + "type": "array", + "items": { "$ref": "#/$defs/mechanism" }, + "minItems": 1 + } + }, + "required": ["supported_mechanisms"] + } + }, + "required": ["config"] + } + ] + }, + + "identity_scopes": { + "title": "Identity Scopes Annotation", + "description": "A custom UCP annotation placed at the root of a capability schema to declare the OAuth 2.0 scopes required to operate that capability. Scopes MUST use reverse DNS dot notation to prevent namespace collisions (e.g., 'dev.ucp.shopping.scopes.checkout_session' for UCP-defined scopes, 'com.example.scopes.my_capability' for third-party scopes). Platforms collect this annotation from every capability in the finalized negotiated intersection and use the union as the authorization scope set. This annotation is intentionally a plain JSON array so it is ignored by standard JSON Schema validators — it carries semantic meaning only to UCP-aware tooling. Absence of this annotation on a capability schema means that capability requires no dedicated scope.", + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9_\\-]*(\\.[a-zA-Z0-9][a-zA-Z0-9_\\-]*)*\\.scopes\\.[a-zA-Z0-9][a-zA-Z0-9_\\-]*(\\.[a-zA-Z0-9][a-zA-Z0-9_\\-]*)*$" + }, + "uniqueItems": true, + "minItems": 1 + }, + + "mechanism": { + "type": "object", + "description": "Base definition for any authentication mechanism. The 'type' field discriminates between known mechanism schemas (e.g., oauth2). Unknown types pass through with only the base requirement, enabling forward-compatible extensibility. Note: this open base schema does not enforce field requirements for known types — use $defs/oauth2 directly to validate an oauth2 mechanism object explicitly.", + "required": ["type"], + "properties": { + "type": { + "type": "string", + "description": "The mechanism type discriminator. Known values: 'oauth2'. Specific mechanism schemas constrain this to a constant value." + } + }, + "additionalProperties": true + }, + + "oauth2": { + "type": "object", + "title": "OAuth 2.0 Mechanism", + "required": ["type", "issuer"], + "properties": { + "type": { + "const": "oauth2", + "description": "OAuth 2.0 authentication mechanism." + }, + "issuer": { + "type": "string", + "format": "uri", + "description": "The authorization server URL, supporting RFC 8414 discovery." + }, + "discovery_endpoint": { + "type": "string", + "format": "uri", + "description": "Optional explicit URI to the authorization server's metadata (e.g., `https://auth.merchant.example.com/.well-known/openid-configuration`). If omitted, platforms construct discovery paths based on the `issuer`." + } + }, + "additionalProperties": true + } + } +} diff --git a/source/schemas/shopping/cart.json b/source/schemas/shopping/cart.json index de3ff3203..875241985 100644 --- a/source/schemas/shopping/cart.json +++ b/source/schemas/shopping/cart.json @@ -3,7 +3,7 @@ "$id": "https://ucp.dev/schemas/shopping/cart.json", "name": "dev.ucp.shopping.cart", "title": "Cart", - "description": "Shopping cart with estimated pricing before checkout. Lightweight pre-purchase exploration with no payment info or complex status states. Cart exists (200) or doesn't (404).", + "description": "Shopping cart with estimated pricing before checkout. Lightweight pre-purchase exploration with no payment info or complex status states.", "$defs": { "checkout": { "title": "Checkout with Cart", @@ -69,6 +69,13 @@ "update": "optional" } }, + "signals": { + "$ref": "types/signals.json", + "ucp_request": { + "create": "optional", + "update": "optional" + } + }, "buyer": { "$ref": "types/buyer.json", "description": "Optional buyer information for personalized estimates.", @@ -83,10 +90,7 @@ "ucp_request": "omit" }, "totals": { - "type": "array", - "items": { - "$ref": "types/total.json" - }, + "$ref": "types/totals.json", "description": "Estimated cost breakdown. May be partial if shipping/tax not yet calculable.", "ucp_request": "omit" }, diff --git a/source/schemas/shopping/catalog_lookup.json b/source/schemas/shopping/catalog_lookup.json new file mode 100644 index 000000000..6642b01db --- /dev/null +++ b/source/schemas/shopping/catalog_lookup.json @@ -0,0 +1,81 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/catalog_lookup.json", + "name": "dev.ucp.shopping.catalog.lookup", + "title": "Catalog Lookup", + "description": "Product/variant lookup by identifier capability.", + "type": "object", + "$defs": { + "lookup_variant": { + "description": "Variant with required correlation metadata for lookup responses.", + "allOf": [ + { "$ref": "types/variant.json" }, + { + "required": ["inputs"], + "properties": { + "inputs": { + "type": "array", + "items": { "$ref": "types/input_correlation.json" }, + "minItems": 1, + "description": "Which request identifiers resolved to this variant, and how. Each entry maps a request ID to its match type." + } + } + } + ] + }, + "lookup_request": { + "type": "object", + "description": "Request body for catalog lookup.", + "required": ["ids"], + "properties": { + "ids": { + "type": "array", + "items": { "type": "string" }, + "minItems": 1, + "description": "Identifiers to lookup. Implementations MUST support product ID and variant ID; MAY support secondary identifiers (SKU, handle, etc.)." + }, + "context": { + "$ref": "types/context.json" + }, + "signals": { + "$ref": "types/signals.json" + } + } + }, + "lookup_response": { + "type": "object", + "required": [ + "ucp", + "products" + ], + "properties": { + "ucp": { + "$ref": "../ucp.json#/$defs/response_catalog_schema" + }, + "products": { + "type": "array", + "items": { + "allOf": [ + { "$ref": "types/product.json" }, + { + "properties": { + "variants": { + "items": { "$ref": "#/$defs/lookup_variant" } + } + } + } + ] + }, + "description": "Products matching the requested identifiers. May contain fewer items if some identifiers not found, or more if identifiers match multiple products." + }, + "messages": { + "type": "array", + "items": { + "$ref": "types/message.json" + }, + "description": "Errors, warnings, or informational messages about the requested items." + } + } + } + } +} diff --git a/source/schemas/shopping/catalog_search.json b/source/schemas/shopping/catalog_search.json new file mode 100644 index 000000000..4d7516915 --- /dev/null +++ b/source/schemas/shopping/catalog_search.json @@ -0,0 +1,60 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/catalog_search.json", + "name": "dev.ucp.shopping.catalog.search", + "title": "Catalog Search", + "description": "Product catalog search capability.", + "type": "object", + "$defs": { + "search_request": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Free-text search query." + }, + "context": { + "$ref": "types/context.json" + }, + "signals": { + "$ref": "types/signals.json" + }, + "filters": { + "$ref": "types/search_filters.json" + }, + "pagination": { + "$ref": "types/pagination.json#/$defs/request" + } + } + }, + "search_response": { + "type": "object", + "required": [ + "ucp", + "products" + ], + "properties": { + "ucp": { + "$ref": "../ucp.json#/$defs/response_catalog_schema" + }, + "products": { + "type": "array", + "items": { + "$ref": "types/product.json" + }, + "description": "Products matching the search criteria." + }, + "pagination": { + "$ref": "types/pagination.json#/$defs/response" + }, + "messages": { + "type": "array", + "items": { + "$ref": "types/message.json" + }, + "description": "Errors, warnings, or informational messages about the search results." + } + } + } + } +} diff --git a/source/schemas/shopping/checkout.json b/source/schemas/shopping/checkout.json index ed2b5a37b..d38d2d179 100644 --- a/source/schemas/shopping/checkout.json +++ b/source/schemas/shopping/checkout.json @@ -4,6 +4,10 @@ "name": "dev.ucp.shopping.checkout", "title": "Checkout", "description": "Base checkout schema. Extensions compose onto this using allOf.", + "$comment": "identity_scopes is a UCP annotation processed by UCP-aware tooling during capability negotiation. Its canonical definition and processing rules are specified in the identity_linking capability schema.", + "identity_scopes": [ + "dev.ucp.shopping.scopes.checkout_session" + ], "type": "object", "required": [ "ucp", @@ -56,6 +60,29 @@ "create": "optional", "update": "optional", "complete": "omit" + } + }, + "signals": { + "$ref": "types/signals.json", + "ucp_request": { + "create": "optional", + "update": "optional", + "complete": "optional" + } + }, + "risk_signals": { + "type": "object", + "description": "Deprecated. Use signals instead. Will be removed in the next version.", + "deprecated": true, + "additionalProperties": true, + "ucp_request": { + "complete": { + "transition": { + "from": "optional", + "to": "omit", + "description": "Replaced by signals. Will be removed in the next version." + } + } }, "ucp_response": "omit" }, @@ -82,10 +109,7 @@ } }, "totals": { - "type": "array", - "items": { - "$ref": "types/total.json" - }, + "$ref": "types/totals.json", "description": "Different cart totals.", "ucp_request": "omit" }, diff --git a/source/schemas/shopping/discount.json b/source/schemas/shopping/discount.json index db1fea8c9..f4dd7eea6 100644 --- a/source/schemas/shopping/discount.json +++ b/source/schemas/shopping/discount.json @@ -3,7 +3,7 @@ "$id": "https://ucp.dev/schemas/shopping/discount.json", "name": "dev.ucp.shopping.discount", "title": "Discount Extension", - "description": "Extends Checkout with discount code support, enabling agents to apply promotional, loyalty, referral, and other discount codes.", + "description": "Extends Cart and Checkout with discount support, including discount codes, automatic discounts, and eligibility-triggered provisional discounts.", "$defs": { "allocation": { "type": "object", @@ -18,9 +18,8 @@ "description": "JSONPath to the allocation target (e.g., '$.line_items[0]', '$.totals.shipping')." }, "amount": { - "type": "integer", - "minimum": 0, - "description": "Amount allocated to this target in minor (cents) currency units." + "$ref": "types/amount.json", + "description": "Amount allocated to this target in ISO 4217 minor units." } } }, @@ -41,9 +40,8 @@ "description": "Human-readable discount name (e.g., 'Summer Sale 20% Off')." }, "amount": { - "type": "integer", - "minimum": 0, - "description": "Total discount amount in minor (cents) currency units." + "$ref": "types/amount.json", + "description": "Total discount amount in ISO 4217 minor units." }, "automatic": { "type": "boolean", @@ -63,6 +61,15 @@ "minimum": 1, "description": "Stacking order for discount calculation. Lower numbers applied first (1 = first)." }, + "provisional": { + "type": "boolean", + "default": false, + "description": "True if this discount requires additional verification." + }, + "eligibility": { + "$ref": "types/reverse_domain_name.json", + "description": "The eligibility claim accepted by the Business for this discount. Corresponds to a value from context.eligibility. Omitted for code-based and non-eligibility automatic discounts." + }, "allocations": { "type": "array", "items": { @@ -93,6 +100,27 @@ } } }, + "dev.ucp.shopping.cart": { + "title": "Cart with Discount", + "description": "Cart extended with discount capability.", + "allOf": [ + { + "$ref": "cart.json" + }, + { + "type": "object", + "properties": { + "discounts": { + "$ref": "#/$defs/discounts_object", + "ucp_request": { + "create": "optional", + "update": "optional" + } + } + } + } + ] + }, "dev.ucp.shopping.checkout": { "title": "Checkout with Discount", "description": "Checkout extended with discount capability.", diff --git a/source/schemas/shopping/order.json b/source/schemas/shopping/order.json index c170c2852..ad738489e 100644 --- a/source/schemas/shopping/order.json +++ b/source/schemas/shopping/order.json @@ -82,11 +82,12 @@ }, "description": "Append-only event log of money movements (refunds, returns, credits, disputes, cancellations, etc.) that exist independently of fulfillment." }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code. MUST match the currency from the originating checkout session." + }, "totals": { - "type": "array", - "items": { - "$ref": "types/total.json" - }, + "$ref": "types/totals.json", "description": "Different totals for the order." } } diff --git a/source/schemas/shopping/types/adjustment.json b/source/schemas/shopping/types/adjustment.json index 6665c38df..2bfc7040b 100644 --- a/source/schemas/shopping/types/adjustment.json +++ b/source/schemas/shopping/types/adjustment.json @@ -53,8 +53,8 @@ "description": "Which line items and quantities are affected (optional)." }, "amount": { - "type": "integer", - "description": "Amount in minor units (cents) for refunds, credits, price adjustments (optional)." + "$ref": "amount.json", + "description": "Amount in ISO 4217 minor units for refunds, credits, or price adjustments." }, "description": { "type": "string", diff --git a/source/schemas/shopping/types/amount.json b/source/schemas/shopping/types/amount.json new file mode 100644 index 000000000..3464f3f25 --- /dev/null +++ b/source/schemas/shopping/types/amount.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/amount.json", + "title": "Amount", + "description": "Monetary amount in the currency's minor unit as defined by ISO 4217. Refer to the currency's exponent to determine minor-to-major ratio (e.g., 2 for USD, 0 for JPY, 3 for KWD).", + "type": "integer", + "minimum": 0 +} diff --git a/source/schemas/shopping/types/category.json b/source/schemas/shopping/types/category.json new file mode 100644 index 000000000..f6f4ff9c6 --- /dev/null +++ b/source/schemas/shopping/types/category.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/category.json", + "title": "Category", + "description": "A product category with optional taxonomy identifier.", + "type": "object", + "required": ["value"], + "properties": { + "value": { + "type": "string", + "description": "Category value or path (e.g., 'Apparel > Shirts', '1604')." + }, + "taxonomy": { + "type": "string", + "description": "Source taxonomy. Well-known values: `google_product_category`, `shopify`, `merchant`." + } + } +} diff --git a/source/schemas/shopping/types/context.json b/source/schemas/shopping/types/context.json index afd46b863..c5f1162a2 100644 --- a/source/schemas/shopping/types/context.json +++ b/source/schemas/shopping/types/context.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/shopping/types/context.json", "title": "Context", - "description": "Provisional buyer signals for relevance and localization: product availability, pricing, currency, tax, shipping, payment methods, and eligibility (e.g., student or affiliation discounts). Businesses SHOULD use these values when authoritative data (e.g., address) is absent, and MAY ignore unsupported values without returning errors. Context SHOULD be non-identifying and can be disclosed progressively—coarse signals early, finer resolution as the session progresses. Higher-resolution data (shipping address, billing address) supersedes context. Platforms SHOULD progressively enhance context throughout the buyer journey.", + "description": "Provisional buyer signals for relevance and localization—not authoritative data. Businesses SHOULD use these values when verified inputs (e.g., shipping address) are absent, and MAY ignore or down-rank them if inconsistent with higher-confidence signals (authenticated account, risk detection) or regulatory constraints (export controls). Eligibility and policy enforcement MUST occur at checkout time using binding transaction data. Context SHOULD be non-identifying and can be disclosed progressively—coarse signals early, finer resolution as the session progresses. Higher-resolution data (shipping address, billing address) supersedes context.", "type": "object", "additionalProperties": true, "properties": { @@ -21,6 +21,22 @@ "intent": { "type": "string", "description": "Background context describing buyer's intent (e.g., 'looking for a gift under $50', 'need something durable for outdoor use'). Informs relevance, recommendations, and personalization." + }, + "language": { + "type": "string", + "description": "Preferred language for content. Use IETF BCP 47 language tags (e.g., 'en', 'fr-CA', 'zh-Hans'). For REST, equivalent to Accept-Language header—platforms SHOULD fall back to Accept-Language when this field is absent; when provided, overrides Accept-Language. Businesses MAY return content in a different language if unavailable." + }, + "currency": { + "type": "string", + "description": "Preferred currency (ISO 4217, e.g., 'EUR', 'USD'). Businesses determine presentment currency from context and authoritative signals; this hint MAY inform selection in multi-currency markets. Also serves as the denomination for price filter values — platforms SHOULD include this field when sending price filters. Response prices include explicit currency confirming the resolution." + }, + "eligibility": { + "type": "array", + "description": "Buyer claims about eligible benefits such as loyalty membership, payment instrument perks, and similar. Recognized claims MAY inform the Business response (e.g., member-only product availability, adjusted pricing in catalog, provisional discounts at cart or checkout). Businesses MUST ignore unrecognized values without error. Values MUST use reverse-domain naming (e.g., 'com.example.loyalty_gold', 'org.school.student') and MUST be non-identifying.", + "uniqueItems": true, + "items": { + "$ref": "reverse_domain_name.json" + } } } } diff --git a/source/schemas/shopping/types/description.json b/source/schemas/shopping/types/description.json new file mode 100644 index 000000000..8de81df17 --- /dev/null +++ b/source/schemas/shopping/types/description.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/description.json", + "title": "Description", + "description": "Description content in one or more formats. At least one format must be provided.", + "type": "object", + "properties": { + "plain": { + "type": "string", + "description": "Plain text content." + }, + "html": { + "type": "string", + "description": "HTML-formatted content. Security: Platforms MUST sanitize before rendering—strip scripts, event handlers, and untrusted elements. Treat all rich text as untrusted input." + }, + "markdown": { + "type": "string", + "description": "Markdown-formatted content." + } + }, + "minProperties": 1 +} diff --git a/source/schemas/shopping/types/error_code.json b/source/schemas/shopping/types/error_code.json index 77ddf32d4..71d0d0199 100644 --- a/source/schemas/shopping/types/error_code.json +++ b/source/schemas/shopping/types/error_code.json @@ -8,6 +8,7 @@ "out_of_stock", "item_unavailable", "address_undeliverable", - "payment_failed" + "payment_failed", + "eligibility_invalid" ] } diff --git a/source/schemas/shopping/types/error_response.json b/source/schemas/shopping/types/error_response.json new file mode 100644 index 000000000..70c451dec --- /dev/null +++ b/source/schemas/shopping/types/error_response.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/error_response.json", + "title": "Error Response", + "description": "Generic error response when business logic prevents resource creation or failed to retrieve resource. Used when no valid resource can be established.", + "type": "object", + "required": ["ucp", "messages"], + "additionalProperties": false, + "properties": { + "ucp": { + "allOf": [ + { "$ref": "../../ucp.json#/$defs/base" }, + { + "properties": { "status": { "const": "error" } }, + "required": ["status"] + } + ], + "description": "UCP protocol metadata. Status MUST be 'error' for error response." + }, + "messages": { + "type": "array", + "items": { + "$ref": "message.json" + }, + "minItems": 1, + "description": "Array of messages describing why the operation failed." + }, + "continue_url": { + "type": "string", + "format": "uri", + "description": "URL for buyer handoff or session recovery." + } + } +} diff --git a/source/schemas/shopping/types/input_correlation.json b/source/schemas/shopping/types/input_correlation.json new file mode 100644 index 000000000..d9734fd3e --- /dev/null +++ b/source/schemas/shopping/types/input_correlation.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/input_correlation.json", + "title": "Input Correlation", + "description": "Maps a request identifier to the variant it resolved to, with match semantics.", + "type": "object", + "required": ["id"], + "properties": { + "id": { + "type": "string", + "description": "The identifier from the lookup request that resolved to this variant." + }, + "match": { + "type": "string", + "description": "How the request identifier resolved to this variant. Well-known values: `exact` (input directly identifies this variant, e.g., variant ID, SKU), `featured` (server selected this variant as representative, e.g., product ID resolved to best match). Businesses MAY implement and provide additional resolution strategies.", + "examples": ["exact", "featured"] + } + } +} diff --git a/source/schemas/shopping/types/item.json b/source/schemas/shopping/types/item.json index 1893e7b40..cde81e1a9 100644 --- a/source/schemas/shopping/types/item.json +++ b/source/schemas/shopping/types/item.json @@ -19,9 +19,8 @@ "ucp_request": "omit" }, "price": { - "type": "integer", - "description": "Unit price in minor (cents) currency units.", - "minimum": 0, + "$ref": "amount.json", + "description": "Unit price in ISO 4217 minor units.", "ucp_request": "omit" }, "image_url": { diff --git a/source/schemas/shopping/types/media.json b/source/schemas/shopping/types/media.json new file mode 100644 index 000000000..cbcaa0713 --- /dev/null +++ b/source/schemas/shopping/types/media.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/media.json", + "title": "Media", + "description": "Product media item (image, video, etc.).", + "type": "object", + "required": [ + "type", + "url" + ], + "properties": { + "type": { + "type": "string", + "description": "Media type. Well-known values: `image`, `video`, `model_3d`." + }, + "url": { + "type": "string", + "format": "uri", + "description": "URL to the media resource." + }, + "alt_text": { + "type": "string", + "description": "Accessibility text describing the media." + }, + "width": { + "type": "integer", + "minimum": 1, + "description": "Width in pixels (for images/video)." + }, + "height": { + "type": "integer", + "minimum": 1, + "description": "Height in pixels (for images/video)." + } + } +} diff --git a/source/schemas/shopping/types/message_error.json b/source/schemas/shopping/types/message_error.json index 14ccd8ff1..2b157a98a 100644 --- a/source/schemas/shopping/types/message_error.json +++ b/source/schemas/shopping/types/message_error.json @@ -40,9 +40,10 @@ "enum": [ "recoverable", "requires_buyer_input", - "requires_buyer_review" + "requires_buyer_review", + "unrecoverable" ], - "description": "Declares who resolves this error. 'recoverable': agent can fix via API. 'requires_buyer_input': merchant requires information their API doesn't support collecting programmatically (checkout incomplete). 'requires_buyer_review': buyer must authorize before order placement due to policy, regulatory, or entitlement rules (checkout complete). Errors with 'requires_*' severity contribute to 'status: requires_escalation'." + "description": "Reflects the resource state and recommended action. 'recoverable': platform can resolve by modifying inputs and retrying via API. 'requires_buyer_input': merchant requires information their API doesn't support collecting programmatically (checkout incomplete). 'requires_buyer_review': buyer must authorize before order placement due to policy, regulatory, or entitlement rules. 'unrecoverable': no valid resource exists to act on, retry with new resource or inputs. Errors with 'requires_*' severity contribute to 'status: requires_escalation'." } } } diff --git a/source/schemas/shopping/types/message_warning.json b/source/schemas/shopping/types/message_warning.json index 20695b73e..d42210bb4 100644 --- a/source/schemas/shopping/types/message_warning.json +++ b/source/schemas/shopping/types/message_warning.json @@ -34,6 +34,21 @@ ], "default": "plain", "description": "Content format, default = plain." + }, + "presentation": { + "type": "string", + "default": "notice", + "description": "Rendering contract for this warning. 'notice' (default): platform MUST display, MAY dismiss. 'disclosure': platform MUST display in proximity to the path-referenced component, MUST NOT hide or auto-dismiss. See specification for full contract." + }, + "image_url": { + "type": "string", + "format": "uri", + "description": "URL to a required visual element (e.g., warning symbol, energy class label)." + }, + "url": { + "type": "string", + "format": "uri", + "description": "Reference URL for more information (e.g., regulatory site, registry entry, policy page)." } } } diff --git a/source/schemas/shopping/types/option_value.json b/source/schemas/shopping/types/option_value.json new file mode 100644 index 000000000..f243b631a --- /dev/null +++ b/source/schemas/shopping/types/option_value.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/option_value.json", + "title": "Option Value", + "description": "A selectable value for a product option.", + "type": "object", + "required": [ + "label" + ], + "properties": { + "label": { + "type": "string", + "description": "Display text for this option value (e.g., 'Small', 'Blue')." + } + } +} diff --git a/source/schemas/shopping/types/pagination.json b/source/schemas/shopping/types/pagination.json new file mode 100644 index 000000000..89b975120 --- /dev/null +++ b/source/schemas/shopping/types/pagination.json @@ -0,0 +1,52 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/pagination.json", + "title": "Pagination", + "description": "Cursor-based pagination for list operations.", + "type": "object", + "$defs": { + "request": { + "type": "object", + "description": "Pagination parameters for requests.", + "properties": { + "cursor": { + "type": "string", + "description": "Opaque cursor from previous response." + }, + "limit": { + "type": "integer", + "minimum": 1, + "default": 10, + "description": "Requested page size. Implementations MAY clamp to a lower maximum." + } + } + }, + "response": { + "type": "object", + "description": "Pagination information in responses.", + "properties": { + "cursor": { + "type": "string", + "description": "Cursor to fetch the next page of results. MUST be present when has_next_page is true." + }, + "has_next_page": { + "type": "boolean", + "description": "Whether more results are available." + }, + "total_count": { + "type": "integer", + "minimum": 0, + "description": "Total number of matching items, if available." + } + }, + "required": ["has_next_page"], + "if": { + "properties": { "has_next_page": { "const": true } }, + "required": ["has_next_page"] + }, + "then": { + "required": ["cursor"] + } + } + } +} diff --git a/source/schemas/shopping/types/price.json b/source/schemas/shopping/types/price.json new file mode 100644 index 000000000..8d39b9cbc --- /dev/null +++ b/source/schemas/shopping/types/price.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/price.json", + "title": "Price", + "description": "Price with explicit currency.", + "type": "object", + "required": [ + "amount", + "currency" + ], + "properties": { + "amount": { + "$ref": "amount.json", + "description": "Amount in ISO 4217 minor units. Use 0 for free items." + }, + "currency": { + "type": "string", + "description": "ISO 4217 currency code (e.g., 'USD', 'EUR', 'GBP').", + "pattern": "^[A-Z]{3}$" + } + } +} diff --git a/source/schemas/shopping/types/price_filter.json b/source/schemas/shopping/types/price_filter.json new file mode 100644 index 000000000..5d92e3066 --- /dev/null +++ b/source/schemas/shopping/types/price_filter.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/price_filter.json", + "title": "Price Filter", + "description": "Price range filter denominated in context.currency. When context.currency matches the presentment currency, businesses apply the filter directly. When it differs, businesses SHOULD convert filter values to the presentment currency before applying; if conversion is not supported, businesses MAY ignore the filter and SHOULD indicate this via a message. When context.currency is absent, filter denomination is ambiguous and businesses MAY ignore it.", + "type": "object", + "properties": { + "min": { + "$ref": "amount.json", + "description": "Minimum price in ISO 4217 minor units." + }, + "max": { + "$ref": "amount.json", + "description": "Maximum price in ISO 4217 minor units." + } + } +} diff --git a/source/schemas/shopping/types/price_range.json b/source/schemas/shopping/types/price_range.json new file mode 100644 index 000000000..cd574d3a7 --- /dev/null +++ b/source/schemas/shopping/types/price_range.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/price_range.json", + "title": "Price Range", + "description": "A price range representing minimum and maximum values (e.g., across product variants).", + "type": "object", + "required": [ + "min", + "max" + ], + "properties": { + "min": { + "$ref": "price.json", + "description": "Minimum price in the range." + }, + "max": { + "$ref": "price.json", + "description": "Maximum price in the range." + } + } +} diff --git a/source/schemas/shopping/types/product.json b/source/schemas/shopping/types/product.json new file mode 100644 index 000000000..7a3e60654 --- /dev/null +++ b/source/schemas/shopping/types/product.json @@ -0,0 +1,89 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/product.json", + "title": "Product", + "description": "A product in the catalog with variants and options.", + "type": "object", + "required": [ + "id", + "title", + "description", + "price_range", + "variants" + ], + "properties": { + "id": { + "type": "string", + "description": "Global ID (GID) uniquely identifying this product." + }, + "handle": { + "type": "string", + "description": "URL-safe slug for SEO-friendly URLs (e.g., 'blue-runner-pro'). Use id for stable API references." + }, + "title": { + "type": "string", + "description": "Product title." + }, + "description": { + "$ref": "description.json", + "description": "Product description in one or more formats." + }, + "url": { + "type": "string", + "format": "uri", + "description": "Canonical product page URL." + }, + "categories": { + "type": "array", + "items": { + "$ref": "category.json" + }, + "description": "Product categories with optional taxonomy identifiers." + }, + "price_range": { + "$ref": "price_range.json", + "description": "Price range across all variants." + }, + "list_price_range": { + "$ref": "price_range.json", + "description": "List price range before discounts (for strikethrough display)." + }, + "media": { + "type": "array", + "items": { + "$ref": "media.json" + }, + "description": "Product media (images, videos, 3D models). First item is the featured media for listings." + }, + "options": { + "type": "array", + "items": { + "$ref": "product_option.json" + }, + "description": "Product options (Size, Color, etc.)." + }, + "variants": { + "type": "array", + "items": { + "$ref": "variant.json" + }, + "minItems": 1, + "description": "Purchasable variants of this product. First item is the featured variant for listings." + }, + "rating": { + "$ref": "rating.json", + "description": "Aggregate product rating." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Product tags for categorization and search." + }, + "metadata": { + "type": "object", + "description": "Business-defined custom data extending the standard product model." + } + } +} diff --git a/source/schemas/shopping/types/product_option.json b/source/schemas/shopping/types/product_option.json new file mode 100644 index 000000000..32663ae9a --- /dev/null +++ b/source/schemas/shopping/types/product_option.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/product_option.json", + "title": "Product Option", + "description": "A product option such as size, color, or material.", + "type": "object", + "required": [ + "name", + "values" + ], + "properties": { + "name": { + "type": "string", + "description": "Option name (e.g., 'Size', 'Color')." + }, + "values": { + "type": "array", + "items": { + "$ref": "option_value.json" + }, + "minItems": 1, + "description": "Available values for this option." + } + } +} diff --git a/source/schemas/shopping/types/rating.json b/source/schemas/shopping/types/rating.json new file mode 100644 index 000000000..6b0b3b5fb --- /dev/null +++ b/source/schemas/shopping/types/rating.json @@ -0,0 +1,34 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/rating.json", + "title": "Rating", + "description": "Product rating aggregate.", + "type": "object", + "required": [ + "value", + "scale_max" + ], + "properties": { + "value": { + "type": "number", + "minimum": 0, + "description": "Average rating value." + }, + "scale_min": { + "type": "number", + "minimum": 0, + "default": 1, + "description": "Minimum value on the rating scale (e.g., 1 for 1-5 stars)." + }, + "scale_max": { + "type": "number", + "minimum": 1, + "description": "Maximum value on the rating scale (e.g., 5 for 5-star)." + }, + "count": { + "type": "integer", + "minimum": 0, + "description": "Number of reviews contributing to the rating." + } + } +} diff --git a/source/schemas/shopping/types/reverse_domain_name.json b/source/schemas/shopping/types/reverse_domain_name.json new file mode 100644 index 000000000..6ecbfd9d1 --- /dev/null +++ b/source/schemas/shopping/types/reverse_domain_name.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/reverse_domain_name.json", + "title": "Reverse Domain Name", + "description": "Reverse-domain identifier used for collision-safe namespacing of capabilities, services, handlers, eligibility claims, and extension-contributed keys. Must contain at least two dot-separated segments (e.g., 'dev.ucp.shopping.checkout', 'com.example.loyalty_gold').", + "type": "string", + "pattern": "^[a-z][a-z0-9]*(?:\\.[a-z][a-z0-9_]*)+$" +} diff --git a/source/schemas/shopping/types/search_filters.json b/source/schemas/shopping/types/search_filters.json new file mode 100644 index 000000000..8380be58b --- /dev/null +++ b/source/schemas/shopping/types/search_filters.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/search_filters.json", + "title": "Search Filters", + "description": "Filter criteria to narrow search results. All specified filters combine with AND logic.", + "type": "object", + "properties": { + "categories": { + "type": "array", + "items": { "type": "string" }, + "description": "Filter by product categories (OR logic — matches products in any listed categories). Values match against the value field in product category entries. Valid values can be discovered from the categories field in search results, merchant documentation, or standard taxonomies that businesses may align with." + }, + "price": { + "$ref": "price_filter.json" + } + }, + "additionalProperties": true +} diff --git a/source/schemas/shopping/types/selected_option.json b/source/schemas/shopping/types/selected_option.json new file mode 100644 index 000000000..5c144fd1e --- /dev/null +++ b/source/schemas/shopping/types/selected_option.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/selected_option.json", + "title": "Selected Option", + "description": "A specific option selection on a variant (e.g., Size: Large).", + "type": "object", + "required": [ + "name", + "label" + ], + "properties": { + "name": { + "type": "string", + "description": "Option name (e.g., 'Size')." + }, + "label": { + "type": "string", + "description": "Selected option label (e.g., 'Large')." + } + } +} diff --git a/source/schemas/shopping/types/signals.json b/source/schemas/shopping/types/signals.json new file mode 100644 index 000000000..874823290 --- /dev/null +++ b/source/schemas/shopping/types/signals.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/signals.json", + "title": "Signals", + "description": "Environment data provided by the platform to support authorization and abuse prevention. Values MUST NOT be buyer-asserted claims — platforms provide signals based on direct observation or independently verifiable third-party attestations. All signal keys MUST use reverse-domain naming to ensure provenance and prevent collisions when multiple extensions contribute to the shared namespace.", + "type": "object", + "propertyNames": { + "pattern": "^[a-z][a-z0-9]*(?:\\.[a-z][a-z0-9_]*)+$", + "description": "Reverse-domain identifier (e.g., dev.ucp.buyer_ip, com.example.device_id)." + }, + "properties": { + "dev.ucp.buyer_ip": { + "type": "string", + "description": "Client's IP address (IPv4 or IPv6)." + }, + "dev.ucp.user_agent": { + "type": "string", + "description": "Client's HTTP User-Agent header or equivalent." + } + }, + "additionalProperties": true +} diff --git a/source/schemas/shopping/types/signed_amount.json b/source/schemas/shopping/types/signed_amount.json new file mode 100644 index 000000000..b68d57789 --- /dev/null +++ b/source/schemas/shopping/types/signed_amount.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/signed_amount.json", + "title": "Signed Amount", + "description": "Monetary amount in the currency's minor unit as defined by ISO 4217. Refer to the currency's exponent to determine minor-to-major ratio (e.g., 2 for USD, 0 for JPY, 3 for KWD). May be negative — the sign is intrinsic to the value (e.g., discounts are negative, charges are positive).", + "type": "integer" +} diff --git a/source/schemas/shopping/types/total.json b/source/schemas/shopping/types/total.json index 4d2e24ad4..6f488d9ec 100644 --- a/source/schemas/shopping/types/total.json +++ b/source/schemas/shopping/types/total.json @@ -2,6 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://ucp.dev/schemas/shopping/types/total.json", "title": "Total", + "description": "A cost breakdown entry with a category, amount, and optional display text.", "type": "object", "required": [ "type", @@ -10,16 +11,7 @@ "properties": { "type": { "type": "string", - "enum": [ - "items_discount", - "subtotal", - "discount", - "fulfillment", - "tax", - "fee", - "total" - ], - "description": "Type of total categorization.", + "description": "Cost category. Well-known values: subtotal, items_discount, discount, fulfillment, tax, fee, total. Businesses MAY use additional values.", "ucp_request": "omit" }, "display_text": { @@ -28,9 +20,7 @@ "ucp_request": "omit" }, "amount": { - "type": "integer", - "description": "If type == total, sums subtotal - discount + fulfillment + tax + fee. Should be >= 0. Amount in minor (cents) currency units.", - "minimum": 0, + "$ref": "amount.json", "ucp_request": "omit" } } diff --git a/source/schemas/shopping/types/totals.json b/source/schemas/shopping/types/totals.json new file mode 100644 index 000000000..71a963b17 --- /dev/null +++ b/source/schemas/shopping/types/totals.json @@ -0,0 +1,73 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/totals.json", + "title": "Totals", + "description": "Pricing breakdown provided by the business. MUST contain exactly one subtotal and one total entry. Detail types (tax, fee, discount, fulfillment) may appear multiple times for itemization. Platforms MUST render all entries in order using display_text and amount.", + "type": "array", + "items": { + "allOf": [ + { + "$ref": "total.json" + }, + { + "type": "object", + "properties": { + "amount": { + "$ref": "signed_amount.json" + }, + "lines": { + "type": "array", + "items": { + "type": "object", + "required": [ + "display_text", + "amount" + ], + "properties": { + "display_text": { + "type": "string", + "description": "Human-readable label for this sub-line." + }, + "amount": { + "$ref": "signed_amount.json" + } + }, + "description": "Sub-line entry. Additional metadata MAY be included." + }, + "description": "Optional itemized breakdown. The parent entry is always rendered; lines are supplementary. Sum of line amounts MUST equal the parent entry amount.", + "ucp_request": "omit" + } + } + }, + { + "if": { "properties": { "type": { "enum": ["discount", "items_discount"] } }, "required": ["type"] }, + "then": { "properties": { "amount": { "exclusiveMaximum": 0 } } } + }, + { + "if": { "properties": { "type": { "enum": ["subtotal", "fulfillment", "tax", "fee"] } }, "required": ["type"] }, + "then": { "properties": { "amount": { "minimum": 0 } } } + }, + { + "if": { + "properties": { + "type": { "not": { "enum": ["subtotal", "items_discount", "discount", "fulfillment", "tax", "fee", "total"] } } + }, + "required": ["type"] + }, + "then": { "required": ["display_text"] } + } + ] + }, + "allOf": [ + { + "contains": { "properties": { "type": { "const": "subtotal" } }, "required": ["type"] }, + "minContains": 1, + "maxContains": 1 + }, + { + "contains": { "properties": { "type": { "const": "total" } }, "required": ["type"] }, + "minContains": 1, + "maxContains": 1 + } + ] +} diff --git a/source/schemas/shopping/types/variant.json b/source/schemas/shopping/types/variant.json new file mode 100644 index 000000000..bcbf753dd --- /dev/null +++ b/source/schemas/shopping/types/variant.json @@ -0,0 +1,167 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://ucp.dev/schemas/shopping/types/variant.json", + "title": "Variant", + "description": "A purchasable variant of a product with specific option selections.", + "type": "object", + "required": [ + "id", + "title", + "description", + "price" + ], + "properties": { + "id": { + "type": "string", + "description": "Global ID (GID) uniquely identifying this variant. Used as item.id in checkout." + }, + "sku": { + "type": "string", + "description": "Business-assigned identifier for inventory and fulfillment." + }, + "barcodes": { + "type": "array", + "items": { + "type": "object", + "required": ["type", "value"], + "properties": { + "type": { + "type": "string", + "description": "Barcode standard. Well-known values: UPC, EAN, ISBN, GTIN, JAN." + }, + "value": { + "type": "string", + "description": "Barcode value." + } + } + }, + "description": "Industry-standard product identifiers for cross-reference and correlation." + }, + "handle": { + "type": "string", + "description": "URL-safe variant handle/slug." + }, + "title": { + "type": "string", + "description": "Variant display title (e.g., 'Blue / Large')." + }, + "description": { + "$ref": "description.json", + "description": "Variant description in one or more formats." + }, + "url": { + "type": "string", + "format": "uri", + "description": "Canonical variant page URL." + }, + "categories": { + "type": "array", + "items": { + "$ref": "category.json" + }, + "description": "Variant categories with optional taxonomy identifiers." + }, + "price": { + "$ref": "price.json", + "description": "Current selling price." + }, + "list_price": { + "$ref": "price.json", + "description": "List price before discounts (for strikethrough display)." + }, + "unit_price": { + "type": "object", + "description": "Price per standard unit of measurement. MAY be omitted when unit pricing does not apply.", + "required": ["amount", "currency", "measure", "reference"], + "properties": { + "amount": { + "$ref": "amount.json", + "description": "Unit price in ISO 4217 minor units. Business MUST return precomputed unit price value: (variant.price / measure.value) * reference.value." + }, + "currency": { + "type": "string", + "pattern": "^[A-Z]{3}$", + "description": "ISO 4217 currency code." + }, + "measure": { + "type": "object", + "description": "Product quantity in packaging (e.g., 750ml bottle).", + "required": ["value", "unit"], + "properties": { + "value": { "type": "number", "description": "Package quantity." }, + "unit": { "type": "string", "description": "Unit of measurement." } + } + }, + "reference": { + "type": "object", + "description": "Denominator for unit price display (e.g., per 100ml, per 1kg).", + "required": ["value", "unit"], + "properties": { + "value": { "type": "integer", "description": "Reference quantity." }, + "unit": { "type": "string", "description": "Unit of measurement." } + } + } + } + }, + "availability": { + "type": "object", + "description": "Variant availability for purchase.", + "properties": { + "available": { + "type": "boolean", + "description": "Whether this variant can be purchased. See status for fulfillment details." + }, + "status": { + "type": "string", + "description": "Qualifies available with fulfillment state. Well-known values: `in_stock`, `backorder`, `preorder`, `out_of_stock`, `discontinued`." + } + } + }, + "selected_options": { + "type": "array", + "items": { + "$ref": "selected_option.json" + }, + "description": "Option selections that define this variant." + }, + "media": { + "type": "array", + "items": { + "$ref": "media.json" + }, + "description": "Variant media (images, videos, 3D models). First item is the featured media for listings." + }, + "rating": { + "$ref": "rating.json", + "description": "Variant rating." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Variant tags for categorization and search." + }, + "metadata": { + "type": "object", + "description": "Business-defined custom data extending the standard variant model." + }, + "seller": { + "type": "object", + "description": "Optional seller context for this variant.", + "properties": { + "name": { + "type": "string", + "description": "Seller display name." + }, + "links": { + "type": "array", + "items": { + "$ref": "link.json" + }, + "description": "Seller policy and information links." + } + } + } + } +} diff --git a/source/schemas/ucp.json b/source/schemas/ucp.json index d6cffac81..f73e305b4 100644 --- a/source/schemas/ucp.json +++ b/source/schemas/ucp.json @@ -11,10 +11,39 @@ "description": "UCP version in YYYY-MM-DD format." }, - "reverse_domain_name": { - "type": "string", - "pattern": "^[a-z][a-z0-9]*(?:\\.[a-z][a-z0-9_]*)+$", - "description": "Reverse-domain identifier (e.g., com.google.pay, dev.ucp.shopping.checkout)" + "version_constraint": { + "type": "object", + "description": "Version range requirement with minimum and optional maximum.", + "properties": { + "min": { + "$ref": "#/$defs/version", + "description": "Minimum required version (inclusive)." + }, + "max": { + "$ref": "#/$defs/version", + "description": "Maximum compatible version (inclusive). When absent, no upper bound." + } + }, + "required": ["min"], + "additionalProperties": true + }, + + "requires": { + "type": "object", + "description": "Version requirements for extension schemas. Declares minimum (and optionally maximum) protocol and capability versions needed for correct operation.", + "properties": { + "protocol": { + "$ref": "#/$defs/version_constraint", + "description": "Required protocol version." + }, + "capabilities": { + "type": "object", + "description": "Required capability versions, keyed by capability name. Keys must be a subset of the extension's $defs keys.", + "propertyNames": { "$ref": "shopping/types/reverse_domain_name.json" }, + "additionalProperties": { "$ref": "#/$defs/version_constraint" } + } + }, + "additionalProperties": true }, "entity": { @@ -54,10 +83,16 @@ "required": ["version"], "properties": { "version": { "$ref": "#/$defs/version" }, + "status": { + "type": "string", + "enum": ["success", "error"], + "default": "success", + "description": "Application-level status of the UCP operation." + }, "services": { "type": "object", "description": "Service registry keyed by reverse-domain name.", - "propertyNames": { "$ref": "#/$defs/reverse_domain_name" }, + "propertyNames": { "$ref": "shopping/types/reverse_domain_name.json" }, "additionalProperties": { "type": "array", "items": { "$ref": "service.json#/$defs/base" } @@ -66,7 +101,7 @@ "capabilities": { "type": "object", "description": "Capability registry keyed by reverse-domain name.", - "propertyNames": { "$ref": "#/$defs/reverse_domain_name" }, + "propertyNames": { "$ref": "shopping/types/reverse_domain_name.json" }, "additionalProperties": { "type": "array", "items": { "$ref": "capability.json#/$defs/base" } @@ -75,7 +110,7 @@ "payment_handlers": { "type": "object", "description": "Payment handler registry keyed by reverse-domain name.", - "propertyNames": { "$ref": "#/$defs/reverse_domain_name" }, + "propertyNames": { "$ref": "shopping/types/reverse_domain_name.json" }, "additionalProperties": { "type": "array", "items": { "$ref": "payment_handler.json#/$defs/base" } @@ -122,6 +157,15 @@ { "required": ["services", "payment_handlers"], "properties": { + "supported_versions": { + "type": "object", + "description": "Previous protocol versions this business supports, mapped to profile URIs. Businesses that support older protocol versions SHOULD advertise each version and link to its profile. Each URI points to a complete, self-contained profile for that version. When omitted, only `version` is supported.", + "propertyNames": { "$ref": "#/$defs/version" }, + "additionalProperties": { + "type": "string", + "format": "uri" + } + }, "services": { "additionalProperties": { "items": { "$ref": "service.json#/$defs/business_schema" } @@ -206,6 +250,23 @@ } } ] + }, + + "response_catalog_schema": { + "title": "UCP Catalog Response Schema", + "description": "UCP metadata for catalog responses.", + "allOf": [ + { "$ref": "#/$defs/base" }, + { + "properties": { + "capabilities": { + "additionalProperties": { + "items": { "$ref": "capability.json#/$defs/response_schema" } + } + } + } + } + ] } } } diff --git a/source/services/shopping/embedded.openrpc.json b/source/services/shopping/embedded.openrpc.json index e058cde2b..c7216e5c4 100644 --- a/source/services/shopping/embedded.openrpc.json +++ b/source/services/shopping/embedded.openrpc.json @@ -2,7 +2,6 @@ "openrpc": "1.3.2", "info": { "title": "UCP Shopping Embedded Protocol", - "version": "2026-01-11", "description": "Embedded Protocol (EP) methods for the UCP shopping service. Methods are sent from Merchant to Host via postMessage using JSON-RPC 2.0. Method prefixes indicate capability scope: ec.* (checkout). Future capabilities may define additional prefixes (e.g., eo.* for order). Schema references are logical pointers - actual payload shape is determined by negotiated capabilities.\n\nEmbedded Protocol is a client-to-client postMessage interface, so there is no endpoint URL to access this API. Platforms detect merchant support for this protocol based on the presence of `services[\"dev.ucp.shopping\"][transport=embedded]` in discovery responses." }, "servers": [], @@ -237,7 +236,31 @@ } } } + }, + + { + "name": "ec.window.open_request", + "summary": "Request window open", + "description": "The buyer activated a link within checkout. The host MUST present the content to the buyer and respond with a success result, or respond with a window_open_rejected_error error if host policy prevented the navigation.", + "params": [ + { + "name": "url", + "required": true, + "schema": { + "type": "string", + "format": "uri", + "description": "The URL of the resource to present." + } + } + ], + "result": { + "name": "windowOpenResult", + "schema": { + "type": "object", + "description": "Acknowledgement that the host handled the request." + } + } } ], - "x-delegations": ["payment.instruments_change", "payment.credential"] + "x-delegations": ["payment.instruments_change", "payment.credential", "window.open"] } diff --git a/source/services/shopping/mcp.openrpc.json b/source/services/shopping/mcp.openrpc.json index c347234a7..1d7654cd6 100644 --- a/source/services/shopping/mcp.openrpc.json +++ b/source/services/shopping/mcp.openrpc.json @@ -2,7 +2,6 @@ "openrpc": "1.3.2", "info": { "title": "UCP Shopping Service", - "version": "2026-01-11", "description": "Canonical MCP/JSON-RPC interface for UCP Shopping service. Schema references are logical pointers - actual payload shape is determined by negotiated capabilities.\n\n**Endpoint Resolution:** This spec defines methods only. The endpoint URL MUST be obtained from the merchant's discovery profile at `/.well-known/ucp` under `services[\"dev.ucp.shopping\"][transport=mcp].endpoint`. The server entry below is a placeholder for tooling compatibility." }, "servers": [ @@ -20,6 +19,18 @@ ], "components": { "schemas": { + "checkout_result": { + "oneOf": [ + { "$ref": "../../schemas/shopping/checkout.json" }, + { "$ref": "../../schemas/shopping/types/error_response.json" } + ] + }, + "cart_result": { + "oneOf": [ + { "$ref": "../../schemas/shopping/cart.json" }, + { "$ref": "../../schemas/shopping/types/error_response.json" } + ] + }, "meta": { "type": "object", "description": "Request metadata.", @@ -70,7 +81,7 @@ ], "result": { "name": "checkout", - "schema": {"$ref": "../../schemas/shopping/checkout.json"} + "schema": {"$ref": "#/components/schemas/checkout_result"} } }, { @@ -90,7 +101,7 @@ ], "result": { "name": "checkout", - "schema": {"$ref": "../../schemas/shopping/checkout.json"} + "schema": {"$ref": "#/components/schemas/checkout_result"} } }, { @@ -115,7 +126,7 @@ ], "result": { "name": "checkout", - "schema": {"$ref": "../../schemas/shopping/checkout.json"} + "schema": {"$ref": "#/components/schemas/checkout_result"} } }, { @@ -145,7 +156,7 @@ ], "result": { "name": "checkout", - "schema": {"$ref": "../../schemas/shopping/checkout.json"} + "schema": {"$ref": "#/components/schemas/checkout_result"} } }, { @@ -170,7 +181,7 @@ ], "result": { "name": "checkout", - "schema": {"$ref": "../../schemas/shopping/checkout.json"} + "schema": {"$ref": "#/components/schemas/checkout_result"} } }, { @@ -191,7 +202,7 @@ ], "result": { "name": "cart", - "schema": {"$ref": "../../schemas/shopping/cart.json"} + "schema": {"$ref": "#/components/schemas/cart_result"} } }, { @@ -211,7 +222,7 @@ ], "result": { "name": "cart", - "schema": {"$ref": "../../schemas/shopping/cart.json"} + "schema": {"$ref": "#/components/schemas/cart_result"} } }, { @@ -236,7 +247,7 @@ ], "result": { "name": "cart", - "schema": {"$ref": "../../schemas/shopping/cart.json"} + "schema": {"$ref": "#/components/schemas/cart_result"} } }, { @@ -261,7 +272,49 @@ ], "result": { "name": "cart", - "schema": {"$ref": "../../schemas/shopping/cart.json"} + "schema": {"$ref": "#/components/schemas/cart_result"} + } + }, + { + "name": "search_catalog", + "summary": "Search for products in the catalog", + "description": "Search for products using query text, filters, and pagination.", + "params": [ + { + "name": "meta", + "required": true, + "schema": {"$ref": "#/components/schemas/meta"} + }, + { + "name": "catalog", + "required": true, + "schema": {"$ref": "../../schemas/shopping/catalog_search.json#/$defs/search_request"} + } + ], + "result": { + "name": "response", + "schema": {"$ref": "../../schemas/shopping/catalog_search.json#/$defs/search_response"} + } + }, + { + "name": "lookup_catalog", + "summary": "Batch lookup products or variants by identifier", + "description": "Batch lookup of products or variants by identifier. See Catalog Lookup capability for supported identifier types and resolution behavior.", + "params": [ + { + "name": "meta", + "required": true, + "schema": {"$ref": "#/components/schemas/meta"} + }, + { + "name": "catalog", + "required": true, + "schema": {"$ref": "../../schemas/shopping/catalog_lookup.json#/$defs/lookup_request"} + } + ], + "result": { + "name": "response", + "schema": {"$ref": "../../schemas/shopping/catalog_lookup.json#/$defs/lookup_response"} } } ] diff --git a/source/services/shopping/rest.openapi.json b/source/services/shopping/rest.openapi.json index 7622f4d3c..145693a78 100644 --- a/source/services/shopping/rest.openapi.json +++ b/source/services/shopping/rest.openapi.json @@ -2,7 +2,6 @@ "openapi": "3.1.0", "info": { "title": "UCP Shopping Service", - "version": "2026-01-11", "description": "Canonical REST interface for UCP Shopping service. Schema references are logical pointers - actual payload shape is determined by negotiated capabilities.\n\n**Endpoint Resolution:** This spec defines operations only. The base URL MUST be obtained from the merchant's discovery profile at `/.well-known/ucp` under `services[\"dev.ucp.shopping\"][transport=rest].endpoint`. The `{endpoint}` server variable below is a placeholder for tooling compatibility." }, "servers": [ @@ -89,7 +88,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/checkout" + "$ref": "#/components/schemas/checkout_response" } } } @@ -157,7 +156,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/checkout" + "$ref": "#/components/schemas/checkout_response" } } } @@ -234,7 +233,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/checkout" + "$ref": "#/components/schemas/checkout_response" } } } @@ -295,22 +294,7 @@ "required": true, "content": { "application/json": { - "schema": { - "allOf": [ - { "$ref": "#/components/schemas/checkout" }, - { - "title": "Risk Signals", - "description": "Optional risk signals for fraud detection.", - "type": "object", - "properties": { - "risk_signals": { - "type": "object", - "description": "Key-value pairs of risk signals." - } - } - } - ] - } + "schema": { "$ref": "#/components/schemas/checkout" } } } }, @@ -330,7 +314,7 @@ }, "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/checkout" } + "schema": { "$ref": "#/components/schemas/checkout_response" } } } } @@ -403,7 +387,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/checkout" + "$ref": "#/components/schemas/checkout_response" } } } @@ -448,7 +432,7 @@ }, "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/cart" } + "schema": { "$ref": "#/components/schemas/cart_response" } } } } @@ -486,12 +470,9 @@ }, "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/cart" } + "schema": { "$ref": "#/components/schemas/cart_response" } } } - }, - "404": { - "description": "Cart not found (cancelled, expired, or never existed)" } } }, @@ -532,7 +513,7 @@ }, "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/cart" } + "schema": { "$ref": "#/components/schemas/cart_response" } } } } @@ -572,7 +553,95 @@ }, "content": { "application/json": { - "schema": { "$ref": "#/components/schemas/cart" } + "schema": { "$ref": "#/components/schemas/cart_response" } + } + } + } + } + } + }, + "/catalog/search": { + "post": { + "operationId": "search_catalog", + "summary": "Search Catalog", + "description": "Search for products in the business's catalog.", + "parameters": [ + { "$ref": "#/components/parameters/authorization" }, + { "$ref": "#/components/parameters/x_api_key" }, + { "$ref": "#/components/parameters/signature" }, + { "$ref": "#/components/parameters/signature_input" }, + { "$ref": "#/components/parameters/content_digest" }, + { "$ref": "#/components/parameters/request_id" }, + { "$ref": "#/components/parameters/user_agent" }, + { "$ref": "#/components/parameters/ucp_agent" }, + { "$ref": "#/components/parameters/content_type" }, + { "$ref": "#/components/parameters/accept" }, + { "$ref": "#/components/parameters/accept_language" }, + { "$ref": "#/components/parameters/accept_encoding" } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/catalog_search_request" } + } + } + }, + "responses": { + "200": { + "description": "Search results", + "headers": { + "Signature": { "$ref": "#/components/headers/signature" }, + "Signature-Input": { "$ref": "#/components/headers/signature_input" }, + "Content-Digest": { "$ref": "#/components/headers/content_digest" } + }, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/catalog_search_response" } + } + } + } + } + } + }, + "/catalog/lookup": { + "post": { + "operationId": "lookup_catalog", + "summary": "Batch Lookup Catalog Items", + "description": "Lookup one or more products by ID with explicit market context. Supports batch lookups of multiple items in a single request.", + "parameters": [ + { "$ref": "#/components/parameters/authorization" }, + { "$ref": "#/components/parameters/x_api_key" }, + { "$ref": "#/components/parameters/signature" }, + { "$ref": "#/components/parameters/signature_input" }, + { "$ref": "#/components/parameters/content_digest" }, + { "$ref": "#/components/parameters/request_id" }, + { "$ref": "#/components/parameters/user_agent" }, + { "$ref": "#/components/parameters/ucp_agent" }, + { "$ref": "#/components/parameters/content_type" }, + { "$ref": "#/components/parameters/accept" }, + { "$ref": "#/components/parameters/accept_language" }, + { "$ref": "#/components/parameters/accept_encoding" } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/catalog_lookup_request" } + } + } + }, + "responses": { + "200": { + "description": "Lookup result", + "headers": { + "Signature": { "$ref": "#/components/headers/signature" }, + "Signature-Input": { "$ref": "#/components/headers/signature_input" }, + "Content-Digest": { "$ref": "#/components/headers/content_digest" } + }, + "content": { + "application/json": { + "schema": { "$ref": "#/components/schemas/catalog_lookup_response" } } } } @@ -808,9 +877,21 @@ "checkout": { "$ref": "../../schemas/shopping/checkout.json" }, + "checkout_response": { + "oneOf": [ + { "$ref": "../../schemas/shopping/checkout.json" }, + { "$ref": "../../schemas/shopping/types/error_response.json" } + ] + }, "cart": { "$ref": "../../schemas/shopping/cart.json" }, + "cart_response": { + "oneOf": [ + { "$ref": "../../schemas/shopping/cart.json" }, + { "$ref": "../../schemas/shopping/types/error_response.json" } + ] + }, "order": { "$ref": "../../schemas/shopping/order.json" }, @@ -819,6 +900,18 @@ }, "ucp": { "$ref": "../../schemas/ucp.json" + }, + "catalog_search_request": { + "$ref": "../../schemas/shopping/catalog_search.json#/$defs/search_request" + }, + "catalog_search_response": { + "$ref": "../../schemas/shopping/catalog_search.json#/$defs/search_response" + }, + "catalog_lookup_request": { + "$ref": "../../schemas/shopping/catalog_lookup.json#/$defs/lookup_request" + }, + "catalog_lookup_response": { + "$ref": "../../schemas/shopping/catalog_lookup.json#/$defs/lookup_response" } } } diff --git a/uv.lock b/uv.lock index 3401f91c5..d0fb7873f 100644 --- a/uv.lock +++ b/uv.lock @@ -45,16 +45,16 @@ wheels = [ [[package]] name = "backrefs" -version = "6.1" +version = "6.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/86/e3/bb3a439d5cb255c4774724810ad8073830fac9c9dee123555820c1bcc806/backrefs-6.1.tar.gz", hash = "sha256:3bba1749aafe1db9b915f00e0dd166cba613b6f788ffd63060ac3485dc9be231", size = 7011962, upload-time = "2025-11-15T14:52:08.323Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/a6/e325ec73b638d3ede4421b5445d4a0b8b219481826cc079d510100af356c/backrefs-6.2.tar.gz", hash = "sha256:f44ff4d48808b243b6c0cdc6231e22195c32f77046018141556c66f8bab72a49", size = 7012303, upload-time = "2026-02-16T19:10:15.828Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/ee/c216d52f58ea75b5e1841022bbae24438b19834a29b163cb32aa3a2a7c6e/backrefs-6.1-py310-none-any.whl", hash = "sha256:2a2ccb96302337ce61ee4717ceacfbf26ba4efb1d55af86564b8bbaeda39cac1", size = 381059, upload-time = "2025-11-15T14:51:59.758Z" }, - { url = "https://files.pythonhosted.org/packages/e6/9a/8da246d988ded941da96c7ed945d63e94a445637eaad985a0ed88787cb89/backrefs-6.1-py311-none-any.whl", hash = "sha256:e82bba3875ee4430f4de4b6db19429a27275d95a5f3773c57e9e18abc23fd2b7", size = 392854, upload-time = "2025-11-15T14:52:01.194Z" }, - { url = "https://files.pythonhosted.org/packages/37/c9/fd117a6f9300c62bbc33bc337fd2b3c6bfe28b6e9701de336b52d7a797ad/backrefs-6.1-py312-none-any.whl", hash = "sha256:c64698c8d2269343d88947c0735cb4b78745bd3ba590e10313fbf3f78c34da5a", size = 398770, upload-time = "2025-11-15T14:52:02.584Z" }, - { url = "https://files.pythonhosted.org/packages/eb/95/7118e935b0b0bd3f94dfec2d852fd4e4f4f9757bdb49850519acd245cd3a/backrefs-6.1-py313-none-any.whl", hash = "sha256:4c9d3dc1e2e558965202c012304f33d4e0e477e1c103663fd2c3cc9bb18b0d05", size = 400726, upload-time = "2025-11-15T14:52:04.093Z" }, - { url = "https://files.pythonhosted.org/packages/1d/72/6296bad135bfafd3254ae3648cd152980a424bd6fed64a101af00cc7ba31/backrefs-6.1-py314-none-any.whl", hash = "sha256:13eafbc9ccd5222e9c1f0bec563e6d2a6d21514962f11e7fc79872fd56cbc853", size = 412584, upload-time = "2025-11-15T14:52:05.233Z" }, - { url = "https://files.pythonhosted.org/packages/02/e3/a4fa1946722c4c7b063cc25043a12d9ce9b4323777f89643be74cef2993c/backrefs-6.1-py39-none-any.whl", hash = "sha256:a9e99b8a4867852cad177a6430e31b0f6e495d65f8c6c134b68c14c3c95bf4b0", size = 381058, upload-time = "2025-11-15T14:52:06.698Z" }, + { url = "https://files.pythonhosted.org/packages/1b/39/3765df263e08a4df37f4f43cb5aa3c6c17a4bdd42ecfe841e04c26037171/backrefs-6.2-py310-none-any.whl", hash = "sha256:0fdc7b012420b6b144410342caeb8adc54c6866cf12064abc9bb211302e496f8", size = 381075, upload-time = "2026-02-16T19:10:04.322Z" }, + { url = "https://files.pythonhosted.org/packages/0f/f0/35240571e1b67ffb19dafb29ab34150b6f59f93f717b041082cdb1bfceb1/backrefs-6.2-py311-none-any.whl", hash = "sha256:08aa7fae530c6b2361d7bdcbda1a7c454e330cc9dbcd03f5c23205e430e5c3be", size = 392874, upload-time = "2026-02-16T19:10:06.314Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/77e8c9745b4d227cce9f5e0a6f68041278c5f9b18588b35905f5f19c1beb/backrefs-6.2-py312-none-any.whl", hash = "sha256:c3f4b9cb2af8cda0d87ab4f57800b57b95428488477be164dd2b47be54db0c90", size = 398787, upload-time = "2026-02-16T19:10:08.274Z" }, + { url = "https://files.pythonhosted.org/packages/c5/71/c754b1737ad99102e03fa3235acb6cb6d3ac9d6f596cbc3e5f236705abd8/backrefs-6.2-py313-none-any.whl", hash = "sha256:12df81596ab511f783b7d87c043ce26bc5b0288cf3bb03610fe76b8189282b2b", size = 400747, upload-time = "2026-02-16T19:10:09.791Z" }, + { url = "https://files.pythonhosted.org/packages/af/75/be12ba31a6eb20dccef2320cd8ccb3f7d9013b68ba4c70156259fee9e409/backrefs-6.2-py314-none-any.whl", hash = "sha256:e5f805ae09819caa1aa0623b4a83790e7028604aa2b8c73ba602c4454e665de7", size = 412602, upload-time = "2026-02-16T19:10:12.317Z" }, + { url = "https://files.pythonhosted.org/packages/21/f8/d02f650c47d05034dcd6f9c8cf94f39598b7a89c00ecda0ecb2911bc27e9/backrefs-6.2-py39-none-any.whl", hash = "sha256:664e33cd88c6840b7625b826ecf2555f32d491800900f5a541f772c485f7cda7", size = 381077, upload-time = "2026-02-16T19:10:13.74Z" }, ] [[package]] @@ -72,7 +72,7 @@ wheels = [ [[package]] name = "black" -version = "26.1.0" +version = "26.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, @@ -84,34 +84,34 @@ dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/13/88/560b11e521c522440af991d46848a2bde64b5f7202ec14e1f46f9509d328/black-26.1.0.tar.gz", hash = "sha256:d294ac3340eef9c9eb5d29288e96dc719ff269a88e27b396340459dd85da4c58", size = 658785, upload-time = "2026-01-18T04:50:11.993Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/1b/523329e713f965ad0ea2b7a047eeb003007792a0353622ac7a8cb2ee6fef/black-26.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ca699710dece84e3ebf6e92ee15f5b8f72870ef984bf944a57a777a48357c168", size = 1849661, upload-time = "2026-01-18T04:59:12.425Z" }, - { url = "https://files.pythonhosted.org/packages/14/82/94c0640f7285fa71c2f32879f23e609dd2aa39ba2641f395487f24a578e7/black-26.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5e8e75dabb6eb83d064b0db46392b25cabb6e784ea624219736e8985a6b3675d", size = 1689065, upload-time = "2026-01-18T04:59:13.993Z" }, - { url = "https://files.pythonhosted.org/packages/f0/78/474373cbd798f9291ed8f7107056e343fd39fef42de4a51c7fd0d360840c/black-26.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb07665d9a907a1a645ee41a0df8a25ffac8ad9c26cdb557b7b88eeeeec934e0", size = 1751502, upload-time = "2026-01-18T04:59:15.971Z" }, - { url = "https://files.pythonhosted.org/packages/29/89/59d0e350123f97bc32c27c4d79563432d7f3530dca2bff64d855c178af8b/black-26.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:7ed300200918147c963c87700ccf9966dceaefbbb7277450a8d646fc5646bf24", size = 1400102, upload-time = "2026-01-18T04:59:17.8Z" }, - { url = "https://files.pythonhosted.org/packages/e1/bc/5d866c7ae1c9d67d308f83af5462ca7046760158bbf142502bad8f22b3a1/black-26.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:c5b7713daea9bf943f79f8c3b46f361cc5229e0e604dcef6a8bb6d1c37d9df89", size = 1207038, upload-time = "2026-01-18T04:59:19.543Z" }, - { url = "https://files.pythonhosted.org/packages/30/83/f05f22ff13756e1a8ce7891db517dbc06200796a16326258268f4658a745/black-26.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3cee1487a9e4c640dc7467aaa543d6c0097c391dc8ac74eb313f2fbf9d7a7cb5", size = 1831956, upload-time = "2026-01-18T04:59:21.38Z" }, - { url = "https://files.pythonhosted.org/packages/7d/f2/b2c570550e39bedc157715e43927360312d6dd677eed2cc149a802577491/black-26.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d62d14ca31c92adf561ebb2e5f2741bf8dea28aef6deb400d49cca011d186c68", size = 1672499, upload-time = "2026-01-18T04:59:23.257Z" }, - { url = "https://files.pythonhosted.org/packages/7a/d7/990d6a94dc9e169f61374b1c3d4f4dd3037e93c2cc12b6f3b12bc663aa7b/black-26.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb1dafbbaa3b1ee8b4550a84425aac8874e5f390200f5502cf3aee4a2acb2f14", size = 1735431, upload-time = "2026-01-18T04:59:24.729Z" }, - { url = "https://files.pythonhosted.org/packages/36/1c/cbd7bae7dd3cb315dfe6eeca802bb56662cc92b89af272e014d98c1f2286/black-26.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:101540cb2a77c680f4f80e628ae98bd2bd8812fb9d72ade4f8995c5ff019e82c", size = 1400468, upload-time = "2026-01-18T04:59:27.381Z" }, - { url = "https://files.pythonhosted.org/packages/59/b1/9fe6132bb2d0d1f7094613320b56297a108ae19ecf3041d9678aec381b37/black-26.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:6f3977a16e347f1b115662be07daa93137259c711e526402aa444d7a88fdc9d4", size = 1207332, upload-time = "2026-01-18T04:59:28.711Z" }, - { url = "https://files.pythonhosted.org/packages/f5/13/710298938a61f0f54cdb4d1c0baeb672c01ff0358712eddaf29f76d32a0b/black-26.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6eeca41e70b5f5c84f2f913af857cf2ce17410847e1d54642e658e078da6544f", size = 1878189, upload-time = "2026-01-18T04:59:30.682Z" }, - { url = "https://files.pythonhosted.org/packages/79/a6/5179beaa57e5dbd2ec9f1c64016214057b4265647c62125aa6aeffb05392/black-26.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dd39eef053e58e60204f2cdf059e2442e2eb08f15989eefe259870f89614c8b6", size = 1700178, upload-time = "2026-01-18T04:59:32.387Z" }, - { url = "https://files.pythonhosted.org/packages/8c/04/c96f79d7b93e8f09d9298b333ca0d31cd9b2ee6c46c274fd0f531de9dc61/black-26.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9459ad0d6cd483eacad4c6566b0f8e42af5e8b583cee917d90ffaa3778420a0a", size = 1777029, upload-time = "2026-01-18T04:59:33.767Z" }, - { url = "https://files.pythonhosted.org/packages/49/f9/71c161c4c7aa18bdda3776b66ac2dc07aed62053c7c0ff8bbda8c2624fe2/black-26.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a19915ec61f3a8746e8b10adbac4a577c6ba9851fa4a9e9fbfbcf319887a5791", size = 1406466, upload-time = "2026-01-18T04:59:35.177Z" }, - { url = "https://files.pythonhosted.org/packages/4a/8b/a7b0f974e473b159d0ac1b6bcefffeb6bec465898a516ee5cc989503cbc7/black-26.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:643d27fb5facc167c0b1b59d0315f2674a6e950341aed0fc05cf307d22bf4954", size = 1216393, upload-time = "2026-01-18T04:59:37.18Z" }, - { url = "https://files.pythonhosted.org/packages/79/04/fa2f4784f7237279332aa735cdfd5ae2e7730db0072fb2041dadda9ae551/black-26.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ba1d768fbfb6930fc93b0ecc32a43d8861ded16f47a40f14afa9bb04ab93d304", size = 1877781, upload-time = "2026-01-18T04:59:39.054Z" }, - { url = "https://files.pythonhosted.org/packages/cf/ad/5a131b01acc0e5336740a039628c0ab69d60cf09a2c87a4ec49f5826acda/black-26.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b807c240b64609cb0e80d2200a35b23c7df82259f80bef1b2c96eb422b4aac9", size = 1699670, upload-time = "2026-01-18T04:59:41.005Z" }, - { url = "https://files.pythonhosted.org/packages/da/7c/b05f22964316a52ab6b4265bcd52c0ad2c30d7ca6bd3d0637e438fc32d6e/black-26.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1de0f7d01cc894066a1153b738145b194414cc6eeaad8ef4397ac9abacf40f6b", size = 1775212, upload-time = "2026-01-18T04:59:42.545Z" }, - { url = "https://files.pythonhosted.org/packages/a6/a3/e8d1526bea0446e040193185353920a9506eab60a7d8beb062029129c7d2/black-26.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:91a68ae46bf07868963671e4d05611b179c2313301bd756a89ad4e3b3db2325b", size = 1409953, upload-time = "2026-01-18T04:59:44.357Z" }, - { url = "https://files.pythonhosted.org/packages/c7/5a/d62ebf4d8f5e3a1daa54adaab94c107b57be1b1a2f115a0249b41931e188/black-26.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:be5e2fe860b9bd9edbf676d5b60a9282994c03fbbd40fe8f5e75d194f96064ca", size = 1217707, upload-time = "2026-01-18T04:59:45.719Z" }, - { url = "https://files.pythonhosted.org/packages/6a/83/be35a175aacfce4b05584ac415fd317dd6c24e93a0af2dcedce0f686f5d8/black-26.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9dc8c71656a79ca49b8d3e2ce8103210c9481c57798b48deeb3a8bb02db5f115", size = 1871864, upload-time = "2026-01-18T04:59:47.586Z" }, - { url = "https://files.pythonhosted.org/packages/a5/f5/d33696c099450b1274d925a42b7a030cd3ea1f56d72e5ca8bbed5f52759c/black-26.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b22b3810451abe359a964cc88121d57f7bce482b53a066de0f1584988ca36e79", size = 1701009, upload-time = "2026-01-18T04:59:49.443Z" }, - { url = "https://files.pythonhosted.org/packages/1b/87/670dd888c537acb53a863bc15abbd85b22b429237d9de1b77c0ed6b79c42/black-26.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53c62883b3f999f14e5d30b5a79bd437236658ad45b2f853906c7cbe79de00af", size = 1767806, upload-time = "2026-01-18T04:59:50.769Z" }, - { url = "https://files.pythonhosted.org/packages/fe/9c/cd3deb79bfec5bcf30f9d2100ffeec63eecce826eb63e3961708b9431ff1/black-26.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:f016baaadc423dc960cdddf9acae679e71ee02c4c341f78f3179d7e4819c095f", size = 1433217, upload-time = "2026-01-18T04:59:52.218Z" }, - { url = "https://files.pythonhosted.org/packages/4e/29/f3be41a1cf502a283506f40f5d27203249d181f7a1a2abce1c6ce188035a/black-26.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:66912475200b67ef5a0ab665011964bf924745103f51977a78b4fb92a9fc1bf0", size = 1245773, upload-time = "2026-01-18T04:59:54.457Z" }, - { url = "https://files.pythonhosted.org/packages/e4/3d/51bdb3ecbfadfaf825ec0c75e1de6077422b4afa2091c6c9ba34fbfc0c2d/black-26.1.0-py3-none-any.whl", hash = "sha256:1054e8e47ebd686e078c0bb0eaf31e6ce69c966058d122f2c0c950311f9f3ede", size = 204010, upload-time = "2026-01-18T04:50:09.978Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/e1/c5/61175d618685d42b005847464b8fb4743a67b1b8fdb75e50e5a96c31a27a/black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07", size = 666155, upload-time = "2026-03-12T03:36:03.593Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/a8/11170031095655d36ebc6664fe0897866f6023892396900eec0e8fdc4299/black-26.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:86a8b5035fce64f5dcd1b794cf8ec4d31fe458cf6ce3986a30deb434df82a1d2", size = 1866562, upload-time = "2026-03-12T03:39:58.639Z" }, + { url = "https://files.pythonhosted.org/packages/69/ce/9e7548d719c3248c6c2abfd555d11169457cbd584d98d179111338423790/black-26.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5602bdb96d52d2d0672f24f6ffe5218795736dd34807fd0fd55ccd6bf206168b", size = 1703623, upload-time = "2026-03-12T03:40:00.347Z" }, + { url = "https://files.pythonhosted.org/packages/7f/0a/8d17d1a9c06f88d3d030d0b1d4373c1551146e252afe4547ed601c0e697f/black-26.3.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c54a4a82e291a1fee5137371ab488866b7c86a3305af4026bdd4dc78642e1ac", size = 1768388, upload-time = "2026-03-12T03:40:01.765Z" }, + { url = "https://files.pythonhosted.org/packages/52/79/c1ee726e221c863cde5164f925bacf183dfdf0397d4e3f94889439b947b4/black-26.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:6e131579c243c98f35bce64a7e08e87fb2d610544754675d4a0e73a070a5aa3a", size = 1412969, upload-time = "2026-03-12T03:40:03.252Z" }, + { url = "https://files.pythonhosted.org/packages/73/a5/15c01d613f5756f68ed8f6d4ec0a1e24b82b18889fa71affd3d1f7fad058/black-26.3.1-cp310-cp310-win_arm64.whl", hash = "sha256:5ed0ca58586c8d9a487352a96b15272b7fa55d139fc8496b519e78023a8dab0a", size = 1220345, upload-time = "2026-03-12T03:40:04.892Z" }, + { url = "https://files.pythonhosted.org/packages/17/57/5f11c92861f9c92eb9dddf515530bc2d06db843e44bdcf1c83c1427824bc/black-26.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:28ef38aee69e4b12fda8dba75e21f9b4f979b490c8ac0baa7cb505369ac9e1ff", size = 1851987, upload-time = "2026-03-12T03:40:06.248Z" }, + { url = "https://files.pythonhosted.org/packages/54/aa/340a1463660bf6831f9e39646bf774086dbd8ca7fc3cded9d59bbdf4ad0a/black-26.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf9bf162ed91a26f1adba8efda0b573bc6924ec1408a52cc6f82cb73ec2b142c", size = 1689499, upload-time = "2026-03-12T03:40:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/f3/01/b726c93d717d72733da031d2de10b92c9fa4c8d0c67e8a8a372076579279/black-26.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:474c27574d6d7037c1bc875a81d9be0a9a4f9ee95e62800dab3cfaadbf75acd5", size = 1754369, upload-time = "2026-03-12T03:40:09.279Z" }, + { url = "https://files.pythonhosted.org/packages/e3/09/61e91881ca291f150cfc9eb7ba19473c2e59df28859a11a88248b5cbbc4d/black-26.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:5e9d0d86df21f2e1677cc4bd090cd0e446278bcbbe49bf3659c308c3e402843e", size = 1413613, upload-time = "2026-03-12T03:40:10.943Z" }, + { url = "https://files.pythonhosted.org/packages/16/73/544f23891b22e7efe4d8f812371ab85b57f6a01b2fc45e3ba2e52ba985b8/black-26.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:9a5e9f45e5d5e1c5b5c29b3bd4265dcc90e8b92cf4534520896ed77f791f4da5", size = 1219719, upload-time = "2026-03-12T03:40:12.597Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f8/da5eae4fc75e78e6dceb60624e1b9662ab00d6b452996046dfa9b8a6025b/black-26.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e6f89631eb88a7302d416594a32faeee9fb8fb848290da9d0a5f2903519fc1", size = 1895920, upload-time = "2026-03-12T03:40:13.921Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9f/04e6f26534da2e1629b2b48255c264cabf5eedc5141d04516d9d68a24111/black-26.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cd2012d35b47d589cb8a16faf8a32ef7a336f56356babd9fcf70939ad1897f", size = 1718499, upload-time = "2026-03-12T03:40:15.239Z" }, + { url = "https://files.pythonhosted.org/packages/04/91/a5935b2a63e31b331060c4a9fdb5a6c725840858c599032a6f3aac94055f/black-26.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f76ff19ec5297dd8e66eb64deda23631e642c9393ab592826fd4bdc97a4bce7", size = 1794994, upload-time = "2026-03-12T03:40:17.124Z" }, + { url = "https://files.pythonhosted.org/packages/e7/0a/86e462cdd311a3c2a8ece708d22aba17d0b2a0d5348ca34b40cdcbea512e/black-26.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ddb113db38838eb9f043623ba274cfaf7d51d5b0c22ecb30afe58b1bb8322983", size = 1420867, upload-time = "2026-03-12T03:40:18.83Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e5/22515a19cb7eaee3440325a6b0d95d2c0e88dd180cb011b12ae488e031d1/black-26.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:dfdd51fc3e64ea4f35873d1b3fb25326773d55d2329ff8449139ebaad7357efb", size = 1230124, upload-time = "2026-03-12T03:40:20.425Z" }, + { url = "https://files.pythonhosted.org/packages/f5/77/5728052a3c0450c53d9bb3945c4c46b91baa62b2cafab6801411b6271e45/black-26.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:855822d90f884905362f602880ed8b5df1b7e3ee7d0db2502d4388a954cc8c54", size = 1895034, upload-time = "2026-03-12T03:40:21.813Z" }, + { url = "https://files.pythonhosted.org/packages/52/73/7cae55fdfdfbe9d19e9a8d25d145018965fe2079fa908101c3733b0c55a0/black-26.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f", size = 1718503, upload-time = "2026-03-12T03:40:23.666Z" }, + { url = "https://files.pythonhosted.org/packages/e1/87/af89ad449e8254fdbc74654e6467e3c9381b61472cc532ee350d28cfdafb/black-26.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56", size = 1793557, upload-time = "2026-03-12T03:40:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/43/10/d6c06a791d8124b843bf325ab4ac7d2f5b98731dff84d6064eafd687ded1/black-26.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:c7e72339f841b5a237ff14f7d3880ddd0fc7f98a1199e8c4327f9a4f478c1839", size = 1422766, upload-time = "2026-03-12T03:40:27.14Z" }, + { url = "https://files.pythonhosted.org/packages/59/4f/40a582c015f2d841ac24fed6390bd68f0fc896069ff3a886317959c9daf8/black-26.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc622538b430aa4c8c853f7f63bc582b3b8030fd8c80b70fb5fa5b834e575c2", size = 1232140, upload-time = "2026-03-12T03:40:28.882Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/e36e27c9cebc1311b7579210df6f1c86e50f2d7143ae4fcf8a5017dc8809/black-26.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2d6bfaf7fd0993b420bed691f20f9492d53ce9a2bcccea4b797d34e947318a78", size = 1889234, upload-time = "2026-03-12T03:40:30.964Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7b/9871acf393f64a5fa33668c19350ca87177b181f44bb3d0c33b2d534f22c/black-26.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f89f2ab047c76a9c03f78d0d66ca519e389519902fa27e7a91117ef7611c0568", size = 1720522, upload-time = "2026-03-12T03:40:32.346Z" }, + { url = "https://files.pythonhosted.org/packages/03/87/e766c7f2e90c07fb7586cc787c9ae6462b1eedab390191f2b7fc7f6170a9/black-26.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b07fc0dab849d24a80a29cfab8d8a19187d1c4685d8a5e6385a5ce323c1f015f", size = 1787824, upload-time = "2026-03-12T03:40:33.636Z" }, + { url = "https://files.pythonhosted.org/packages/ac/94/2424338fb2d1875e9e83eed4c8e9c67f6905ec25afd826a911aea2b02535/black-26.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:0126ae5b7c09957da2bdbd91a9ba1207453feada9e9fe51992848658c6c8e01c", size = 1445855, upload-time = "2026-03-12T03:40:35.442Z" }, + { url = "https://files.pythonhosted.org/packages/86/43/0c3338bd928afb8ee7471f1a4eec3bdbe2245ccb4a646092a222e8669840/black-26.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:92c0ec1f2cc149551a2b7b47efc32c866406b6891b0ee4625e95967c8f4acfb1", size = 1258109, upload-time = "2026-03-12T03:40:36.832Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/52d98722666d6fc6c3dd4c76df339501d6efd40e0ff95e6186a7b7f0befd/black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b", size = 207542, upload-time = "2026-03-12T03:36:01.668Z" }, ] [[package]] @@ -345,15 +345,15 @@ wheels = [ [[package]] name = "cssselect2" -version = "0.8.0" +version = "0.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "tinycss2" }, { name = "webencodings" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9f/86/fd7f58fc498b3166f3a7e8e0cddb6e620fe1da35b02248b1bd59e95dbaaa/cssselect2-0.8.0.tar.gz", hash = "sha256:7674ffb954a3b46162392aee2a3a0aedb2e14ecf99fcc28644900f4e6e3e9d3a", size = 35716, upload-time = "2025-03-05T14:46:07.988Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/20/92eaa6b0aec7189fa4b75c890640e076e9e793095721db69c5c81142c2e1/cssselect2-0.9.0.tar.gz", hash = "sha256:759aa22c216326356f65e62e791d66160a0f9c91d1424e8d8adc5e74dddfc6fb", size = 35595, upload-time = "2026-02-12T17:16:39.614Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/e7/aa315e6a749d9b96c2504a1ba0ba031ba2d0517e972ce22682e3fccecb09/cssselect2-0.8.0-py3-none-any.whl", hash = "sha256:46fc70ebc41ced7a32cd42d58b1884d72ade23d21e5a4eaaf022401c13f0e76e", size = 15454, upload-time = "2025-03-05T14:46:06.463Z" }, + { url = "https://files.pythonhosted.org/packages/21/0e/8459ca4413e1a21a06c97d134bfaf18adfd27cea068813dc0faae06cbf00/cssselect2-0.9.0-py3-none-any.whl", hash = "sha256:6a99e5f91f9a016a304dd929b0966ca464bcfda15177b6fb4a118fc0fb5d9563", size = 15453, upload-time = "2026-02-12T17:16:38.317Z" }, ] [[package]] @@ -791,7 +791,7 @@ wheels = [ [[package]] name = "mkdocs-material" -version = "9.7.1" +version = "9.7.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "babel" }, @@ -806,9 +806,9 @@ dependencies = [ { name = "pymdown-extensions" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/27/e2/2ffc356cd72f1473d07c7719d82a8f2cbd261666828614ecb95b12169f41/mkdocs_material-9.7.1.tar.gz", hash = "sha256:89601b8f2c3e6c6ee0a918cc3566cb201d40bf37c3cd3c2067e26fadb8cce2b8", size = 4094392, upload-time = "2025-12-18T09:49:00.308Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/ce/a1cd02ac7448763f0bb56aaf5f23fa2527944ac6df335080c38c2f253165/mkdocs_material-9.7.4.tar.gz", hash = "sha256:711b0ee63aca9a8c7124d4c73e83a25aa996e27e814767c3a3967df1b9e56f32", size = 4097804, upload-time = "2026-03-03T19:57:36.827Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3e/32/ed071cb721aca8c227718cffcf7bd539620e9799bbf2619e90c757bfd030/mkdocs_material-9.7.1-py3-none-any.whl", hash = "sha256:3f6100937d7d731f87f1e3e3b021c97f7239666b9ba1151ab476cabb96c60d5c", size = 9297166, upload-time = "2025-12-18T09:48:56.664Z" }, + { url = "https://files.pythonhosted.org/packages/e7/94/e3535a9ed078b238df3df75a44694ca0ff5772fd538df4939c658a58c59d/mkdocs_material-9.7.4-py3-none-any.whl", hash = "sha256:6549ad95e4d130ed5099759dfa76ea34c593eefdb9c18c97273605518e99cfbf", size = 9305224, upload-time = "2026-03-03T19:57:34.063Z" }, ] [package.optional-dependencies] @@ -894,104 +894,100 @@ wheels = [ [[package]] name = "pillow" -version = "11.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/5d/45a3553a253ac8763f3561371432a90bdbe6000fbdcf1397ffe502aa206c/pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860", size = 5316554, upload-time = "2025-07-01T09:13:39.342Z" }, - { url = "https://files.pythonhosted.org/packages/7c/c8/67c12ab069ef586a25a4a79ced553586748fad100c77c0ce59bb4983ac98/pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad", size = 4686548, upload-time = "2025-07-01T09:13:41.835Z" }, - { url = "https://files.pythonhosted.org/packages/2f/bd/6741ebd56263390b382ae4c5de02979af7f8bd9807346d068700dd6d5cf9/pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0", size = 5859742, upload-time = "2025-07-03T13:09:47.439Z" }, - { url = "https://files.pythonhosted.org/packages/ca/0b/c412a9e27e1e6a829e6ab6c2dca52dd563efbedf4c9c6aa453d9a9b77359/pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b", size = 7633087, upload-time = "2025-07-03T13:09:51.796Z" }, - { url = "https://files.pythonhosted.org/packages/59/9d/9b7076aaf30f5dd17e5e5589b2d2f5a5d7e30ff67a171eb686e4eecc2adf/pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50", size = 5963350, upload-time = "2025-07-01T09:13:43.865Z" }, - { url = "https://files.pythonhosted.org/packages/f0/16/1a6bf01fb622fb9cf5c91683823f073f053005c849b1f52ed613afcf8dae/pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae", size = 6631840, upload-time = "2025-07-01T09:13:46.161Z" }, - { url = "https://files.pythonhosted.org/packages/7b/e6/6ff7077077eb47fde78739e7d570bdcd7c10495666b6afcd23ab56b19a43/pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9", size = 6074005, upload-time = "2025-07-01T09:13:47.829Z" }, - { url = "https://files.pythonhosted.org/packages/c3/3a/b13f36832ea6d279a697231658199e0a03cd87ef12048016bdcc84131601/pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e", size = 6708372, upload-time = "2025-07-01T09:13:52.145Z" }, - { url = "https://files.pythonhosted.org/packages/6c/e4/61b2e1a7528740efbc70b3d581f33937e38e98ef3d50b05007267a55bcb2/pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6", size = 6277090, upload-time = "2025-07-01T09:13:53.915Z" }, - { url = "https://files.pythonhosted.org/packages/a9/d3/60c781c83a785d6afbd6a326ed4d759d141de43aa7365725cbcd65ce5e54/pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f", size = 6985988, upload-time = "2025-07-01T09:13:55.699Z" }, - { url = "https://files.pythonhosted.org/packages/9f/28/4f4a0203165eefb3763939c6789ba31013a2e90adffb456610f30f613850/pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f", size = 2422899, upload-time = "2025-07-01T09:13:57.497Z" }, - { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531, upload-time = "2025-07-01T09:13:59.203Z" }, - { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560, upload-time = "2025-07-01T09:14:01.101Z" }, - { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978, upload-time = "2025-07-03T13:09:55.638Z" }, - { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168, upload-time = "2025-07-03T13:10:00.37Z" }, - { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053, upload-time = "2025-07-01T09:14:04.491Z" }, - { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273, upload-time = "2025-07-01T09:14:06.235Z" }, - { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043, upload-time = "2025-07-01T09:14:07.978Z" }, - { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516, upload-time = "2025-07-01T09:14:10.233Z" }, - { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768, upload-time = "2025-07-01T09:14:11.921Z" }, - { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055, upload-time = "2025-07-01T09:14:13.623Z" }, - { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079, upload-time = "2025-07-01T09:14:15.268Z" }, - { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload-time = "2025-07-01T09:14:17.648Z" }, - { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload-time = "2025-07-01T09:14:19.828Z" }, - { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload-time = "2025-07-03T13:10:04.448Z" }, - { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652, upload-time = "2025-07-03T13:10:10.391Z" }, - { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787, upload-time = "2025-07-01T09:14:21.63Z" }, - { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload-time = "2025-07-01T09:14:23.321Z" }, - { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload-time = "2025-07-01T09:14:25.237Z" }, - { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload-time = "2025-07-01T09:14:27.053Z" }, - { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload-time = "2025-07-01T09:14:30.104Z" }, - { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload-time = "2025-07-01T09:14:31.899Z" }, - { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" }, - { url = "https://files.pythonhosted.org/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd", size = 2128328, upload-time = "2025-07-01T09:14:35.276Z" }, - { url = "https://files.pythonhosted.org/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8", size = 2170652, upload-time = "2025-07-01T09:14:37.203Z" }, - { url = "https://files.pythonhosted.org/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f", size = 2227443, upload-time = "2025-07-01T09:14:39.344Z" }, - { url = "https://files.pythonhosted.org/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c", size = 5278474, upload-time = "2025-07-01T09:14:41.843Z" }, - { url = "https://files.pythonhosted.org/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd", size = 4686038, upload-time = "2025-07-01T09:14:44.008Z" }, - { url = "https://files.pythonhosted.org/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e", size = 5864407, upload-time = "2025-07-03T13:10:15.628Z" }, - { url = "https://files.pythonhosted.org/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1", size = 7639094, upload-time = "2025-07-03T13:10:21.857Z" }, - { url = "https://files.pythonhosted.org/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805", size = 5973503, upload-time = "2025-07-01T09:14:45.698Z" }, - { url = "https://files.pythonhosted.org/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8", size = 6642574, upload-time = "2025-07-01T09:14:47.415Z" }, - { url = "https://files.pythonhosted.org/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2", size = 6084060, upload-time = "2025-07-01T09:14:49.636Z" }, - { url = "https://files.pythonhosted.org/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b", size = 6721407, upload-time = "2025-07-01T09:14:51.962Z" }, - { url = "https://files.pythonhosted.org/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3", size = 6273841, upload-time = "2025-07-01T09:14:54.142Z" }, - { url = "https://files.pythonhosted.org/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51", size = 6978450, upload-time = "2025-07-01T09:14:56.436Z" }, - { url = "https://files.pythonhosted.org/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580", size = 2423055, upload-time = "2025-07-01T09:14:58.072Z" }, - { url = "https://files.pythonhosted.org/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e", size = 5281110, upload-time = "2025-07-01T09:14:59.79Z" }, - { url = "https://files.pythonhosted.org/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d", size = 4689547, upload-time = "2025-07-01T09:15:01.648Z" }, - { url = "https://files.pythonhosted.org/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced", size = 5901554, upload-time = "2025-07-03T13:10:27.018Z" }, - { url = "https://files.pythonhosted.org/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c", size = 7669132, upload-time = "2025-07-03T13:10:33.01Z" }, - { url = "https://files.pythonhosted.org/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8", size = 6005001, upload-time = "2025-07-01T09:15:03.365Z" }, - { url = "https://files.pythonhosted.org/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59", size = 6668814, upload-time = "2025-07-01T09:15:05.655Z" }, - { url = "https://files.pythonhosted.org/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe", size = 6113124, upload-time = "2025-07-01T09:15:07.358Z" }, - { url = "https://files.pythonhosted.org/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c", size = 6747186, upload-time = "2025-07-01T09:15:09.317Z" }, - { url = "https://files.pythonhosted.org/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788", size = 6277546, upload-time = "2025-07-01T09:15:11.311Z" }, - { url = "https://files.pythonhosted.org/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31", size = 6985102, upload-time = "2025-07-01T09:15:13.164Z" }, - { url = "https://files.pythonhosted.org/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e", size = 2424803, upload-time = "2025-07-01T09:15:15.695Z" }, - { url = "https://files.pythonhosted.org/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12", size = 5278520, upload-time = "2025-07-01T09:15:17.429Z" }, - { url = "https://files.pythonhosted.org/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a", size = 4686116, upload-time = "2025-07-01T09:15:19.423Z" }, - { url = "https://files.pythonhosted.org/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632", size = 5864597, upload-time = "2025-07-03T13:10:38.404Z" }, - { url = "https://files.pythonhosted.org/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673", size = 7638246, upload-time = "2025-07-03T13:10:44.987Z" }, - { url = "https://files.pythonhosted.org/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027", size = 5973336, upload-time = "2025-07-01T09:15:21.237Z" }, - { url = "https://files.pythonhosted.org/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77", size = 6642699, upload-time = "2025-07-01T09:15:23.186Z" }, - { url = "https://files.pythonhosted.org/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874", size = 6083789, upload-time = "2025-07-01T09:15:25.1Z" }, - { url = "https://files.pythonhosted.org/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a", size = 6720386, upload-time = "2025-07-01T09:15:27.378Z" }, - { url = "https://files.pythonhosted.org/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214", size = 6370911, upload-time = "2025-07-01T09:15:29.294Z" }, - { url = "https://files.pythonhosted.org/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635", size = 7117383, upload-time = "2025-07-01T09:15:31.128Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6", size = 2511385, upload-time = "2025-07-01T09:15:33.328Z" }, - { url = "https://files.pythonhosted.org/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae", size = 5281129, upload-time = "2025-07-01T09:15:35.194Z" }, - { url = "https://files.pythonhosted.org/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653", size = 4689580, upload-time = "2025-07-01T09:15:37.114Z" }, - { url = "https://files.pythonhosted.org/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6", size = 5902860, upload-time = "2025-07-03T13:10:50.248Z" }, - { url = "https://files.pythonhosted.org/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36", size = 7670694, upload-time = "2025-07-03T13:10:56.432Z" }, - { url = "https://files.pythonhosted.org/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b", size = 6005888, upload-time = "2025-07-01T09:15:39.436Z" }, - { url = "https://files.pythonhosted.org/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477", size = 6670330, upload-time = "2025-07-01T09:15:41.269Z" }, - { url = "https://files.pythonhosted.org/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50", size = 6114089, upload-time = "2025-07-01T09:15:43.13Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b", size = 6748206, upload-time = "2025-07-01T09:15:44.937Z" }, - { url = "https://files.pythonhosted.org/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12", size = 6377370, upload-time = "2025-07-01T09:15:46.673Z" }, - { url = "https://files.pythonhosted.org/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db", size = 7121500, upload-time = "2025-07-01T09:15:48.512Z" }, - { url = "https://files.pythonhosted.org/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa", size = 2512835, upload-time = "2025-07-01T09:15:50.399Z" }, - { url = "https://files.pythonhosted.org/packages/6f/8b/209bd6b62ce8367f47e68a218bffac88888fdf2c9fcf1ecadc6c3ec1ebc7/pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967", size = 5270556, upload-time = "2025-07-01T09:16:09.961Z" }, - { url = "https://files.pythonhosted.org/packages/2e/e6/231a0b76070c2cfd9e260a7a5b504fb72da0a95279410fa7afd99d9751d6/pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe", size = 4654625, upload-time = "2025-07-01T09:16:11.913Z" }, - { url = "https://files.pythonhosted.org/packages/13/f4/10cf94fda33cb12765f2397fc285fa6d8eb9c29de7f3185165b702fc7386/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c", size = 4874207, upload-time = "2025-07-03T13:11:10.201Z" }, - { url = "https://files.pythonhosted.org/packages/72/c9/583821097dc691880c92892e8e2d41fe0a5a3d6021f4963371d2f6d57250/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25", size = 6583939, upload-time = "2025-07-03T13:11:15.68Z" }, - { url = "https://files.pythonhosted.org/packages/3b/8e/5c9d410f9217b12320efc7c413e72693f48468979a013ad17fd690397b9a/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27", size = 4957166, upload-time = "2025-07-01T09:16:13.74Z" }, - { url = "https://files.pythonhosted.org/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a", size = 5581482, upload-time = "2025-07-01T09:16:16.107Z" }, - { url = "https://files.pythonhosted.org/packages/d9/28/1000353d5e61498aaeaaf7f1e4b49ddb05f2c6575f9d4f9f914a3538b6e1/pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f", size = 6984596, upload-time = "2025-07-01T09:16:18.07Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566, upload-time = "2025-07-01T09:16:19.801Z" }, - { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618, upload-time = "2025-07-01T09:16:21.818Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248, upload-time = "2025-07-03T13:11:20.738Z" }, - { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963, upload-time = "2025-07-03T13:11:26.283Z" }, - { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170, upload-time = "2025-07-01T09:16:23.762Z" }, - { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505, upload-time = "2025-07-01T09:16:25.593Z" }, - { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" }, +version = "12.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/30/5bd3d794762481f8c8ae9c80e7b76ecea73b916959eb587521358ef0b2f9/pillow-12.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f1625b72740fdda5d77b4def688eb8fd6490975d06b909fd19f13f391e077e0", size = 5304099, upload-time = "2026-02-11T04:20:06.13Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c1/aab9e8f3eeb4490180e357955e15c2ef74b31f64790ff356c06fb6cf6d84/pillow-12.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:178aa072084bd88ec759052feca8e56cbb14a60b39322b99a049e58090479713", size = 4657880, upload-time = "2026-02-11T04:20:09.291Z" }, + { url = "https://files.pythonhosted.org/packages/f1/0a/9879e30d56815ad529d3985aeff5af4964202425c27261a6ada10f7cbf53/pillow-12.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b66e95d05ba806247aaa1561f080abc7975daf715c30780ff92a20e4ec546e1b", size = 6222587, upload-time = "2026-02-11T04:20:10.82Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5f/a1b72ff7139e4f89014e8d451442c74a774d5c43cd938fb0a9f878576b37/pillow-12.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89c7e895002bbe49cdc5426150377cbbc04767d7547ed145473f496dfa40408b", size = 8027678, upload-time = "2026-02-11T04:20:12.455Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c2/c7cb187dac79a3d22c3ebeae727abee01e077c8c7d930791dc592f335153/pillow-12.1.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a5cbdcddad0af3da87cb16b60d23648bc3b51967eb07223e9fed77a82b457c4", size = 6335777, upload-time = "2026-02-11T04:20:14.441Z" }, + { url = "https://files.pythonhosted.org/packages/0c/7b/f9b09a7804ec7336effb96c26d37c29d27225783dc1501b7d62dcef6ae25/pillow-12.1.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f51079765661884a486727f0729d29054242f74b46186026582b4e4769918e4", size = 7027140, upload-time = "2026-02-11T04:20:16.387Z" }, + { url = "https://files.pythonhosted.org/packages/98/b2/2fa3c391550bd421b10849d1a2144c44abcd966daadd2f7c12e19ea988c4/pillow-12.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:99c1506ea77c11531d75e3a412832a13a71c7ebc8192ab9e4b2e355555920e3e", size = 6449855, upload-time = "2026-02-11T04:20:18.554Z" }, + { url = "https://files.pythonhosted.org/packages/96/ff/9caf4b5b950c669263c39e96c78c0d74a342c71c4f43fd031bb5cb7ceac9/pillow-12.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36341d06738a9f66c8287cf8b876d24b18db9bd8740fa0672c74e259ad408cff", size = 7151329, upload-time = "2026-02-11T04:20:20.646Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f8/4b24841f582704da675ca535935bccb32b00a6da1226820845fac4a71136/pillow-12.1.1-cp310-cp310-win32.whl", hash = "sha256:6c52f062424c523d6c4db85518774cc3d50f5539dd6eed32b8f6229b26f24d40", size = 6325574, upload-time = "2026-02-11T04:20:22.43Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f9/9f6b01c0881d7036063aa6612ef04c0e2cad96be21325a1e92d0203f8e91/pillow-12.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:c6008de247150668a705a6338156efb92334113421ceecf7438a12c9a12dab23", size = 7032347, upload-time = "2026-02-11T04:20:23.932Z" }, + { url = "https://files.pythonhosted.org/packages/79/13/c7922edded3dcdaf10c59297540b72785620abc0538872c819915746757d/pillow-12.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:1a9b0ee305220b392e1124a764ee4265bd063e54a751a6b62eff69992f457fa9", size = 2453457, upload-time = "2026-02-11T04:20:25.392Z" }, + { url = "https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32", size = 5304084, upload-time = "2026-02-11T04:20:27.501Z" }, + { url = "https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38", size = 4657866, upload-time = "2026-02-11T04:20:29.827Z" }, + { url = "https://files.pythonhosted.org/packages/13/84/583a4558d492a179d31e4aae32eadce94b9acf49c0337c4ce0b70e0a01f2/pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5", size = 6232148, upload-time = "2026-02-11T04:20:31.329Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e2/53c43334bbbb2d3b938978532fbda8e62bb6e0b23a26ce8592f36bcc4987/pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090", size = 8038007, upload-time = "2026-02-11T04:20:34.225Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/3d0e79c8a9d58150dd98e199d7c1c56861027f3829a3a60b3c2784190180/pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af", size = 6345418, upload-time = "2026-02-11T04:20:35.858Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b", size = 7034590, upload-time = "2026-02-11T04:20:37.91Z" }, + { url = "https://files.pythonhosted.org/packages/af/bf/e6f65d3db8a8bbfeaf9e13cc0417813f6319863a73de934f14b2229ada18/pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5", size = 6458655, upload-time = "2026-02-11T04:20:39.496Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c2/66091f3f34a25894ca129362e510b956ef26f8fb67a0e6417bc5744e56f1/pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d", size = 7159286, upload-time = "2026-02-11T04:20:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5a/24bc8eb526a22f957d0cec6243146744966d40857e3d8deb68f7902ca6c1/pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c", size = 6328663, upload-time = "2026-02-11T04:20:43.184Z" }, + { url = "https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563", size = 7031448, upload-time = "2026-02-11T04:20:44.696Z" }, + { url = "https://files.pythonhosted.org/packages/49/70/f76296f53610bd17b2e7d31728b8b7825e3ac3b5b3688b51f52eab7c0818/pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80", size = 2453651, upload-time = "2026-02-11T04:20:46.243Z" }, + { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" }, + { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" }, + { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" }, + { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" }, + { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" }, + { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" }, + { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" }, + { url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" }, + { url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" }, + { url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" }, + { url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" }, + { url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" }, + { url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" }, + { url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" }, + { url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" }, + { url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" }, + { url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" }, + { url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" }, + { url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" }, + { url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" }, + { url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" }, + { url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" }, + { url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" }, + { url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" }, + { url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" }, + { url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" }, + { url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" }, + { url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" }, + { url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" }, + { url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" }, + { url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" }, + { url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" }, + { url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" }, + { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/5d43209aa4cb58e0cc80127956ff1796a68b928e6324bbf06ef4db34367b/pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f", size = 5228606, upload-time = "2026-02-11T04:22:52.106Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d5/3b005b4e4fda6698b371fa6c21b097d4707585d7db99e98d9b0b87ac612a/pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9", size = 4622321, upload-time = "2026-02-11T04:22:53.827Z" }, + { url = "https://files.pythonhosted.org/packages/df/36/ed3ea2d594356fd8037e5a01f6156c74bc8d92dbb0fa60746cc96cabb6e8/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e", size = 5247579, upload-time = "2026-02-11T04:22:56.094Z" }, + { url = "https://files.pythonhosted.org/packages/54/9a/9cc3e029683cf6d20ae5085da0dafc63148e3252c2f13328e553aaa13cfb/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9", size = 6989094, upload-time = "2026-02-11T04:22:58.288Z" }, + { url = "https://files.pythonhosted.org/packages/00/98/fc53ab36da80b88df0967896b6c4b4cd948a0dc5aa40a754266aa3ae48b3/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3", size = 5313850, upload-time = "2026-02-11T04:23:00.554Z" }, + { url = "https://files.pythonhosted.org/packages/30/02/00fa585abfd9fe9d73e5f6e554dc36cc2b842898cbfc46d70353dae227f8/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735", size = 5963343, upload-time = "2026-02-11T04:23:02.934Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880, upload-time = "2026-02-11T04:23:04.783Z" }, ] [[package]] @@ -1482,7 +1478,7 @@ dev = [ { name = "mike", specifier = "==2.1.3" }, { name = "mkdocs-llmstxt", specifier = "==0.5.0" }, { name = "mkdocs-macros-plugin", specifier = "==1.4.0" }, - { name = "mkdocs-material", extras = ["imaging"], specifier = ">=9.0.0" }, + { name = "mkdocs-material", extras = ["imaging"], specifier = ">=9.5.0" }, { name = "mkdocs-redirects", specifier = "==1.2.2" }, { name = "mkdocs-site-urls", specifier = "==0.3.1" }, { name = "pyyaml", specifier = ">=6.0.3" },