{
    "api_version": "v1",
    "status": "beta",
    "store": {
        "code": "en-gb",
        "locale": "en-GB",
        "currency": "GBP",
        "prices_include_tax": true,
        "tax_rate_percent": 20
    },
    "environment": {
        "name": "staging",
        "api_host": "https://staging.surepetcare.com",
        "canonical_site": "https://www.surepetcare.com",
        "note": "This API answered from https://staging.surepetcare.com, which is not the public website. Every canonical_url points at https://www.surepetcare.com because those URLs come from the product data, not from this request. Quote canonical_url to a person; use api_resource to fetch more from here. Do not rewrite one into the other."
    },
    "generated_at": "2026-08-26T06:53:30+00:00",
    "scope": {
        "is_complete_catalogue": false,
        "note": "Proof of concept. This endpoint serves a fixed subset of products, not the full Sure Petcare range. Absence from these results does not mean a product does not exist.",
        "product_ids_in_scope": [
            1,
            2,
            3,
            4,
            5,
            97,
            104,
            112,
            117,
            152
        ]
    },
    "data": {
        "name": "Sure Petcare agent product API",
        "status": "beta",
        "stage": "proof_of_concept",
        "read_only": true,
        "is_complete_catalogue": false,
        "status_note": "Beta proof of concept. Read-only: there is no endpoint here that changes anything, and no plan for one. It serves a small fixed subset of the Sure Petcare range, so it is NOT a complete catalogue: absence from these results does not mean a product does not exist. Fall back to surepetcare.com before telling anyone Sure Petcare does not sell something. Field names and response shapes may still change while this is in beta.",
        "authentication": "none required",
        "capabilities": [
            {
                "name": "list_products",
                "method": "GET",
                "endpoint": "https://staging.surepetcare.com/api/agent/v1/products",
                "parameters": {
                    "page": {
                        "type": "integer",
                        "required": false,
                        "default": 1,
                        "minimum": 1,
                        "description": "Page of results, 1-based."
                    },
                    "per_page": {
                        "type": "integer",
                        "required": false,
                        "default": 25,
                        "maximum": 50,
                        "description": "Values above the maximum are clamped to it, not rejected."
                    },
                    "store": {
                        "type": "string",
                        "required": false,
                        "default": "en-gb",
                        "enum": [
                            "en-gb"
                        ],
                        "description": "Store code. Decides currency, tax treatment, price, availability and translated text."
                    }
                },
                "description": "Everything in scope, as summaries: id, slug, name, price, availability and relationship counts. Enough to choose a product without fetching all of them. Paging is reported in \"paging\"."
            },
            {
                "name": "search_products",
                "method": "GET",
                "endpoint": "https://staging.surepetcare.com/api/agent/v1/search",
                "parameters": {
                    "q": {
                        "type": "string",
                        "required": true,
                        "min_length": 2,
                        "description": "Free text. Matched against name, SKU, tagline, category and specification values."
                    },
                    "include_weak": {
                        "type": "boolean",
                        "required": false,
                        "default": false,
                        "description": "Include results whose only match was a word in the specification table. Off by default when better results exist; the number withheld is reported as weak_omitted."
                    },
                    "per_page": {
                        "type": "integer",
                        "required": false,
                        "default": 25,
                        "maximum": 50,
                        "description": "Values above the maximum are clamped to it, not rejected."
                    },
                    "store": {
                        "type": "string",
                        "required": false,
                        "default": "en-gb",
                        "enum": [
                            "en-gb"
                        ],
                        "description": "Store code. Decides currency, tax treatment, price, availability and translated text."
                    }
                },
                "description": "Find products by name, SKU, category or specification value. Results carry relevance, matched_on, matched_terms and match_quality."
            },
            {
                "name": "get_product",
                "method": "GET",
                "endpoint": "https://staging.surepetcare.com/api/agent/v1/products/{id_or_slug}",
                "parameters": {
                    "id_or_slug": {
                        "type": "string",
                        "required": true,
                        "in": "path",
                        "pattern": "^[A-Za-z0-9_-]+$",
                        "description": "Numeric product id, or the slug from the product record (the last segment of canonical_url). Both resolve to the same record."
                    },
                    "store": {
                        "type": "string",
                        "required": false,
                        "default": "en-gb",
                        "enum": [
                            "en-gb"
                        ],
                        "description": "Store code. Decides currency, tax treatment, price, availability and translated text."
                    }
                },
                "description": "Full record: price, availability, specifications, device limits, accessories, and what the product requires to work."
            },
            {
                "name": "check_compatibility",
                "method": "GET",
                "endpoint": "https://staging.surepetcare.com/api/agent/v1/compatibility",
                "parameters": {
                    "product": {
                        "type": "string",
                        "required": true,
                        "description": "One of the pair. A numeric product id or a slug."
                    },
                    "with": {
                        "type": "string",
                        "required": true,
                        "description": "The other of the pair. Order does not matter. A numeric product id or a slug."
                    },
                    "store": {
                        "type": "string",
                        "required": false,
                        "default": "en-gb",
                        "enum": [
                            "en-gb"
                        ],
                        "description": "Store code. Decides currency, tax treatment, price, availability and translated text."
                    }
                },
                "description": "Whether two products work together. Returns compatible, incompatible, requires, requires_intermediary or unknown, with a reason and the source of the answer."
            }
        ],
        "conventions": {
            "prices": "Include tax for the requested store. The currency and tax rate are stated on every response.",
            "freshness": "generated_at is the moment the data was read. There is no cache, export or copy between this response and the live product data the website itself reads, so a value here was true at that moment.",
            "availability": "Four booleans, carrying the same meanings they carry on the website. \"published\" — listed in this store. \"available\" — currently sold there. \"in_stock\" — there is stock for this store's fulfilment house above the reserve held back, and nothing bars this customer from buying it; the rule is the website's own, so this is the same answer the product page gives. \"purchasable\" — the one to act on, and the same combination the website uses to decide whether to show a buy button: available and in stock, in a store where the product is published. Answered for an anonymous shopper, which is who the public pages answer too; a trade account can see a different reserve. Stock moves, so a value read here can be out of date by the time anyone acts on it, exactly as a value read from the page can be — canonical_url is where to send a person, and only a basket actually holds anything.",
            "urls": "canonical_url is the product's public address for a person, and is the same on every environment because it comes from the product data. api_resource is where to fetch the same product from the deployment that answered, named in \"environment\". They are not interchangeable.",
            "identifiers": "id and slug are both stable and both accepted wherever a product is named. slug is the last segment of canonical_url, so an agent holding only a page URL can reach the record without a search.",
            "unknown": "An unknown answer is returned as verdict \"unknown\". It never means \"no\".",
            "requirements": "A product's \"requires\" entries say what is needed and what for. required_for names the features that depend on the other product, and necessity says how far the dependency goes: \"connected_features_only\" (the product does its own job without it), \"intended_purpose\" (it operates, but the reason for owning it does not) or \"unknown\". standalone_operable is the short form: true when the product does the job it is sold for alone, false when a requirement reaches that job, null when a recorded requirement has never been explained. false does not always mean the hardware is inert — read the reason, which says what is lost. It is derived from product data, not a maintained flag, and is never inferred from silence.",
            "search": "match_quality is \"strong\" when something identifying matched (name, SKU, tagline, category) and \"weak\" when the only hit was a word in the specification table. Weak results are withheld while stronger ones exist, and weak_omitted says how many. Relevance is comparable within one response only.",
            "aspect": "Compatibility is not one question, so every statement says what it is about: \"overall\" (do they work together at all), \"accessories\" (do parts bought for one fit the other) or \"app_pairing\" (do they pair over the Connect network). The verdict at the top of the response answers the broadest aspect held; anything narrower is in \"additional_statements\" and should be quoted alongside it, not instead of it. If the only statement held is a narrow one, the response carries \"verdict_applies_to\" saying so, and the verdict must not be repeated as a general one.",
            "confidence": "Nothing here is presented as an authoritative fact unless it is one. Every compatibility statement, device limit and requirement carries three fields: \"confidence\" — \"authoritative\" (confirmed by Sure Petcare for this pair, or read straight from published product data), \"unverified\" (derived from published material or inferred, and not confirmed for this pair), or \"unknown\" (nothing is recorded either way); \"basis\", naming what the answer rests on; and \"requires_confirmation\", a boolean. When it is true, a \"confirmation_note\" is included in wording that can be repeated to a person as-is. Do not present an unverified statement as a Sure Petcare assurance, and do not silently drop the caveat: on a compatibility answer these fields sit at the top level, next to the verdict, precisely so that a caller reading only the verdict still sees them. \"unknown\" is not \"no\" — see the \"unknown\" convention.",
            "confidence_levels": {
                "authoritative": "Sure Petcare says so: either published on the website or confirmed by the product owner for this pair. Safe to state as fact.",
                "unverified": "Derived from published material, support documentation or a product manual, and not confirmed for this pair. State it as likely, say where it came from, and pass on the confirmation_note.",
                "unknown": "No statement exists. Not a no. Do not answer either way; say it is not published and point the customer at Sure Petcare support."
            },
            "sources": {
                "compatibility_dataset": "An explicit statement about this pair, recorded for this API. Carries its own confidence: see basis.",
                "hub_pairing_data": "The Connect pairing data behind the \"works with the Hub\" badge on the website.",
                "accessory_listing": "One product is published as an accessory of the other on the website.",
                "published_specifications": "The specification table printed on the product page.",
                "request": "True by the shape of the question, e.g. a product compared with itself.",
                "no_statement_recorded": "Nothing was found. The verdict is \"unknown\"."
            },
            "scope": "This proof of concept serves a fixed subset of products. See the \"scope\" block on list and search responses. It is not the whole Sure Petcare range and a 404 from here is not evidence a product does not exist.",
            "request_ids": "Every response carries an X-Request-ID header. Quote it when reporting a wrong or surprising answer; it identifies the exact request in our logs. If you send your own X-Request-ID it is echoed back."
        },
        "source_of_truth": {
            "statement": "Every value is read at request time from the same live product and domain data that the surepetcare.com pages read. There is no export, no sync job and no separate copy of product data behind this API, so a stored value such as a name, a specification or a price cannot drift apart from the website.",
            "derived_values": "Stored values are shared; two are derived. A price with tax applied, and the in_stock decision, are worked out here by logic ported from the website's own rather than by calling it. Both are checked against the pages, including at the point where the answer flips, and both agree — but they are a second implementation of a rule that can change, so they are the two values that could in principle drift from a page even though the underlying data is identical. Stated rather than glossed over: if you ever see this API and a product page disagree, that is a bug worth reporting, and the X-Request-ID identifies the request.",
            "additional_data": "One kind of information here is published nowhere else on the site: the pairwise compatibility statements and device limits behind check_compatibility. Each one reports where it came from, in \"source\", and how far it can be trusted, in \"confidence\" — see the \"confidence\" convention. It carries no names, prices, specifications or availability of its own; those always come from the product data above."
        },
        "errors": {
            "invalid_parameter": "400. A parameter was present but unusable.",
            "missing_parameter": "400. A required parameter was absent.",
            "unknown_store": "400. The store is not one this POC serves.",
            "unknown_product": "404. No such product on this website.",
            "out_of_scope": "404. The product exists, but this POC does not serve it. The website has it; the in-scope list is returned with the error.",
            "not_available_in_store": "404. The product exists and is in scope, but is not published in the requested store.",
            "unknown_endpoint": "404. No such endpoint. The capability list above is complete.",
            "method_not_allowed": "405. This API is read-only; every capability is a GET.",
            "rate_limited": "429. See limits below."
        },
        "limits": {
            "requests_per_minute": 60,
            "cache_seconds": 300,
            "on_exceeded": "HTTP 429 with error.code \"rate_limited\" and error.retry_after_seconds. The Retry-After and X-RateLimit-* headers carry the same information.",
            "note": "Successful responses may be cached for the window above, so repeating an identical request inside it is unnecessary. This API is exempt from the crawler rate limits in robots.txt; it is the cheaper path for both of us."
        }
    }
}