Skip to content

feat: [RFC] Business Identity - #939

Open
gsmith85 wants to merge 2 commits into
mainfrom
feat/business-identity
Open

gsmith85 wants to merge 2 commits into
mainfrom
feat/business-identity

Conversation

@gsmith85

@gsmith85 gsmith85 commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

1. Summary & Problem

A UCP business profile (/.well-known/ucp) states what an endpoint can do (ucp.services, ucp.capabilities, ucp.payment_handlers) and how to verify its signatures (keys[]), but nothing in the profile states who operates that storefront or brand. Meanwhile, when a marketplace or multi-entity global brand serves catalog items and checkouts, platforms need a consistent, low-latency way to represent both the storefront brand and the specific seller or legal entity behind each offer and transaction.

Today, UCP models physical branches via dev.ucp.common.location.* (common/types/location.json), which describes individual physical places (address, geo, hours, amenities) queried at runtime. However, UCP has no shared party primitive or capability for location-independent business and seller identity, creating four structural gaps across UCP deployments:

Identity need Where it lives today Who is underserved
Public, location-independent brand and storefront card (schema.org/Organization equivalent): display name, registered legal name, Markdown description, logo/icon/cover media, policy and storefront links, public corporate address, public contacts, and business categories (mcc, naics) Nowhere in profile.json Online-only merchants, distinct brands within a multi-brand enterprise, regional profiles, and businesses advertising non-transactional capabilities (such as Location or Ask) without full UCP checkout
Symmetric seller and legal-entity attribution across 1P brands, 3P marketplaces, and multi-entity regional brands (such as US Inc. vs. Canada Ltd. vs. EU GmbH) Inline variant.seller carries only { id, name }; no session or line-item seller on Cart, Checkout, or Order Marketplaces whose third-party sellers do not host /.well-known/ucp servers, and global brands whose contracting legal entity varies by buyer region, cart line item, or shipping address
Binding a technical profile origin (/.well-known/ucp) to its customer-facing website or regional storefront URL, plus public support and account-claiming contacts Nowhere Platform-hosted businesses (joes-pizza.pos-host.example serving https://joespizza.example or https://pos-host.example/joes-pizza) and multi-region or multi-brand enterprises mapping UCP origins (us.ucp.global-apparel.example) to path-based or brand-specific storefronts (https://global-apparel.example/us)
Vertical-specific and third-party business identity attributes (such as dining cuisines, service categories, or compliance extensions) Nowhere Vertical specifications (Dining, Services, Retail, Lodging) and third-party ecosystems that need a base business identity capability to extend via UCP's extends graph

This RFC and PR stack introduce a two-layer architecture built around a shared common/types/business_summary.json primitive (#1017):

  1. Layer 1: Public Profile Brand Card (dev.ucp.common.business in /.well-known/ucp, this PR feat: [RFC] Business Identity #939). A declarative Common capability (source/schemas/common/business.json) whose config publishes the profile host's public, self-declared brand and storefront metadata (name, legal_name, description, media, links, address, contacts, categories), decoupled from transactional capabilities and extensible via "extends": "dev.ucp.common.business".
  2. Layer 2: Response-Normalized Seller Attribution Across Catalog (#1019, #1011) and Cart / Checkout / Order (#1018). Catalog, Cart, Checkout, and Order responses reuse common/types/business_summary.json inside a top-level sellers map (map<seller_id, BusinessSummary & { profile?: uri }>) referenced by seller_id on products, variants, checkout/cart sessions, and line items. This works symmetrically for 1P brands, 3P marketplace sellers without UCP servers, and regional legal entities resolved at checkout time, with zero extra lookup round trips.

Decisions for the TC:

  1. Standardize common/types/business_summary.json (name required; optional legal_name, description, media, links, address, contacts) as the shared party primitive across dev.ucp.common.business profile cards, catalog sellers maps (#1019, #1011), and transactional Cart/Checkout/Order sellers maps (#1018).
  2. Scope dev.ucp.common.business on /.well-known/ucp strictly as the profile host's public, self-declared brand and storefront card (schema.org/Organization equivalent), separate from physical retail branches (dev.ucp.common.location.*), private tax/KYB disclosures, and per-offer or regional contracting seller resolution.
  3. Enforce the "Presented as Stated" trust boundary: all profile business fields are self-declared by the origin and MUST NOT be badged as third-party verified or treated as underwritten payment classifications without independent verification or a cryptographic attestation (#1020).

2. Proposal

2.1 The dev.ucp.common.business capability and business_summary.json primitive

Businesses declare dev.ucp.common.business (source/schemas/common/business.json) in ucp.capabilities with a config object carrying public, location-independent identity metadata for the brand or storefront operating that profile origin:

{
  "ucp": {
    "version": "2026-08-25",
    "services": {},
    "capabilities": {
      "dev.ucp.common.business": [
        {
          "version": "2026-08-25",
          "spec": "https://ucp.dev/2026-08-25/specification/common/business/",
          "schema": "https://ucp.dev/2026-08-25/schemas/common/business.json",
          "config": {
            "name": "Joe's Pizza",
            "legal_name": "Joe's Pizza LLC",
            "description": {
              "markdown": "New York slice shop serving Greenwich Village since 1975."
            },
            "media": [
              { "type": "image", "role": "logo", "url": "https://cdn.pos-host.example/joes/logo.png", "alt_text": "Joe's Pizza logo" },
              { "type": "image", "role": "icon", "url": "https://cdn.pos-host.example/joes/icon.png", "alt_text": "Joe's Pizza icon" },
              { "type": "image", "role": "cover", "url": "https://cdn.pos-host.example/joes/cover.jpg" }
            ],
            "links": [
              { "type": "website", "url": "https://joespizza.example" }
            ],
            "address": {
              "street_address": "123 Carmine St",
              "address_locality": "New York",
              "address_region": "NY",
              "postal_code": "10014",
              "address_country": "US"
            },
            "contacts": [
              { "type": "general", "phone_number": "+12125550100", "email": "info@joespizza.example" },
              { "type": "customer_support", "email": "support@joespizza.example" }
            ],
            "categories": [
              { "taxonomy": "mcc", "value": "5812" },
              { "taxonomy": "merchant", "value": "Pizza restaurant" }
            ]
          }
        }
      ]
    },
    "payment_handlers": {}
  }
}

The base party fields live in a reusable common/types/business_summary.json schema (#1017), which business.json#/$defs/config composes alongside categories[]. Every field reuses or minimally extends an existing UCP type and aligns with schema.org/Organization:

  • name (required): Human-readable display name of the business, brand, or seller (minLength: 1). Required on business_summary.json and business.json#/$defs/config so an empty {} object cannot validate.
  • legal_name (optional): Registered corporate or legal name of the business or seller (schema.org/Organization.legalName equivalent), used when the legal entity name differs from the customer-facing display name (for example, "Joe's Pizza LLC").
  • description (optional): Reuses common/types/description.json (plain, markdown, html) so businesses and sellers can provide a rich "about" summary without inventing a custom text container.
  • media[] (optional): Reuses common/types/media.json with an optional open-vocabulary role field (well-known values: logo for a primary brand wordmark, icon for a compact square mark or avatar, and cover for a banner or hero image). Unrecognized role values MUST NOT cause a platform to reject the containing media item; platforms MAY ignore an unrecognized role attribute.
  • links[] (optional): Reuses common/types/link.json with a well-known website type alongside existing policy link types (privacy_policy, terms_of_service, refund_policy, shipping_policy, faq). links[] is for canonical storefront binding (website, at most one per profile) and business-wide legal or policy disclosures, not a sitemap of site navigation pages.
  • address (optional): Reuses common/types/postal_address.json for the publicly disclosable corporate, headquarters, or statutory establishment address of the business or seller. Physical retail branches live in dev.ucp.common.location.*.
  • contacts[] (optional): Uses common/types/contact.json (anyOf requiring email or E.164 phone_number, plus optional type and human-readable name) for public contact channels (general, customer_support, legal, press). Internal operational contacts such as admin are excluded from the public profile vocabulary.
  • categories[] (optional, on business.json#/$defs/config): Uses the { value, taxonomy } shape of shopping/types/category.json, with business taxonomies (mcc per ISO 18245, naics, isic, merchant). The first entry is the primary category.

2.2 Two-Layer Architecture: Public Profile Brand Card vs. Catalog and Transactional Seller Attribution

A static config block on /.well-known/ucp cannot dynamically interrogate thousands of third-party marketplace sellers or resolve a global brand's regional contracting entity when the legal seller depends on the buyer's shipping address. At the same time, requiring every marketplace seller to host its own /.well-known/ucp server fails for third-party merchants without UCP endpoints, and requiring a separate business.lookup RPC call after catalog search adds serial round-trip latency before a platform can render product cards.

UCP resolves this cleanly by separating public profile brand identity from response-normalized seller attribution, using common/types/business_summary.json across both layers:

Layer Surface Role & Mechanism
Layer 1: Public Profile Brand Card dev.ucp.common.business (config in /.well-known/ucp, #939) Declares who operates the UCP profile origin (the storefront or marketplace brand). Static, publicly cacheable, and decoupled from transactional capabilities so even non-transactional profiles (Location, Ask) can publish brand metadata.
Layer 2a: Catalog Seller Attribution sellers map on search_response, lookup_response, and get_product_response + seller_id on product and variant (#1019, #1011) Deduplicates seller metadata once per response (map<seller_id, BusinessSummary & { profile?: uri }>, paralleling policies[]) and deprecates inline variant.seller. Works symmetrically whether the seller is a 3P marketplace shop without a UCP server (profile omitted) or a brand with its own /.well-known/ucp (profile set), with zero extra lookup round trips.
Layer 2b: Transactional Seller of Record sellers map ("ucp_request": "omit") + seller_id on cart, checkout, order, line_item, and order_line_item (#1018) Resolves the authoritative contracting legal entity at transaction time. Session-level seller_id identifies the default contracting entity once buyer context or shipping address fixes the market (for example, resolving a Canada Ltd. entity vs. a US Inc. entity), while line_item.seller_id supports multi-seller marketplace carts and multi-entity split carts.

2.3 Privacy and Security Considerations ("Presented as Stated")

  • Public profile privacy boundary: Because /.well-known/ucp is a public, unauthenticated document cached across platforms, businesses and hosts MUST NOT place private tax or underwriting identifiers, non-public personal or residential addresses (such as a sole proprietor's home address), or internal operational contacts in profile config. Context-dependent or statutory seller disclosures required before purchase bind on transactional responses (checkout.sellers[seller_id]) rather than in the static public /.well-known/ucp file.
  • Normative "Presented as Stated" rule: All fields in dev.ucp.common.business (name, legal_name, description, media, links, address, contacts, categories) are self-declared by the business publishing the profile. Platforms MUST treat these fields as stated by the business, MUST NOT badge or label them as third-party verified based solely on their presence in /.well-known/ucp, MUST NOT infer omitted fields, and MUST NOT treat self-declared categories (such as mcc) as underwritten payment or compliance classifications without independent verification.
  • Rich-text sanitization: Platforms rendering description.markdown or description.html MUST sanitize untrusted markup and strip active content in accordance with common/types/description.json.

PR Stack

Layer PR Branch Scope
Base: Shared Party & Contact Primitives #1017 feat/business-summary Foundational common/types/business_summary.json, common/types/contact.json, media.role (logo, icon, cover), and link.type (website)
Arm 1: Public Profile Brand Card (This PR) This PR feat/business-identity Declarative dev.ucp.common.business profile capability (/.well-known/ucp public brand card)
Arm 2a: Catalog Seller Attribution #1019 feat/catalog-sellers Response-normalized sellers map and seller_id on Catalog Search and Lookup (implements #1011)
Arm 2b: Transactional Seller Attribution #1018 feat/transactional-sellers Response-normalized sellers map and session / line-item seller_id on Cart, Checkout, and Order
Arm 3: Verifiable Attestations (RFC 04) #1020 feat/verifiable-attestations Optional third-party cryptographic trust marks (attestations[]) on business_summary.json and location.json

@gsmith85 gsmith85 added gov:tc-meeting Nominated for synchronous discussion at an upcoming weekly DTC meeting RFC labels Oct 13, 2026
@gsmith85
gsmith85 force-pushed the feat/business-identity branch from b619f89 to e4ebe9f Compare October 13, 2026 03:40
@gsmith85 gsmith85 added this to the 2026-12-08 milestone Oct 16, 2026

@karangoel16 karangoel16 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a well-constructed RFC. The gap is real: the profile says what a business can do and how to verify it, but nothing says who it is, and Location only covers physical branches. The Business/Location separation is clean — independently adoptable, online-only merchants covered — and making the storefront binding a SHOULD rather than a MUST is the right call.

I verified the moving parts: the examples validate against the new schemas (media needs type+url, link needs type+url — both satisfied), the "first category is primary" semantics are documented consistently in both the schema and the docs, and the MCC 5812 in the example is actually the right code for restaurants. The website addition to the shared link type is backwards compatible since it's already an open vocabulary.

A few design questions:

  • Self-declared MCC needs a trust story. The MCC in this proposal is written by the business into its own profile — nothing verifies it. So the spec needs to define one of two things: either agents MUST NOT treat self-declared MCC codes as trustworthy (display hint only), or there needs to be a mechanism for trusted MCC attestation (e.g., a payment ecosystem countersigning via the extends model — the acquirer that underwrote the merchant is the natural attester, since it carries liability for the coding). Right now it's neither, which leaves every consumer guessing how much weight to give it. The extends model already gives you the shape for tiering this later: self-declared now, attested later.
  • No description field. Businesses usually want a one-line about-blurb; additionalProperties: true allows it, but it's not standardized. Worth considering as a first-class field.
  • Legal entity vs brand. The capability says "business or brand" throughout, and compliance/registration identifiers get punted to verticals via extends. Fine as a starting point, but worth naming explicitly whether the base should eventually carry a legal-entity field — it matters for anything payments-adjacent.
  • "MUST ignore unrecognized role values" is ambiguous. Does that mean drop the role and render the item anyway, or drop the whole media item? One clarifying sentence would settle it.
  • Minor: nothing prevents duplicate website links in config.links. Probably fine to leave, just noting it.

No blockers — this is RFC feedback, and the design is solid as proposed. Curious what others think, especially on the MCC trust question.

@frmoretto

Copy link
Copy Markdown

Building on @karangoel16's "legal entity vs brand" point, with one reason it may belong in the base rather than in verticals: in the EU it is horizontal. Any business selling to consumers at a distance has to give its identity and the geographical address where it is established (Directive 2011/83/EU, Art. 6(1)(b)-(c)); for online services, also its trade register and registration number and its VAT identification number (Directive 2000/31/EC, Art. 5(1)).

And since business_summary.json is meant for third-party sellers in catalogs (#674): on marketplaces, Art. 6a of Directive 2011/83/EU (added by Directive (EU) 2019/2161) requires telling the consumer whether the seller is a trader, because consumer rights apply only if it is.

A minimal optional shape for the base, close to what schema.org already has on Organization (legalName, vatID, taxID, leiCode):

  • legal_name, when it differs from name;
  • registrations[], e.g. { "type": "vat", "value": "...", "issuer": "IT" };
  • a way to mark which address is the registered one;
  • for catalog seller records, an optional statement of whether the seller acts as a trader.

On trust, the split karangoel16 suggests for MCC fits here too: self-declared in the base, presented as stated and never inferred, with attestation layered later through extends, for instance by a marketplace that has assessed the trader's details, as DSA Art. 30 requires.

@gil-- gil-- left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really excited to see this land. A shared "who is this business" shape is something every platform ends up rebuilding on its own, and having it in the profile next to Location makes a lot of sense. I've been prototyping against this with close to 200 real storefronts, and the base shape held up well. Inline comments are small:

  • add icon as a well-known media role
  • require name in config
  • pin address to where the business is legally established, and +1 to @frmoretto's legal_name / registrations[]

A few bigger things I'd love to see covered, either here or as follow-ups:

Seller of record by market. One profile can still sell through more than one company, e.g. a US Inc. at home, a GmbH for EU buyers, and a distributor running its own store in Japan. Today a German buyer would see the US company. An optional entities[] could cover it:

"entities": [
  {
    "markets": ["DE", "AT", "NL", "FR"],
    "legal_name": "Juniper Leaf GmbH",
    "registrations": [{ "type": "vat", "value": "DE123456789", "issuer": "DE" }],
    "address": { /* where this entity is established */ },
    "links": [ /* e.g. its own privacy_policy, legal_notice */ ]
  },
  { "markets": ["JP"], "profile": "https://juniperleaf.example.jp/.well-known/ucp" }
]

Brand stuff (name, media, categories) always comes from the top level. The legal block is swapped as a whole, links/contacts override by type, and a market in two entities means no seller. The profile form should only count if that profile points back, since anyone can claim to sell a brand.

Business attributes. Location gets amenities (reverse-DNS, open vocab), but there's no equivalent for the business itself. I think the same pattern would work, e.g. attributes: ["dev.ucp.business.attribute.handmade"]. Some examples:

  • how it makes things: handmade, made_to_order, small_batch
  • who owns it: family_owned, women_owned, black_owned, veteran_owned, employee_owned
  • what kind of org it is: nonprofit, cooperative, independent

Ownership ones are self-identified, so platforms should show them that way ("identifies as women-owned"). Certifications (B Corp, organic, Fair Trade) and green claims are different: in the EU, a sustainability label that isn't based on a certification scheme is banned outright (2024/825), so those shouldn't be plain self-declared strings.

Brand owner vs. reseller. Buyers and agents care a lot about whether they're buying from the brand, an authorized retailer, or someone reselling. But it can vary by product, since one store can sell its own brand next to other brands. So I'd keep it off the business and put it on the offer, something like variant.seller.relationship: brand_owner, authorized_reseller, reseller. The same two-way check as entities could back up authorized_reseller: the brand's profile lists the stores it authorizes.

Presented as stated. Everything above is stated by the business. I'd add a line to security considerations that a platform shows legal_name, registrations, categories and attributes as stated, never labels them verified just because they're there, and never infers missing ones. Attestation can follow later via extends, like @karangoel16 and @frmoretto suggested.

Checkout should name the seller. context.json already says policy is enforced at checkout, and I think seller identity is the same. The profile is a preview; checkout should return the resolved seller once the shipping address fixes the market. Right now it doesn't carry one.

Comment thread source/schemas/common/types/media.json Outdated
},
"role": {
"type": "string",
"description": "Semantic role of the media item within its container. Well-known values: `logo`, `cover`. Open vocabulary; a Platform MUST ignore unrecognized values."

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd add icon as a well-known role: a small square mark meant for tiny sizes, like a favicon or an avatar in a chat list. It's neither a logo (often a wide wordmark) nor a cover.

"allOf": [
{ "$ref": "types/business_summary.json" },
{
"type": "object",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

config: {} validates today, so a profile can declare the capability without saying who the business is. I'd require name here, in config

Suggested change
"type": "object",
"type": "object",
"required": ["name"],

},
"description": "Business-level links, such as the canonical customer-facing `website` storefront URL and business-wide policy links (`privacy_policy`, `terms_of_service`, `refund_policy`, `shipping_policy`, `faq`)."
},
"address": {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Corporate, headquarters, or registered legal address" could be three different addresses for the same business, and a platform has no way to tell which one it got. I think we should pick one: where the business is legally established. That's the address the EU makes sellers show (2011/83/EU Art. 6(1)(c)). It's also the one a buyer needs when the brand name and the company behind it aren't the same. Anything else, like a returns or complaints address, can live on a contact.

Something like:

"description": "Geographical address where the business is legally established. Physical retail branches live in `dev.ucp.common.location.*`; other addresses (e.g., returns, complaints) belong on a contact."

Also +1 to @frmoretto's legal_name and registrations[], so a buyer (or platform) can see who they're actually buying from. legal_name only needs to be set when it's different from name.:

"legal_name": "Juniper Leaf Inc.",
"registrations": [
  { "type": "vat", "value": "DE123456789", "issuer": "DE" },
  { "type": "company_register", "value": "HRB 234567 B", "issuer": "DE", "authority": "Amtsgericht Berlin-Charlottenburg" }
]
  • type is open (vat, tax_id, company_register, lei)
  • issuer is the ISO 3166 code of the country that issued it
  • authority is optional, for places where the law says you have to name the register

@amithanda

Copy link
Copy Markdown
Contributor

Thanks @gsmith85, +1 on the gap and on keeping this separate from Location. Before we go deeper on individual fields, I'd like to align on one high level design question.
The challenge: Profile config is public, cached, and the same for every buyer, platform, and market. That works well for a brand card (name, logo, website, support contact), but several proposed additions don't fit that model:

  • Sensitive data: legal addresses (often a home address for sole proprietors), tax registrations, and platform-facing contacts like admin.
  • Context-dependent data: the seller of record can vary by market, cart, and shipping address. UCP already treats pre-checkout context as provisional and requires binding data at checkout.
  • Scale: a marketplace profile can't describe thousands of sellers.

Options to consider:

  • A. Profile only: simple, no extra round trip, works with static hosting. But it can't handle the sensitive, contextual, or marketplace cases.
  • B. Dedicated operation (dev.ucp.common.business.lookup, REST + MCP): contextual, cacheable, can be access-controlled, and gives one place to evolve identity fields. But it can only return a preview of the seller, so it can't guarantee that the legal disclosure matches the actual transaction. Compliance would also depend on every agent remembering to call it.
  • Any other options?

@frmoretto

Copy link
Copy Markdown

Any other options?

Both A and B are useful as previews, so C keeps them and adds the contracting seller on the checkout response, building on @gil--'s "checkout should name the seller". Concretely, a seller on checkout and order identifying the party the buyer contracts with.

The catalog already carries per-variant seller context in variant.seller, but catalog responses "are not transactional commitments — checkout is authoritative." In UCP's current model, the Business typically acts as Merchant of Record; on a marketplace, the contracting seller may be a third party. This proposal assumes one contracting seller per checkout; multi-seller checkout would need its own model.

The object can reuse this PR's identity fields, with transactional meaning. Display can use the existing warning contract: a warning with presentation: "disclosure" and path: "$.seller", carrying the seller details in its content.
The platform MUST display that content and MUST NOT hide or auto-dismiss it.

This follows context.json: "Eligibility and policy enforcement MUST occur at checkout time using binding transaction data."
It also fits the timing of Directive 2011/83/EU, which requires the trader's identity, geographical address and contact details before the consumer is bound (Art. 6(1)(b)-(c)). The order preserves the seller identity for the contract confirmation required by Art. 8(7) to draw on.

The profile need not publish legal addresses and registrations; they can be returned in the transaction-specific checkout, where they still reach the platform handling it. On a marketplace, the seller's trader status can sit in the same object, based on the seller's declaration (Art. 6a).

@damaz91

damaz91 commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

📋 Discussed in UCP Shopping Tech Council (STC) Meeting - 2026-10-23

This pull request was referenced under the following agenda topic(s):

See the full meeting minutes for discussion notes and action items.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:common Issues and pull requests related to common protocol schemas and specifications gov:tc-meeting Nominated for synchronous discussion at an upcoming weekly DTC meeting RFC

Projects

Status: Triage / Unscheduled

Development

Successfully merging this pull request may close these issues.

6 participants