AI Interfaces

Public, read-only endpoints that expose each company's published catalog — as schema.org JSON-LD for search engines, as an MCP server for AI agents, and as a WebMCP embed for in-browser agents.

Every endpoint is keyed by the company's pluginKey — the stable public key issued by the platform the catalog is sourced from. Unknown keys return 404. All data is a read-only mirror of the company's published catalog — no customer data or internal pricing is ever exposed. Examples below use Tudi Mechanical Systems.

1 · JSON-LD feed

GET/companies/{pluginKey}.jsonld

Returns the company's complete schema.org graph as application/ld+json, ready to embed verbatim in a <script type="application/ld+json"> tag. The @graph contains:

NodeContents
LocalBusinessBusiness name, postal address, geo coordinates, website.
Product (system type)One per system type (e.g. HP & Air Handler, Water Heater). The AggregateOffer spans the type's lowest and highest fully-installed price, nests up to three Good/Better/Best tier offers, and carries a configurable note describing what the price covers. Disable per deployment by removing system from CONCOM_OFFERABLE_ITEM_TYPES.
Product / ServiceOne per remaining offer group (by category, plus one per uncategorized item), typed by its member items. Each carries an AggregateOffer with lowPrice/highPrice/offerCount.
Up to 50 offers per company. The feed emits at most 50 offer nodes — the most representative first (largest categories, then highest-priced individual items) — so the embedded JSON-LD stays a sensible size for large catalogs.

Example

curl http://ai-interfaces.toolbox.contractorcommerce.com/companies/000qeNvgDSrmHEbIejt1beAWureP3Z5HrsB3NZFq.jsonld

Abbreviated response:

{
  "@context": "https://schema.org",
  "@graph": [
    { "@type": "LocalBusiness", "name": "Tudi Mechanical Systems",
      "address": { "@type": "PostalAddress", "addressLocality": "…" } },
    { "@type": "Product", "name": "HP & Air Handler",
      "offers": { "@type": "AggregateOffer", "priceCurrency": "USD",
                  "lowPrice": "8500.00", "highPrice": "15200.00", "offerCount": 24,
                  "description": "Fully-installed price. Includes installation and permits.",
                  "offers": [
                    { "@type": "Offer", "name": "Good",   "price": "8500.00"  },
                    { "@type": "Offer", "name": "Better", "price": "10200.00" },
                    { "@type": "Offer", "name": "Best",   "price": "12750.00" } ] } },
    { "@type": "Product", "name": "Air Filters",
      "offers": { "@type": "AggregateOffer", "priceCurrency": "USD",
                  "lowPrice": "60.05", "highPrice": "186.04", "offerCount": 3 } }
  ]
}

Embedding on a website

<script type="application/ld+json">
  …paste the response body verbatim…
</script>

2 · MCP server (for AI agents)

POST/companies/{pluginKey}/mcp

A per-company Model Context Protocol server using the Streamable HTTP transport (JSON-RPC 2.0, protocol 2025-06-18). It is stateless: every request is answered with a single application/json body, notifications return 202, and GET returns 405 (no server-initiated stream). Point any MCP client at the URL and it will discover the tools. After initialize, send the negotiated MCP-Protocol-Version header on each request — an unsupported value is answered with 400; CORS preflights allow it.

Tools

ToolArgumentsReturns (structuredContent)
get_business_profile—The schema.org LocalBusiness/Organization node (name, address, geo, phone, website) plus quote_url, shop_url when known, and service_area.postal_code_count.
list_offers—{ quote_url, offers: [ … ] } — offer groups with USD price ranges, offer counts and tier prices.
search_catalogquery string of 2–100 characters (required; every word must match); limit integer 1–25 (default 10){ query, total_matches, quote_url, matches: [ { id, name, sku, type, category, price, currency, image_url, description } ] } — ranked exact SKU → exact name → name starts with → name contains → SKU, alphabetical within a tier; the order does not depend on limit.
check_service_areapostal_code string matching ^\s*\d{5}(-\d{4})?\s*$ (required — a US ZIP or ZIP+4; "02134", not 2134){ postal_code, served, company, service_area } — the five-digit ZIP actually checked and the size of the territory.
get_faqs—{ faqs: [ { category, question, attribute, intent, applies_when: [ { attribute, in, not_in } ], answers: [ { label, description, intent } ] } ] } — the quote flow's branching questionnaire; the same question can recur under different applies_when conditions.

