{
  "openapi": "3.1.0",
  "info": {
    "title": "Surfing Dog Network",
    "version": "1",
    "summary": "A public directory of businesses an AI can ask, book or buy from, through their own inboxes and other agent doors.",
    "description": "Everything here is a public read: no key, no sign-in, the same answer for everyone. The directory's order is published at GET /v1/ranking and cannot be bought; every query parameter only leaves businesses out. To book, order or ask, go to the business's own doors (a listing's doors, or a member's protocols); this network takes nothing. A business this network found on its own website is marked as found, with the date it was checked; only facts it published itself are shown, never a phone or an email. Errors are RFC 9457 problem documents. The protocol behind this API, with its JSON Schemas and test vectors, is public; the MCP server at /mcp offers the same reads as tools."
  },
  "externalDocs": {
    "description": "The network protocol",
    "url": "https://github.com/surfingdogai/inbox/blob/main/docs/protocol/network.md"
  },
  "servers": [{ "url": "https://network.surfingdog.ai" }],
  "tags": [
    { "name": "directory", "description": "Finding businesses." },
    { "name": "rules", "description": "How the directory is ordered." },
    { "name": "score", "description": "The agentic score: how far an agent can go with a business, by published rules." }
  ],
  "paths": {
    "/v1/businesses": {
      "get": {
        "operationId": "searchBusinesses",
        "tags": ["directory"],
        "summary": "Search the directory",
        "description": "Listed businesses in the order of the rules in force: the hourly order. Every parameter only leaves businesses out; nothing a business's profile says moves it. Software, API and AI companies (the group software-ai) are kept apart from local and retail businesses: they come only when category names that group or one of its categories, q names their kind (such as saas or ai agents), or every word of q is in one's name. Until rules version 7 takes effect, businesses that are not members come only after every member. A cached answer may be a few minutes old (a minute with open_now=true).",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "description": "Words that must all be in the business's name, city, description, categories (with their labels and synonyms), tags or services' names, ignoring case and accents. Words of one letter, and a few that say nothing about a business (and, the, in, near, de, em, perto, and the like), are ignored; a q with no word left is 400.",
            "schema": { "type": "string", "maxLength": 80 },
            "example": "haircut alfama"
          },
          {
            "name": "near",
            "in": "query",
            "description": "lat,lng: only businesses within radius_km, each with its distance_km.",
            "schema": { "type": "string", "pattern": "^-?\\d+(\\.\\d+)?,-?\\d+(\\.\\d+)?$" },
            "example": "38.72,-9.14"
          },
          {
            "name": "radius_km",
            "in": "query",
            "description": "With near: how far, in kilometres. 10 when left out.",
            "schema": { "type": "number", "exclusiveMinimum": 0, "maximum": 1000 }
          },
          {
            "name": "category",
            "in": "query",
            "description": "A group slug of GET /v1/categories, or an Overture place category id (GET /c/{id}) with every category below it, found by its id, slug, label or synonym, ignoring capitals and accents. Under rules before version 7 anything else matches a business's tags; from version 7 it is 400 category_unresolved with up to 5 candidates.",
            "schema": { "type": "string" },
            "example": "hair-beauty"
          },
          {
            "name": "item_type",
            "in": "query",
            "description": "Only businesses whose inbox takes it.",
            "schema": { "type": "string", "enum": ["booking", "order", "quote_request", "message", "refund"] }
          },
          {
            "name": "language",
            "in": "query",
            "description": "A language tag: a business that speaks it or a variant of it (pt keeps pt and pt-br; pt-br keeps pt-br only).",
            "schema": { "type": "string", "maxLength": 12, "pattern": "^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})*$" },
            "example": "pt"
          },
          {
            "name": "open_now",
            "in": "query",
            "description": "true: only businesses open at the moment of the answer by the hours they published, in their own time zone, a closure winning over the hours. A business that published no hours is left out, never shown as closed.",
            "schema": { "type": "boolean" }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 }
          },
          {
            "name": "cursor",
            "in": "query",
            "description": "next_cursor from the previous page of the same search. A cursor from another search or an earlier hour's order is 410 cursor_expired.",
            "schema": { "type": "string", "maxLength": 512 }
          },
          {
            "name": "attributes",
            "in": "query",
            "description": "Comma-separated keys of GET /v1/attributes (key=value for one with values), at most 10: every one must hold.",
            "schema": { "type": "string", "maxLength": 620 },
            "example": "walk_ins,wifi"
          },
          {
            "name": "country",
            "in": "query",
            "description": "ISO 3166-1 alpha-2: a business located in that country, serving it, or shipping there.",
            "schema": { "type": "string", "pattern": "^[A-Za-z]{2}$" },
            "example": "PT"
          },
          {
            "name": "price_band",
            "in": "query",
            "description": "1 (cheapest) to 4, as 2 or a range like 1-2.",
            "schema": { "type": "string", "pattern": "^[1-4](-[1-4])?$" }
          },
          {
            "name": "accepts",
            "in": "query",
            "description": "Comma-separated kinds (ask, quote, book, order, pay) a business's live doors take, and payments of GET /v1/attributes it accepts, at most 8: all must hold.",
            "schema": { "type": "string", "maxLength": 330 },
            "example": "book,card"
          },
          {
            "name": "requestable",
            "in": "query",
            "description": "A live door that declared it answers this kind.",
            "schema": { "type": "string", "enum": ["ask", "quote"] }
          },
          {
            "name": "door_type",
            "in": "query",
            "description": "Comma-separated door types (inbox, mcp, a2a, openapi, api, ucp, acp, nlweb, other, platform:<name>, or platform for any platform), at most 5: a live door of any of them.",
            "schema": { "type": "string", "maxLength": 250 }
          },
          {
            "name": "level",
            "in": "query",
            "description": "At this readiness level or above (orderable is bookable). Below askable nothing is listed yet: an empty page.",
            "schema": { "type": "string", "enum": ["listed", "readable", "askable", "bookable", "orderable", "payable"] }
          },
          {
            "name": "has_inbox",
            "in": "query",
            "description": "true: with an inbox door, ours or any compatible one; false: without one.",
            "schema": { "type": "boolean" }
          },
          {
            "name": "source",
            "in": "query",
            "description": "Comma-separated: member, registered, found; any of them.",
            "schema": { "type": "string", "maxLength": 40 }
          },
          {
            "name": "order",
            "in": "query",
            "description": "rank, the published order, by default; nearest sorts by distance and needs near (rules version 7).",
            "schema": { "type": "string", "enum": ["rank", "nearest"] }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of the directory.",
            "headers": { "ETag": { "$ref": "#/components/headers/ETag" } },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["businesses", "next_cursor"],
                  "properties": {
                    "businesses": { "type": "array", "items": { "$ref": "#/components/schemas/Listing" } },
                    "next_cursor": { "type": ["string", "null"] }
                  }
                }
              }
            }
          },
          "304": { "description": "The ETag sent in If-None-Match still holds." },
          "400": { "$ref": "#/components/responses/Problem" },
          "410": { "$ref": "#/components/responses/Problem" },
          "503": { "description": "The search ran out of time (10 seconds): try again in a minute.", "headers": { "Retry-After": { "schema": { "type": "integer" } } } }
        }
      }
    },
    "/v1/businesses/{domain}": {
      "get": {
        "operationId": "getBusiness",
        "tags": ["directory"],
        "summary": "One listed business",
        "description": "The listing, the distinct customers its receipts came from, and a count for every outcome code. For a business that is not a member, every displayed value with its source and date (facts); one found by this network is served with X-Robots-Tag: noindex. A business that left the directory, or was set aside, is 404 and the problem's detail says which.",
        "parameters": [{ "$ref": "#/components/parameters/Domain" }],
        "responses": {
          "200": {
            "description": "The listing and its outcomes.",
            "headers": { "ETag": { "$ref": "#/components/headers/ETag" } },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Listing" } } }
          },
          "304": { "description": "The ETag sent in If-None-Match still holds." },
          "400": { "$ref": "#/components/responses/Problem" },
          "404": { "$ref": "#/components/responses/Problem" }
        }
      }
    },
    "/v1/categories": {
      "get": {
        "operationId": "listCategories",
        "tags": ["directory"],
        "summary": "The categories list",
        "description": "Every slug with its labels and its kind: local_and_retail, or software_and_ai for software, API and AI companies, kept apart (a search for one kind never returns the other); the full list, with synonyms, is packages/spec/vocab/categories.json in the protocol's repository. With group, also that group's place categories and, for Overture's, the taxonomy they come from (Overture Maps Foundation, CC BY 4.0); software-ai's categories are the network's own, and come without a taxonomy.",
        "parameters": [
          {
            "name": "group",
            "in": "query",
            "description": "A slug of the list: adds place_categories and taxonomy.",
            "schema": { "type": "string" },
            "example": "hair-beauty"
          }
        ],
        "responses": {
          "200": {
            "description": "The categories list.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["version", "categories"],
                  "properties": {
                    "version": { "type": "integer", "minimum": 1 },
                    "categories": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": ["slug", "labels"],
                        "properties": {
                          "slug": { "type": "string", "pattern": "^[a-z][a-z0-9]*(-[a-z0-9]+)*$" },
                          "kind": { "type": "string", "enum": ["local_and_retail", "software_and_ai"] },
                          "labels": {
                            "type": "object",
                            "description": "Language → label.",
                            "additionalProperties": { "type": "string" }
                          }
                        }
                      }
                    },
                    "place_categories": {
                      "type": "array",
                      "description": "With group: every place category of that group.",
                      "items": {
                        "type": "object",
                        "required": ["id", "label", "parent"],
                        "properties": {
                          "id": { "type": "string" },
                          "label": { "type": "string" },
                          "parent": { "type": ["string", "null"] }
                        }
                      }
                    },
                    "taxonomy": { "$ref": "#/components/schemas/Taxonomy" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/attributes": {
      "get": {
        "operationId": "listAttributes",
        "tags": ["directory"],
        "summary": "The attributes a search may ask for",
        "description": "Every attribute key with its group, type, values and labels, and the payments accepts takes. A key that needs a register's proof is listed with filterable false: not filterable or shown yet.",
        "responses": {
          "200": {
            "description": "The attributes and payments.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["version", "keys", "payments"],
                  "properties": {
                    "version": { "type": "integer", "minimum": 1 },
                    "keys": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": ["key", "group", "type", "labels", "filterable"],
                        "properties": {
                          "key": { "type": "string" },
                          "group": { "type": "string" },
                          "type": { "type": "string", "enum": ["bool", "enum"] },
                          "values": { "type": "array", "items": { "type": "string" } },
                          "labels": { "type": "object", "additionalProperties": { "type": "string" } },
                          "filterable": { "type": "boolean" }
                        }
                      }
                    },
                    "payments": {
                      "type": "object",
                      "required": ["methods", "wallets", "agent"],
                      "properties": {
                        "methods": { "type": "array", "items": { "type": "string" } },
                        "wallets": { "type": "array", "items": { "type": "string" } },
                        "agent": { "type": "array", "items": { "type": "string" } }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/c/{id}": {
      "get": {
        "operationId": "getPlaceCategory",
        "tags": ["directory"],
        "summary": "One place category",
        "description": "A place category by its id: its label, parent and path, its group, and why businesses that are not members are not listed in it, when they are not. Overture's come with the taxonomy they are from (Overture Maps Foundation, CC BY 4.0). Since 7 Oct 2026 the group software-ai's categories (software_and_ai and the four below it) are the network's own, not Overture's, and come without taxonomy: it is required no more.",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "example": "hair_salon" }
        ],
        "responses": {
          "200": {
            "description": "The category.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["id", "label", "parent", "path"],
                  "properties": {
                    "id": { "type": "string" },
                    "label": { "type": "string" },
                    "parent": { "type": ["string", "null"] },
                    "path": { "type": "array", "items": { "type": "string" } },
                    "group": { "type": "string" },
                    "regulated": { "type": "string" },
                    "taxonomy": { "$ref": "#/components/schemas/Taxonomy" }
                  }
                }
              }
            }
          },
          "404": { "$ref": "#/components/responses/Problem" }
        }
      }
    },
    "/v1/ranking": {
      "get": {
        "operationId": "getRules",
        "tags": ["rules"],
        "summary": "The rules that order the directory",
        "description": "The version in force, or any version ever published. Every number the rules use is here, with the changelog and the next version when one is announced.",
        "parameters": [
          {
            "name": "version",
            "in": "query",
            "schema": { "type": "integer", "minimum": 1 }
          }
        ],
        "responses": {
          "200": {
            "description": "The rules.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "The rules document.",
                  "externalDocs": {
                    "description": "Its JSON Schema",
                    "url": "https://github.com/surfingdogai/inbox/blob/main/packages/spec/schemas/ranking.json"
                  },
                  "required": ["version", "status"],
                  "properties": {
                    "version": { "type": "integer", "minimum": 1 },
                    "status": { "type": "string" }
                  }
                }
              }
            }
          },
          "404": { "$ref": "#/components/responses/Problem" }
        }
      }
    },
    "/v1/score-rules": {
      "get": {
        "operationId": "getScoreRules",
        "tags": ["score"],
        "summary": "The rules of the agentic score",
        "description": "How far an AI agent can go with a business through the doors it published: the capabilities, their groups and weights, what applies to each kind of business, the formula, the grades and the fixes. The agentic score never changes the directory's order.",
        "parameters": [
          {
            "name": "version",
            "in": "query",
            "schema": { "type": "integer", "minimum": 1 }
          }
        ],
        "responses": {
          "200": {
            "description": "The score rules.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "The score rules document.",
                  "externalDocs": {
                    "description": "Its JSON Schema",
                    "url": "https://github.com/surfingdogai/inbox/blob/main/packages/spec/schemas/score-rules.json"
                  },
                  "required": ["version", "status", "groups", "profiles", "formula"],
                  "properties": {
                    "version": { "type": "integer", "minimum": 1 },
                    "status": { "type": "string" },
                    "groups": { "type": "array", "items": { "type": "object" } },
                    "profiles": { "type": "array", "items": { "type": "object" } },
                    "formula": { "type": "string" }
                  }
                }
              }
            }
          },
          "404": { "$ref": "#/components/responses/Problem" }
        }
      }
    },
    "/leaderboard.json": {
      "get": {
        "operationId": "getLeaderboard",
        "tags": ["score"],
        "summary": "Businesses ordered by agentic score",
        "description": "By category group, country and place, or over everyone: agentic score, then the most recent check, then domain. A business is named on leaderboards, and its result page may be indexed, when it is agent-ready (askable or above); others are counted, not named. If your site tells AI systems not to use or train on its content, we don't name you or list you publicly. It is not the directory's search order.",
        "parameters": [
          { "name": "category", "in": "query", "description": "A category group (GET /v1/categories).", "schema": { "type": "string" } },
          { "name": "country", "in": "query", "description": "A two-letter country code.", "schema": { "type": "string", "pattern": "^[A-Za-z]{2}$" } },
          { "name": "place", "in": "query", "description": "A place in that country, in lower case with hyphens; needs country.", "schema": { "type": "string", "pattern": "^[a-z0-9-]{1,80}$" } },
          { "name": "page", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 20 } }
        ],
        "responses": {
          "200": {
            "description": "One page of fifty named businesses, and how many more were checked.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "externalDocs": {
                    "description": "Its JSON Schema",
                    "url": "https://github.com/surfingdogai/inbox/blob/main/packages/spec/schemas/leaderboard.json"
                  },
                  "required": ["scope", "rules", "order", "not_search_order", "total", "unnamed", "page", "next_page", "named"],
                  "properties": {
                    "scope": { "type": "object" },
                    "rules": { "type": "object" },
                    "order": { "type": "string" },
                    "not_search_order": { "type": "string" },
                    "total": { "type": "integer" },
                    "unnamed": { "type": "integer" },
                    "page": { "type": "integer" },
                    "next_page": { "type": ["integer", "null"] },
                    "named": { "type": "array", "items": { "type": "object" } }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/Problem" }
        }
      }
    },
    "/b/{domain}.json": {
      "get": {
        "operationId": "getCheckResult",
        "tags": ["score"],
        "summary": "Where one business's check stands, and its agentic score",
        "description": "A business's result: not checked, queued, checking, scoring, done, blocked, failed, hidden by its owner, or not checkable. Once done: its agentic score and grade by the published rules, the five answers (can an agent message, book, order, cancel or negotiate there) each with its door and date, every capability, the fixes worth most points, its rank, and whether its page may be indexed. Reading it never starts a check: POST /check or the MCP tool check_business does. A different spelling of the domain is redirected to its canonical one.",
        "parameters": [
          { "name": "domain", "in": "path", "required": true, "description": "The business's registrable domain, such as salon.example.", "schema": { "type": "string", "maxLength": 253 } }
        ],
        "responses": {
          "200": {
            "description": "The result.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "externalDocs": { "description": "Its JSON Schema", "url": "https://github.com/surfingdogai/inbox/blob/main/packages/spec/schemas/check-result.json" },
                  "required": ["domain", "url", "state", "indexable"],
                  "properties": {
                    "domain": { "type": "string" },
                    "url": { "type": "string", "format": "uri" },
                    "state": { "type": "string", "enum": ["not_checked", "queued", "checking", "scoring", "done", "blocked", "failed", "hidden", "not_checkable"] },
                    "agentic_score": { "type": "integer", "minimum": 0, "maximum": 100 },
                    "grade": { "type": "string", "enum": ["A", "B", "C", "D", "E"] },
                    "answers": { "type": "array", "items": { "type": "object" } },
                    "capabilities": { "type": "array", "items": { "type": "object" } },
                    "fixes": { "type": "array", "items": { "type": "object" } },
                    "indexable": { "type": "boolean" }
                  }
                }
              }
            }
          },
          "301": { "description": "Another spelling of the domain: the canonical result." },
          "404": { "description": "This site cannot be checked." }
        }
      }
    },
    "/v1/stats": {
      "get": {
        "operationId": "getStats",
        "tags": ["directory"],
        "summary": "The directory in numbers",
        "responses": {
          "200": {
            "description": "Counts of listed and answering businesses, the last day's activity and the receipts held.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "instances_online",
                    "businesses_listed",
                    "activity_24h",
                    "receipts_issued",
                    "receipts_acknowledged",
                    "reviews_published",
                    "status",
                    "directory",
                    "generated_at"
                  ],
                  "properties": {
                    "instances_online": { "type": "integer", "minimum": 0 },
                    "businesses_listed": { "type": "integer", "minimum": 0 },
                    "activity_24h": {
                      "type": "object",
                      "properties": {
                        "bookings": { "type": "integer", "minimum": 0 },
                        "orders": { "type": "integer", "minimum": 0 },
                        "quotes": { "type": "integer", "minimum": 0 },
                        "messages": { "type": "integer", "minimum": 0 }
                      }
                    },
                    "receipts_issued": { "type": "integer", "minimum": 0 },
                    "receipts_acknowledged": { "type": "integer", "minimum": 0 },
                    "reviews_published": { "type": "integer", "minimum": 0 },
                    "status": {
                      "type": "object",
                      "properties": {
                        "api": { "type": "string", "enum": ["ok", "degraded"] },
                        "jobs_lag_seconds": { "type": "number" },
                        "last_sweep_at": { "type": ["string", "null"], "format": "date-time" }
                      }
                    },
                    "directory": {
                      "type": "object",
                      "description": "The sites the crawler has checked, and how many of them are agent-ready, can do each thing, or have each kind of door. Counts only, never a name or a domain, taken at most every ten minutes; every key is present, 0 or not.",
                      "required": ["checked", "agent_ready", "capabilities", "doors", "as_of"],
                      "properties": {
                        "checked": { "type": "integer", "minimum": 0, "description": "Sites the crawler has checked at least once: not hidden by their owner, not suppressed, and of a kind the directory lists." },
                        "agent_ready": { "type": "integer", "minimum": 0, "description": "Of those, askable or above on a site that does not tell AI systems to keep away, or a member of the network the directory lists." },
                        "software_and_ai": { "type": "integer", "minimum": 0, "description": "Of the agent-ready, how many are software, API and AI companies (the category group software-ai), kept apart from local and retail businesses." },
                        "capabilities": {
                          "type": "object",
                          "description": "Agent-ready businesses whose capability state is yes for each (GET /v1/score-rules), leaving out a yes that rests on experimental evidence alone.",
                          "required": ["message", "book", "order", "cancel", "change", "negotiate", "catalogue", "availability", "pay", "track", "return"],
                          "properties": {
                            "message": { "type": "integer", "minimum": 0 },
                            "book": { "type": "integer", "minimum": 0 },
                            "order": { "type": "integer", "minimum": 0 },
                            "cancel": { "type": "integer", "minimum": 0 },
                            "change": { "type": "integer", "minimum": 0 },
                            "negotiate": { "type": "integer", "minimum": 0 },
                            "catalogue": { "type": "integer", "minimum": 0 },
                            "availability": { "type": "integer", "minimum": 0 },
                            "pay": { "type": "integer", "minimum": 0 },
                            "track": { "type": "integer", "minimum": 0 },
                            "return": { "type": "integer", "minimum": 0 }
                          }
                        },
                        "doors": {
                          "type": "object",
                          "description": "Agent-ready businesses with a live door of each type; a member's own inbox is an inbox door while it answers (pinged in the last 24 hours). WebMCP is experimental: it is counted here, and never toward a level or a listing.",
                          "required": ["inbox", "mcp", "a2a", "openapi", "ucp", "acp", "webmcp"],
                          "properties": {
                            "inbox": { "type": "integer", "minimum": 0 },
                            "mcp": { "type": "integer", "minimum": 0 },
                            "a2a": { "type": "integer", "minimum": 0 },
                            "openapi": { "type": "integer", "minimum": 0 },
                            "ucp": { "type": "integer", "minimum": 0 },
                            "acp": { "type": "integer", "minimum": 0 },
                            "webmcp": { "type": "integer", "minimum": 0 }
                          }
                        },
                        "as_of": { "type": "string", "format": "date-time", "description": "When the counts were taken. The zero time (0001-01-01T00:00:00Z), with status.api degraded, when none could be taken yet: the zeros are then no counts." }
                      }
                    },
                    "generated_at": { "type": "string", "format": "date-time" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/instances/{domain}/status": {
      "get": {
        "operationId": "getInstanceStatus",
        "tags": ["directory"],
        "summary": "Where a registered inbox stands",
        "description": "What an inbox polls after it registers: its status, whether the directory lists it, and its verification.",
        "parameters": [{ "$ref": "#/components/parameters/Domain" }],
        "responses": {
          "200": {
            "description": "The status.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["domain", "status", "listed", "manifest_url"],
                  "properties": {
                    "domain": { "type": "string" },
                    "status": { "type": "string", "enum": ["pending", "verified", "unreachable"] },
                    "listed": { "type": "boolean" },
                    "delisted_at": { "type": ["string", "null"], "format": "date-time" },
                    "dormant_since": { "type": ["string", "null"], "format": "date-time" },
                    "verified_at": { "type": ["string", "null"], "format": "date-time" },
                    "last_checked_at": { "type": ["string", "null"], "format": "date-time" },
                    "last_ping_at": { "type": ["string", "null"], "format": "date-time" },
                    "fail_count": { "type": "integer", "minimum": 0 },
                    "manifest_url": { "type": "string", "format": "uri" },
                    "verification": {
                      "type": "object",
                      "properties": {
                        "attempts": { "type": "integer", "minimum": 0 },
                        "next_attempt_at": { "type": "string", "format": "date-time" },
                        "last_error": { "type": "string" }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/Problem" },
          "404": { "$ref": "#/components/responses/Problem" }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "Domain": {
        "name": "domain",
        "in": "path",
        "required": true,
        "description": "The business's domain, like anasalon.pt.",
        "schema": { "type": "string", "maxLength": 253 }
      }
    },
    "headers": {
      "ETag": {
        "description": "Send it back in If-None-Match for a 304 while the answer holds.",
        "schema": { "type": "string" }
      }
    },
    "responses": {
      "Problem": {
        "description": "An RFC 9457 problem document.",
        "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } }
      }
    },
    "schemas": {
      "Problem": {
        "type": "object",
        "required": ["type", "title", "status"],
        "properties": {
          "type": { "type": "string" },
          "title": { "type": "string" },
          "status": { "type": "integer" },
          "code": { "type": "string", "description": "What to do differently, when a status alone does not say: cursor_expired, not_found, too_large, rate_limited, category_unresolved, page_too_deep." },
          "detail": { "type": "string" },
          "instance": { "type": "string" },
          "candidates": { "type": "array", "items": { "type": "string" }, "description": "category_unresolved: up to 5 categories to try." }
        }
      },
      "Taxonomy": {
        "type": "object",
        "description": "The place taxonomy categories come from, its release and its licence.",
        "required": ["name", "release", "licence", "url"],
        "properties": {
          "name": { "type": "string" },
          "release": { "type": "string" },
          "licence": { "type": "string" },
          "url": { "type": "string", "format": "uri" }
        }
      },
      "Door": {
        "type": "object",
        "description": "A machine door the business published for agents; mail, phone, messaging, forms and web pages are never doors.",
        "required": ["type", "url", "level", "status", "kinds", "src"],
        "properties": {
          "type": { "type": "string", "pattern": "^(inbox|mcp|a2a|openapi|api|ucp|acp|nlweb|webhook|other|platform:[a-z0-9-]{1,40})$" },
          "url": { "type": "string", "format": "uri" },
          "level": { "type": "string", "enum": ["listed", "readable", "askable", "bookable", "payable"] },
          "status": { "type": "string", "enum": ["live", "failing"] },
          "kinds": { "type": "array", "items": { "type": "string", "enum": ["ask", "quote", "book", "order", "pay"] } },
          "src": { "type": "string", "enum": ["declared", "seen"] },
          "protocol": { "type": "string" },
          "checked_at": { "type": "string", "format": "date-time" }
        }
      },
      "PlaceCategory": {
        "type": "object",
        "required": ["id", "label", "src"],
        "properties": {
          "id": { "type": "string" },
          "label": { "type": "string" },
          "src": { "type": "string", "enum": ["declared", "seen", "probably"] }
        }
      },
      "Fact": {
        "type": "object",
        "description": "A displayed value with its source and date.",
        "required": ["field", "v", "src", "at"],
        "properties": {
          "field": { "type": "string" },
          "v": {},
          "src": { "type": "string", "enum": ["declared", "seen", "probably"] },
          "url": { "type": "string", "format": "uri" },
          "at": { "type": "string", "format": "date-time" },
          "via": { "type": "string" }
        }
      },
      "Address": {
        "type": "object",
        "description": "The street only when the business published one.",
        "properties": {
          "street": { "type": "string" },
          "locality": { "type": "string" },
          "postal_code": { "type": "string" },
          "country": { "type": "string", "minLength": 2, "maxLength": 2 }
        }
      },
      "Hours": {
        "type": "object",
        "description": "Weekly windows [opens, closes) in the business's own time zone, and the closures that have not ended (whole days, both included).",
        "required": ["timezone", "weekly", "closures"],
        "properties": {
          "timezone": { "type": "string", "example": "Europe/Lisbon" },
          "weekly": {
            "type": "object",
            "propertyNames": { "enum": ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] },
            "additionalProperties": {
              "type": "array",
              "maxItems": 6,
              "items": {
                "type": "array",
                "prefixItems": [
                  { "type": "string", "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$" },
                  { "type": "string", "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$" }
                ],
                "minItems": 2,
                "maxItems": 2
              }
            }
          },
          "closures": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["from", "to"],
              "properties": {
                "from": { "type": "string", "format": "date" },
                "to": { "type": "string", "format": "date" }
              }
            }
          }
        }
      },
      "Service": {
        "type": "object",
        "required": ["name", "type"],
        "properties": {
          "name": { "type": "string", "maxLength": 80 },
          "type": { "type": "string", "enum": ["booking", "order", "quote_request"] }
        }
      },
      "Receipts": {
        "type": "object",
        "description": "Promises the business signed and published here.",
        "required": ["issued", "acknowledged"],
        "properties": {
          "issued": { "type": "integer", "minimum": 0 },
          "acknowledged": { "type": "integer", "minimum": 0 },
          "customers": { "type": "integer", "minimum": 0, "description": "On a single business only." },
          "last_at": { "type": "string", "format": "date-time" }
        }
      },
      "Reputation": {
        "type": "object",
        "description": "From the last nightly snapshot. The tiers are new, building and trusted: nothing below new.",
        "required": ["ranked", "score", "tier", "kept", "broken", "customers", "verified_share", "rules", "rules_url"],
        "properties": {
          "ranked": { "type": "boolean", "description": "Sorts before the daily shuffle (building or better)." },
          "score": { "type": "number", "minimum": 0, "maximum": 1 },
          "tier": { "type": "string", "enum": ["new", "building", "trusted"] },
          "kept": { "type": "integer", "minimum": 0 },
          "broken": { "type": "integer", "minimum": 0 },
          "customers": { "type": "integer", "minimum": 0 },
          "verified_share": { "type": "number", "minimum": 0, "maximum": 1 },
          "rules": { "type": "integer", "minimum": 1 },
          "rules_url": { "type": "string", "format": "uri" }
        }
      },
      "Listing": {
        "type": "object",
        "description": "A listed business. Name, description, city, address, tags and services are the business's own words. Nothing about any customer is ever returned.",
        "required": [
          "domain",
          "name",
          "categories",
          "tags",
          "languages",
          "item_types",
          "protocols",
          "open_now",
          "services",
          "receipts",
          "answering",
          "online",
          "not_answering_since"
        ],
        "properties": {
          "domain": { "type": "string" },
          "name": { "type": "string" },
          "description": { "type": "string" },
          "city": { "type": "string" },
          "country": { "type": "string", "minLength": 2, "maxLength": 2 },
          "address": { "$ref": "#/components/schemas/Address" },
          "categories": { "type": "array", "items": { "type": "string" }, "description": "Slugs of GET /v1/categories." },
          "tags": { "type": "array", "items": { "type": "string" } },
          "languages": { "type": "array", "items": { "type": "string" } },
          "item_types": { "type": "array", "items": { "type": "string" }, "description": "What its inbox takes." },
          "protocols": {
            "type": "object",
            "description": "Protocol name → the inbox's entry URL. Book, order or ask at mcp, rest or openapi; the others, the owner's own door (mcp_owner) among them, are not for customers.",
            "additionalProperties": { "type": "string" }
          },
          "hours": { "$ref": "#/components/schemas/Hours" },
          "open_now": { "type": ["boolean", "null"], "description": "null: it published no hours." },
          "services": { "type": "array", "items": { "$ref": "#/components/schemas/Service" } },
          "geo": {
            "type": "object",
            "required": ["lat", "lng"],
            "properties": { "lat": { "type": "number" }, "lng": { "type": "number" } }
          },
          "distance_km": { "type": "number", "minimum": 0, "description": "Near searches only." },
          "url": { "type": "string", "description": "The business's own web site." },
          "manifest_url": { "type": "string", "format": "uri", "description": "Always present for a member." },
          "verified_at": { "type": "string", "format": "date-time", "description": "Always present for a member." },
          "last_ping_at": { "type": "string", "format": "date-time" },
          "software": {
            "type": "object",
            "properties": { "version": { "type": "string" }, "runtime": { "type": "string" } }
          },
          "receipts": { "$ref": "#/components/schemas/Receipts" },
          "answering": { "type": "boolean", "description": "Its inbox answered within the last day." },
          "online": { "type": "boolean", "description": "The same as answering." },
          "not_answering_since": { "type": ["string", "null"], "format": "date-time" },
          "rank_pos": { "type": "integer", "minimum": 1, "description": "Its place in this hour's order." },
          "rank_shuffle": { "type": "string", "pattern": "^[0-9a-f]{16}$", "description": "The day's shuffle key: a string, never a number." },
          "reputation": { "$ref": "#/components/schemas/Reputation" },
          "source": { "type": "string", "enum": ["member", "registered", "found"], "description": "member: through its inbox; registered: by the business, with a proof; found: by this network on its own website." },
          "claimed": { "type": "boolean" },
          "proof": { "type": "string", "enum": ["domain", "key", "platform", "code"] },
          "level": { "type": "string", "enum": ["listed", "readable", "askable", "bookable", "payable"], "description": "The highest level of its live doors." },
          "has_inbox": { "type": "boolean" },
          "doors": { "type": "array", "items": { "$ref": "#/components/schemas/Door" } },
          "requestable": { "type": "array", "items": { "type": "string", "enum": ["ask", "quote"] }, "description": "The kinds a live door declared it answers." },
          "accepts": {
            "type": "object",
            "required": ["kinds"],
            "properties": {
              "kinds": { "type": "array", "items": { "type": "string", "enum": ["ask", "quote", "book", "order", "pay"] } },
              "pay": { "type": "array", "items": { "type": "string" } }
            }
          },
          "category": {
            "type": "object",
            "required": ["primary", "alternates", "path"],
            "properties": {
              "primary": { "$ref": "#/components/schemas/PlaceCategory" },
              "alternates": { "type": "array", "maxItems": 2, "items": { "$ref": "#/components/schemas/PlaceCategory" } },
              "path": { "type": "array", "items": { "type": "string" } },
              "group": { "type": "string" }
            }
          },
          "attributes": {
            "type": "object",
            "description": "Keys of GET /v1/attributes, each with its value and source.",
            "additionalProperties": {
              "type": "object",
              "required": ["v", "src"],
              "properties": { "v": { "type": ["boolean", "string", "number"] }, "src": { "type": "string", "enum": ["declared", "seen", "probably"] } }
            }
          },
          "place": {
            "type": "object",
            "description": "For a business that is not a member: its town, region and country, never the street until it claims its entry.",
            "required": ["kind"],
            "properties": {
              "locality": { "type": "string" },
              "region": { "type": "string" },
              "country": { "type": "string", "minLength": 2, "maxLength": 2 },
              "kind": { "type": "array", "items": { "type": "string", "enum": ["storefront", "service_area", "online"] } },
              "service_area": {
                "type": "object",
                "properties": {
                  "radius_km": { "type": "number", "exclusiveMinimum": 0, "maximum": 300 },
                  "countries": { "type": "array", "maxItems": 50, "items": { "type": "string" } }
                }
              },
              "ships_to": { "type": "array", "items": { "type": "string" } }
            }
          },
          "why": { "type": "string", "description": "Why it is in this place of the list, in plain words." },
          "found": {
            "type": "object",
            "required": ["note", "checked_at", "about_url"],
            "properties": {
              "note": { "type": "string" },
              "checked_at": { "type": "string", "format": "date-time" },
              "about_url": { "type": "string", "format": "uri", "description": "Why it is here, and how to correct it or opt out." }
            }
          },
          "outcomes": {
            "type": "object",
            "description": "On a single business only: how many of its promises closed with each outcome code.",
            "additionalProperties": { "type": "integer", "minimum": 0 }
          },
          "facts": { "type": "array", "items": { "$ref": "#/components/schemas/Fact" }, "description": "On a single business that is not a member: every displayed value with its source and date." }
        }
      }
    }
  }
}
