Repository navigation
Conversation
b619f89 to
e4ebe9f
Compare
karangoel16
left a comment
There was a problem hiding this comment.
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
extendsmodel — 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. Theextendsmodel already gives you the shape for tiering this later: self-declared now, attested later. - No
descriptionfield. Businesses usually want a one-line about-blurb;additionalProperties: trueallows 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
rolevalues" 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
websitelinks inconfig.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.
|
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 A minimal optional shape for the base, close to what schema.org already has on Organization (
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 |
gil--
left a comment
There was a problem hiding this comment.
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
iconas a well-known media role - require
nameinconfig - pin
addressto where the business is legally established, and +1 to @frmoretto'slegal_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.
| }, | ||
| "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." |
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
config: {} validates today, so a profile can declare the capability without saying who the business is. I'd require name here, in config
| "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": { |
There was a problem hiding this comment.
"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
|
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.
Options to consider:
|
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 The catalog already carries per-variant seller context in The object can reuse this PR's identity fields, with transactional meaning. Display can use the existing warning contract: a warning with This follows 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). |
|
📋 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. |
f35fea2 to
6e8a043
Compare
6e8a043 to
3e67f45
Compare
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:schema.org/Organizationequivalent): 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)profile.jsonvariant.sellercarries only{ id, name }; no session or line-item seller on Cart, Checkout, or Order/.well-known/ucpservers, and global brands whose contracting legal entity varies by buyer region, cart line item, or shipping address/.well-known/ucp) to its customer-facing website or regional storefront URL, plus public support and account-claiming contactsjoes-pizza.pos-host.exampleservinghttps://joespizza.exampleorhttps://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)extendsgraphThis RFC and PR stack introduce a two-layer architecture built around a shared
common/types/business_summary.jsonprimitive (#1017):dev.ucp.common.businessin/.well-known/ucp, this PR feat: [RFC] Business Identity #939). A declarative Common capability (source/schemas/common/business.json) whoseconfigpublishes 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".common/types/business_summary.jsoninside a top-levelsellersmap (map<seller_id, BusinessSummary & { profile?: uri }>) referenced byseller_idon 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:
common/types/business_summary.json(namerequired; optionallegal_name,description,media,links,address,contacts) as the shared party primitive acrossdev.ucp.common.businessprofile cards, catalogsellersmaps (#1019, #1011), and transactional Cart/Checkout/Ordersellersmaps (#1018).dev.ucp.common.businesson/.well-known/ucpstrictly as the profile host's public, self-declared brand and storefront card (schema.org/Organizationequivalent), separate from physical retail branches (dev.ucp.common.location.*), private tax/KYB disclosures, and per-offer or regional contracting seller resolution.2. Proposal
2.1 The
dev.ucp.common.businesscapability andbusiness_summary.jsonprimitiveBusinesses declare
dev.ucp.common.business(source/schemas/common/business.json) inucp.capabilitieswith aconfigobject 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.jsonschema (#1017), whichbusiness.json#/$defs/configcomposes alongsidecategories[]. Every field reuses or minimally extends an existing UCP type and aligns withschema.org/Organization:name(required): Human-readable display name of the business, brand, or seller (minLength: 1). Required onbusiness_summary.jsonandbusiness.json#/$defs/configso an empty{}object cannot validate.legal_name(optional): Registered corporate or legal name of the business or seller (schema.org/Organization.legalNameequivalent), used when the legal entity name differs from the customer-facing displayname(for example,"Joe's Pizza LLC").description(optional): Reusescommon/types/description.json(plain,markdown,html) so businesses and sellers can provide a rich "about" summary without inventing a custom text container.media[](optional): Reusescommon/types/media.jsonwith an optional open-vocabularyrolefield (well-known values:logofor a primary brand wordmark,iconfor a compact square mark or avatar, andcoverfor a banner or hero image). Unrecognizedrolevalues MUST NOT cause a platform to reject the containing media item; platforms MAY ignore an unrecognizedroleattribute.links[](optional): Reusescommon/types/link.jsonwith a well-knownwebsitetype 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): Reusescommon/types/postal_address.jsonfor the publicly disclosable corporate, headquarters, or statutory establishment address of the business or seller. Physical retail branches live indev.ucp.common.location.*.contacts[](optional): Usescommon/types/contact.json(anyOfrequiringemailor E.164phone_number, plus optionaltypeand human-readablename) for public contact channels (general,customer_support,legal,press). Internal operational contacts such asadminare excluded from the public profile vocabulary.categories[](optional, onbusiness.json#/$defs/config): Uses the{ value, taxonomy }shape ofshopping/types/category.json, with business taxonomies (mccper 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
configblock on/.well-known/ucpcannot 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/ucpserver fails for third-party merchants without UCP endpoints, and requiring a separatebusiness.lookupRPC 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.jsonacross both layers:dev.ucp.common.business(configin/.well-known/ucp, #939)sellersmap onsearch_response,lookup_response, andget_product_response+seller_idonproductandvariant(#1019, #1011)map<seller_id, BusinessSummary & { profile?: uri }>, parallelingpolicies[]) and deprecates inlinevariant.seller. Works symmetrically whether the seller is a 3P marketplace shop without a UCP server (profileomitted) or a brand with its own/.well-known/ucp(profileset), with zero extra lookup round trips.sellersmap ("ucp_request": "omit") +seller_idoncart,checkout,order,line_item, andorder_line_item(#1018)seller_ididentifies the default contracting entity once buyercontextor shippingaddressfixes the market (for example, resolving a Canada Ltd. entity vs. a US Inc. entity), whileline_item.seller_idsupports multi-seller marketplace carts and multi-entity split carts.2.3 Privacy and Security Considerations ("Presented as Stated")
/.well-known/ucpis 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 profileconfig. 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/ucpfile.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-declaredcategories(such asmcc) as underwritten payment or compliance classifications without independent verification.description.markdownordescription.htmlMUST sanitize untrusted markup and strip active content in accordance withcommon/types/description.json.PR Stack
feat/business-summarycommon/types/business_summary.json,common/types/contact.json,media.role(logo,icon,cover), andlink.type(website)feat/business-identitydev.ucp.common.businessprofile capability (/.well-known/ucppublic brand card)feat/catalog-sellerssellersmap andseller_idon Catalog Search and Lookup (implements #1011)feat/transactional-sellerssellersmap and session / line-itemseller_idon Cart, Checkout, and Orderfeat/verifiable-attestationsattestations[]) onbusiness_summary.jsonandlocation.json