Every tool declares a title, an inputSchema, an outputSchema and MCP annotations (readOnlyHint and idempotentHint true, destructiveHint and openWorldHint false — the answers come from our own catalog mirror). A successful call returns structuredContent conforming to the tool's outputSchema, plus the same JSON pretty-printed as its text block. Wherever an agent might need the next step, the result carries quote_url — our redirect into the contractor's own quote funnel.

Connect from Claude Code

claude mcp add --transport http tudi-mechanical-systems \
  "http://ai-interfaces.toolbox.contractorcommerce.com/companies/000qeNvgDSrmHEbIejt1beAWureP3Z5HrsB3NZFq/mcp"

Then ask things like “do they serve zip 63376, and what do air filters cost?” — the agent discovers and calls the tools itself.

Raw JSON-RPC examples

Handshake:

curl http://ai-interfaces.toolbox.contractorcommerce.com/companies/000qeNvgDSrmHEbIejt1beAWureP3Z5HrsB3NZFq/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{
        "protocolVersion":"2025-06-18","capabilities":{},
        "clientInfo":{"name":"demo","version":"1"}}}'

List tools:

curl http://ai-interfaces.toolbox.contractorcommerce.com/companies/000qeNvgDSrmHEbIejt1beAWureP3Z5HrsB3NZFq/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'

Call a tool — search the catalog:

curl http://ai-interfaces.toolbox.contractorcommerce.com/companies/000qeNvgDSrmHEbIejt1beAWureP3Z5HrsB3NZFq/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{
        "name":"search_catalog","arguments":{"query":"Lennox","limit":5}}}'

Check service-area coverage:

curl http://ai-interfaces.toolbox.contractorcommerce.com/companies/000qeNvgDSrmHEbIejt1beAWureP3Z5HrsB3NZFq/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{
        "name":"check_service_area","arguments":{"postal_code":"63376"}}}'
Errors. Unknown methods return JSON-RPC -32601; unknown tools -32602. Arguments are validated strictly against the tool's inputSchema before anything runs: an unknown key, a wrong JSON type ("postal_code": 45050 where a string is declared), a value that misses its pattern ("postal_code": "@@@") or an out-of-range limit returns a result with isError: true naming the argument and what was expected, per the MCP tool-error convention. Leading and trailing whitespace on string arguments is trimmed before validation. A failure inside a tool returns a flat Tool execution failed. — details are logged server-side, never sent to the caller.

3 · WebMCP embed (in-browser agents)

GET/webmcp.js

A drop-in script that exposes the same catalog tools to AI agents running inside the visitor's browser via the emerging WebMCP (document.modelContext) API. Add one tag to the company's site — the script ships with the tool definitions embedded (the same tools/list the endpoint above serves, annotations included) and registers them the moment it executes, with no discovery request; each call is forwarded to the MCP endpoint and the agent receives the tool's structuredContent as a native object. Browser discovery shows each tool's title and — since WebMCP has no output-schema channel — a Returns: shape summary appended to its description. Beside the MCP hints, every tool carries Chrome's readOnlyHint, consequentialHint: false and untrustedContentHint: true — catalog text is contractor-authored data, not instructions. No keys or server calls on the contractor's part:

<script type="module"
        src="https://ai-interfaces.toolbox.contractorcommerce.com/webmcp.js"
        data-plugin-key="000qeNvgDSrmHEbIejt1beAWureP3Z5HrsB3NZFq"></script>
