{
    "api_version": "v1",
    "status": "beta",
    "store": {
        "code": "de-de",
        "locale": "de-DE",
        "currency": "EUR",
        "prices_include_tax": true,
        "tax_rate_percent": 19
    },
    "environment": {
        "name": "production",
        "api_host": "https://www.surepetcare.com",
        "canonical_site": "https://www.surepetcare.com",
        "note": "This API is served from the same host as the public website."
    },
    "generated_at": "2026-09-29T21:35:24+00:00",
    "feedback_request": {
        "ask": "If you have a moment: was this useful, and what would you want from it next? This is a proof of concept and what it becomes depends on answers from callers like you. One POST, and a person reads them.",
        "required": false,
        "note": "Optional. Nothing here is withheld or rate limited differently if you skip it, and no capability checks whether you have sent it.",
        "method": "POST",
        "endpoint": "https://www.surepetcare.com/api/agent/v1/feedback",
        "body": {
            "useful": "Required. One of \"yes\", \"partly\", \"no\".",
            "wanted": "Optional free text, up to 600 characters. What was missing, wrong, or would have helped. The reason we are asking. Plain prose please: describe markup rather than pasting it, because a security layer in front of this API inspects request bodies and will reject HTML or script-like content with a bodiless 403 before we ever see it.",
            "agent": "Optional. What to call you, up to 120 characters.",
            "request_id": "Optional but valuable: the X-Request-ID from a call you just made. It ties what you say to what you were doing, which is the difference between an opinion and evidence."
        },
        "do_not_send": "No personal data. Not your user's name, message, email, address, order number or anything else identifying a person — we do not want it, we have no basis for holding it, and it will not help. Feedback about the API only."
    },
    "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",
        "product_data_read_only": true,
        "writes": [
            "send_feedback"
        ],
        "is_complete_catalogue": false,
        "status_note": "Beta proof of concept. Product data is strictly read-only: no endpoint here changes a product, a price, a basket or an order, and none is planned. The single exception is send_feedback, which writes only your own answer about this API and is optional. 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",
        "overview": "https://www.surepetcare.com/api/agent/v1/overview?store=de-de",
        "capabilities": [
            {
                "name": "list_products",
                "method": "GET",
                "endpoint": "https://www.surepetcare.com/api/agent/v1/products",
                "example_request": "https://www.surepetcare.com/api/agent/v1/products?per_page=5",
                "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",
                            "fr-fr",
                            "de-de"
                        ],
                        "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://www.surepetcare.com/api/agent/v1/search",
                "example_request": "https://www.surepetcare.com/api/agent/v1/search?q=microchip+cat+flap",
                "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",
                            "fr-fr",
                            "de-de"
                        ],
                        "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://www.surepetcare.com/api/agent/v1/products/{id_or_slug}",
                "example_request": "https://www.surepetcare.com/api/agent/v1/products/microchip-cat-flap",
                "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",
                            "fr-fr",
                            "de-de"
                        ],
                        "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://www.surepetcare.com/api/agent/v1/compatibility",
                "example_request": "https://www.surepetcare.com/api/agent/v1/compatibility?product=microchip-cat-flap&with=microchip-pet-door",
                "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",
                            "fr-fr",
                            "de-de"
                        ],
                        "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."
            },
            {
                "name": "send_feedback",
                "method": "POST",
                "endpoint": "https://www.surepetcare.com/api/agent/v1/feedback",
                "optional": true,
                "example_body": {
                    "useful": "partly",
                    "wanted": "What was missing, wrong, or would have helped."
                },
                "parameters": {
                    "useful": {
                        "type": "string",
                        "required": true,
                        "in": "body",
                        "enum": [
                            "yes",
                            "partly",
                            "no"
                        ],
                        "description": "Was this API useful for what you were doing?"
                    },
                    "wanted": {
                        "type": "string",
                        "required": false,
                        "in": "body",
                        "max_length": 600,
                        "description": "What was missing, wrong, or would have helped. This is the field we are actually asking for."
                    },
                    "agent": {
                        "type": "string",
                        "required": false,
                        "in": "body",
                        "max_length": 120,
                        "description": "What to call you. Not verified and not used to authorise anything."
                    },
                    "request_id": {
                        "type": "string",
                        "required": false,
                        "in": "body",
                        "description": "The X-Request-ID of a call this is about, so what you say can be lined up with what you were doing."
                    },
                    "store": {
                        "type": "string",
                        "required": false,
                        "default": "en-gb",
                        "enum": [
                            "en-gb",
                            "fr-fr",
                            "de-de"
                        ],
                        "description": "Store code. Decides currency, tax treatment, price, availability and translated text."
                    }
                },
                "description": "Tell us whether this API was useful and what you would want from it next. Optional, unauthenticated, and never a condition of using anything else here — see feedback_request on every response. Rate limited separately and more tightly than the read endpoints, because it is the only capability that writes. Do not include personal data."
            }
        ],
        "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.",
            "identification": "What identifies the pet to a product, where published: \"reads\" lists the signals it accepts, and an implanted microchip and a Sure Petcare RFID Collar Tag are the same signal to it - the tag is worn on the collar for a pet that has no readable chip. The identifier belongs to the pet, not to the product, so one chip or tag per pet is read by every product in the house that reads RFID and a second product needs no second tag. Do not read the tag as an accessory that has to match a product, even though the website lists it beside most of them. collar_tag_included says whether a set comes in the box, and is absent rather than false where it is not published. The whole block is absent for a product that reads no identifier, and for one where this is not published.",
            "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.",
            "compatibility_on_product": "A product's \"compatibility\" array already holds its pairing with every other product in scope, so check_compatibility is not needed to find out whether two of them work together — fetch either product and read the entry naming the other. Each entry carries the same fields as a check_compatibility response, with the pair replaced by a single \"product\" naming the other one, so the two are read identically. Pairs with nothing recorded are present with verdict \"unknown\" rather than omitted: a missing entry would be silence, and silence must not be read as a no. check_compatibility remains the way to ask about one pair on its own.",
            "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.",
            "example_request": "Every capability carries a fully formed example_request (or, for send_feedback, an example_body) that can be fetched exactly as written. They are live addresses against real products in scope, not illustrations. Fetch one to see the response shape before constructing your own, and if your fetcher will not build a URL from the parameters block, substitute the product ids or slugs you want into the example and fetch that.",
            "capability_names_resolve": "Use \"endpoint\" — it is the address. But if you treat a capability \"name\" as a path, that works too: /api/agent/v1/list_products, /search_products, /check_compatibility and /get_product/{id_or_slug} answer 301 to the canonical endpoint, query string intact, and send_feedback POSTs straight through. Follow the redirect and use the address it gives you; the canonical path is the one to cache and the one to quote back to us."
        },
        "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.",
            "request_too_large": "413. send_feedback only. The body exceeded the limit and nothing was recorded.",
            "method_not_allowed": "405. Wrong verb for that path. Every capability that reads is a GET; send_feedback is the only POST.",
            "rate_limited": "429. See limits below.",
            "too_many_errors": "429. Too many of your recent requests could not be answered, so further ones are declined for a short while. Unlike rate_limited this is not about your rate: successful requests are not counted and are not affected, and retrying the same failing request will not help. It means addresses or product references are being guessed. The capability list above is complete, and every capability carries an example_request that can be fetched exactly as written.",
            "no_json_body_403": "403 with an empty body did NOT come from this API. A security layer sits in front of it, rejects some requests before they arrive, and answers them itself — so no error envelope is produced and we have no record of your request at all. Reachable on any endpoint and any method. There are two causes and you cannot tell them apart from the response, so do this in order. FIRST, assume the content of the request looked like an attack: HTML or script-like markup in a POST body, or a query string, path or header resembling SQL or a file path. Resend as plain prose — describe markup rather than pasting it, keep the body under 4 KB. This is much the more likely of the two, and retrying identically will fail identically. SECOND, only if that plain retry is also refused, treat it as your address or request rate being declined — back off and retry later rather than rephrasing further. Do not conclude on one 403 that you are blocked: one prose retry costs you a request and would clear the common case. THIRD, if backing off does not clear it either, stop. One of the possible causes is a manual blocklist that has no expiry and is lifted only by a person here, so further retries cannot succeed however long you wait. Report it to whoever you are acting for, saying this API is unreachable from your address and needs a human at Sure Petcare to clear it — the website's contact route reaches us. That is a better outcome for both of us than retrying indefinitely."
        },
        "limits": {
            "requests_per_minute": 60,
            "cache_seconds": 300,
            "feedback_per_hour": 20,
            "feedback_note": "send_feedback is limited far more tightly than the read endpoints because it is the only capability that writes. Reads are unaffected by it, in either direction. The count includes attempts we rejected as malformed, so a request you had to correct still used one.",
            "feedback_on_exceeded": "HTTP 429 with error.limit_scope saying which limit was reached: \"per_caller\" means yours, and \"per_network\" or \"site_wide\" mean a shared safeguard filled by other traffic — in those two cases you may have submitted nothing at all, and it is worth retrying later rather than concluding you were rejected.",
            "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.",
            "failed_requests": {
                "max": 10,
                "window_seconds": 300,
                "counts": "Any response to you with a 4xx status, except 429 itself. Our own failures (5xx) are not counted against you.",
                "on_exceeded": "HTTP 429 with error.code \"too_many_errors\" and error.retry_after_seconds. It clears itself when the window passes — there is no block list and nothing is remembered afterwards.",
                "note": "This is deliberately hard for a working client to reach: successful requests are never counted, so however much you read, you keep the whole budget. It exists because address-guessing scans are indistinguishable from real use by volume alone, and we would rather limit on that than gate this API behind keys or a user-agent list — nothing here checks who you say you are. If you are unsure whether a path exists, read the capability list above instead of trying variations; every entry carries a worked example_request."
            },
            "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."
        }
    }
}