Experimental. WebMCP is a W3C Community Group draft shipping as a Chrome origin trial (Chrome 149–156). The script carries our origin-trial token, so the tools register in stock Chrome on any site that embeds the tag — no flag and nothing to register per site. Everywhere else the tag is a safe no-op: it logs one [webmcp] console line saying the API isn't available and does nothing more (to exercise it outside the trial, enable chrome://flags/#enable-webmcp-testing). On success the console shows [webmcp] registered 5 tools for {pluginKey}. The module is cached for 300 seconds with an ETag, so a deploy reaches embedded pages within that window; agents should still treat the tool list as dynamic, as WebMCP expects. The tools, arguments and behavior are identical to the MCP server above — this is just a browser-native delivery of the same catalog.

4 · Source tracking

Any endpoint accepts an optional source query parameter. When present, the request is recorded — the source label, the endpoint path, the HTTP method, and a timestamp — so you can attribute traffic to a campaign, referrer, or integration. It is optional, never changes the response body or status, and is ignored when omitted or empty.

GET/companies/{pluginKey}.jsonld?source=your-label

Append ?source= (any short label) to any endpoint URL — for example:

curl "http://ai-interfaces.toolbox.contractorcommerce.com/companies/000qeNvgDSrmHEbIejt1beAWureP3Z5HrsB3NZFq.jsonld?source=newsletter"
Optional & non-intrusive. The parameter only adds an attribution record; requests without a source are not tracked. Use a stable label per channel (e.g. newsletter, partner-acme) so hits group cleanly.
Outbound attribution. The mirror direction of the same idea: when a contractor URL we publish already carries a referrer (or referer) variable, its value is rewritten to concom_ai_interfaces — in the query string or inside a hash route's own query, wherever it sits. Everything else about the URL is passed through untouched, and a URL with no such variable never gains one.

5 · Data freshness

The mirror is synchronized from the source platform's API on a recurring incremental schedule; offer groups and price ranges are rebuilt after each sync. Prices reflect the company's published catalog at the time of the last sync.

The pricing view model (section 7) publishes that moment as last_synced_at (the last successful sync — a failed run never advances it) and summarizes it as freshness_state: fresh, stale once the last success is older than the configured threshold, or unavailable when the company has never synced. Consumers should stop showing prices that are not fresh.

6 · Get Quote (OpenAI local services)

Support for OpenAI's local-services request-quote conversion spec, which lets ChatGPT show an in-chat "Get Quote" launcher for a contractor. Three surfaces work together, and all three are read-only — a quote request is a handoff to the contractor's own funnel, never something this app collects or stores.

Business feed

GET/v1/businesses?page=1&page_size=50

One record per eligible contractor — a current customer (platform status active, with a live storefront), with a pluginKey, a business address with coordinates, a valid website, and a phone number: the fields the spec requires on every record. Churned contractors' endpoints — this feed, JSON-LD, MCP, quote and pricing pages — all return 404. A contractor missing any of them is excluded until the data is completed upstream, and returns automatically on the next sync. Optional changes_token takes the changes_token from a previous response and returns only the contractors whose company or address record has changed since.

Records are validated and ordered — by pluginKey, then id — before pagination: every page is full except possibly the last, the order is identical across requests, and no id is duplicated or skipped between adjacent pages. Tokens are versioned (chg_<version>_<timestamp>): a token minted before a feed-contract change is treated as stale and answered with a full re-walk. checksum is false exactly when your token is current and nothing has changed — skip processing and keep the token you have.

platform_url is the contractor's own quote funnel when they publish one — their systems_estimate_url if the source has vouched for it, else their concom_shop_url storefront — and the hosted quote page below otherwise. provider_action_url is always the hosted quote page: that is the click the launcher makes, and it is the one we attribute. Either way any referrer variable on a published URL is stamped concom_ai_interfaces — see source tracking.

{
  "checksum": true,
  "page": 1,
  "page_size": 50,
  "total_pages": 3,
  "changes_token": "chg_1755500000",
  "businesses": [
    {
      "id": "000qeNvgDSrmHEbIejt1beAWureP3Z5HrsB3NZFq",
      "name": "Tudi Mechanical Systems",
      "address": { "line1": "…", "locality": "…", "region": "…",
                   "postal_code": "…", "country": "US", "formatted": "…" },
      "location": { "latitude": 37.793, "longitude": -122.396 },
      "phone_number": "+14155551234",
      "website_url": "https://example.com",
      "platform_url": "https://example.com/shop/",
      "service_providers": [
        {
          "provider": "contractor_commerce",
          "provider_business_id": "000qeNvgDSrmHEbIejt1beAWureP3Z5HrsB3NZFq",
          "action_type": "request_a_quote",
          "provider_action_url": "https://ai-interfaces.toolbox.contractorcommerce.com/companies/000qeNvgDSrmHEbIejt1beAWureP3Z5HrsB3NZFq/quote",
          "display_name": "Get Quote"
        }
      ]
    }
  ]
}
The changes_token checkpoint tracks each contractor's source-system source_updated_at, so a contractor whose source timestamp predates onboarding, or one that has since been deactivated, may not produce a change signal — a periodic full walk of the feed (no changes_token) is still required.
Rate limited per client IP: 600 requests/minute for the feed, 600/minute for /mcp and the quote page. Over the limit returns 429. The JSON-LD feed and the per-company MCP server in sections 1 and 2 are unaffected.

Provider MCP server

POST/mcp

Install-base-wide, unlike the per-company server in section 2. It serves the request_service launcher — bound to the ui://widget/request-service.html card — plus a widget-accessible check_service_area helper. Both take the business_id from the feed (the contractor's pluginKey).

curl -s https://ai-interfaces.toolbox.contractorcommerce.com/mcp \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
       "params":{"name":"request_service","arguments":{"business_id":"000qeNvgDSrmHEbIejt1beAWureP3Z5HrsB3NZFq"}}}'

Both tools declare an outputSchema describing their structuredContent, and both validate their arguments: a missing or empty business_id returns a tool error naming the argument, while a well-formed id matching no active contractor returns a distinct No business found error. Both arrive as MCP tool errors (isError: true), never protocol errors. The launcher's structured result carries provider_business_id — pass that value (not the listing's id) to follow-up tool calls.

Quote page

GET/companies/000qeNvgDSrmHEbIejt1beAWureP3Z5HrsB3NZFq/quote

The contractor's public quote page — provider_action_url in the feed, and platform_url too for contractors with no funnel of their own.

When the contractor has a quote funnel of their own it 302s straight to it: their systems_estimate_url when the source has vouched for it, else their concom_shop_url storefront. Someone who clicked “Get Quote” wants the funnel, not another page with a button on it. The redirect is temporary by design — the destination comes from mirrored settings and can change on any sync.

Contractors with no funnel get the page itself instead: address, phone, published price ranges, and a link to their website. So the URL is always valid, which is what keeps the launcher appearing at all. Append ?source= to attribute the click (section 4) — tracking survives the redirect.

The launcher only appears once OpenAI has configured a partner plugin under the provider name the feed publishes (contractor_commerce). Publishing the feed is necessary but not sufficient.

7 · Pricing page + view model

Pricing view model

GET/companies/{pluginKey}/pricing.json

The data contract behind the WordPress Public Pricing Page feature: the company's published system pricing as one small JSON object, so a consumer never parses the full JSON-LD graph. Everything in it is already public — the same catalog the feed above serves.

FieldContents
businessName, website, E.164 phone, address, geo coordinates, and the service area (postal_code_count + the codes).
categories[]One row per published system type: name, slug, price_low/price_high, offer_count, and tiers (good/better/best starting prices; a tier the catalog doesn't publish is absent, never zero — render it as "Not published").
currencyISO code for every price in the payload.
price_descriptionWhat an installed price includes — render it next to any price you show.
data_provenanceWhy this data beats third-party estimates: first-party, current, non-anonymized pricing from the named local contractor. Render it near the prices — search and LLM retrieval weigh exactly this distinction against aggregator averages built from past anonymous jobs.
quote_urlThe CTA target. Redirects into the contractor's own quote funnel when they have one (section 6). Append ?source= to attribute clicks.
last_synced_at / freshness_stateSee section 5. Don't show prices unless fresh.
content_updated_atWhen the pricing content last actually changed — your sitemap <lastmod>. Unlike last_synced_at, a sync that changes nothing does not move it.
content_versionFingerprint of the pricing content (business, categories, copy, quote URL). Changes only when the content changes — cache your rendered page against it.

Example

curl https://ai-interfaces.toolbox.contractorcommerce.com/companies/000qeNvgDSrmHEbIejt1beAWureP3Z5HrsB3NZFq/pricing.json

Abbreviated response:

{
  "business": { "name": "Tudi Mechanical Systems", "address": { "locality": "…" },
                "service_area": { "postal_code_count": 91, "postal_codes": ["…"] } },
  "categories": [
    { "name": "A/C & Gas Furnace", "slug": "ac-gas-furnace",
      "price_low": 11006, "price_high": 29644, "offer_count": 24,
      "tiers": { "good": 11006, "better": 14074, "best": 26540 } }
  ],
  "currency": "USD",
  "price_description": "Fully-installed price. Includes installation and permits.",
  "data_provenance": "These are Tudi Mechanical Systems's own current installed prices — first-party pricing published by the local contractor that performs the work …",
  "quote_url": "https://ai-interfaces.toolbox.contractorcommerce.com/companies/000qeNvgDSrmHEbIejt1beAWureP3Z5HrsB3NZFq/quote",
  "last_synced_at": "2026-09-01T17:30:17+00:00",
  "content_updated_at": "2026-08-30T19:33:42+00:00",
  "freshness_state": "fresh",
  "content_version": "6f90f9a0802bfdec"
}

Polling cheaply

Responses carry an ETag and Cache-Control: public, max-age=300. Send If-None-Match with the last ETag and an unchanged payload answers 304 with no body. The ETag tracks the whole response (a sync advancing last_synced_at busts it); content_version tracks only the pricing content — use the ETag to poll, and content_version to decide whether a cached rendering needs regenerating.

WordPress plugins should fetch with ?source=wp and append ?source=wp to quote_url clicks so both ends of the funnel attribute (section 4). Unknown or inactive companies 404 — render your fallback, don't retry.

Hosted pricing page

GET/companies/000qeNvgDSrmHEbIejt1beAWureP3Z5HrsB3NZFq/pricing

A server-rendered reference implementation of the pricing page: the direct answer, the tier table, inclusions and the quote CTA — all present in the initial HTML response, no JavaScript required, with page-scoped JSON-LD (LocalBusiness + one Product per visible category) built from the same object as the visible content. It demonstrates the contract above; the SEO destination is the page on the contractor's own domain, not this one.

Sitemap inclusion (WordPress)

The pricing page belongs in the contractor's WordPress sitemap — that is where ranking equity should accrue. Register its URL through WordPress core's sitemap provider API (wp_sitemaps_register_provider, WP 5.5+), or the equivalent filter when an SEO plugin owns the sitemap (Yoast: wpseo_sitemap_index; Rank Math: rank_math/sitemap/index).

Use content_updated_at as the entry's <lastmod> — it moves when the pricing content changes, not on every sync. For exactness, publish max(content_updated_at, when you first saw the current content_version): the one change the timestamp cannot date is a system type disappearing outright, which flips content_version only.

List the URL only while the payload has categories and freshness_state is not unavailable — the same condition under which the page should be indexable at all. When that stops holding, drop the URL from the sitemap and serve the page noindex (the hosted reference page above behaves exactly this way) rather than advertising a page with no prices.