{"openapi":"3.1.0","info":{"title":"Parts API","version":"1.0.0","description":"Models, aliases, fitments, virtual parts and stock alerts for the fitment catalogue, plus a two-direction /search and a type-ahead /suggest over the same engine the public finder pages use. `products` and `variants` are read-only mirrors of the connected Shopify store, kept current by the shopify-inventory pipeline \u2014 writes are not exposed because the next sync would silently overwrite them. Every catalogue resource may carry a `shop_eid` (t1 shop tenancy) \u2014 see the `shop` query parameter and each write schema's `shop_eid` field for how a request gets scoped to one shop."},"servers":[{"url":"https://partsdna-74a225.tiknix.com/partsapi"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Two credential shapes are accepted here, tried in this order: (1) an API key token (`tk_\u2026`, minted at /apikeys) \u2014 member-issued, scoped by its declared scopes/allowedServers/allowedOrigins, and, for a shop-provisioned key, permanently bound to that one shop. (2) a Shopify App Bridge session token \u2014 an HS256 JWT \u2014 the credential the embedded admin UI sends on every /partsapi call made from inside the storefront iframe, where third-party cookies never arrive. A verified session token resolves to the shop's own member row and is scoped to that shop exactly like a shop-bound API key: every resource's read, and the four writable catalogue resources' write only when `[shopify_embed]` grants that shop `admin` access."},"apiKeyHeader":{"type":"apiKey","in":"header","name":"X-Api-Key","description":"Alternative to `Authorization: Bearer` for an API key token only \u2014 a session token is never accepted here, only via Bearer."}},"schemas":{"PartModel":{"type":"object","properties":{"id":{"type":"integer"},"model_number":{"type":"string"},"model_norm":{"type":"string"},"title":{"type":"string"},"brand":{"type":"string"},"status":{"type":"string"},"product_ref":{"type":"integer"},"superseded_by_ref":{"type":"integer"},"part_count":{"type":"integer"},"published_at":{"type":"string"},"external_eid":{"type":"string"},"notes":{"type":"string"},"keywords":{"type":"string"},"keywords_norm":{"type":"string"},"shop_eid":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["model_number","title"]},"PartModelCreate":{"type":"object","properties":{"model_number":{"type":"string"},"title":{"type":"string"},"brand":{"type":"string"},"status":{"type":"string"},"product_ref":{"type":"integer"},"superseded_by_ref":{"type":"integer"},"notes":{"type":"string"},"keywords":{"type":"string"},"shop_eid":{"type":"string","description":"Assigns this row to a shop, e.g. acme-tools.myshopify.com. Only takes effect for an UNSCOPED credential (an unscoped API key with no shop of its own) \u2014 a shop-bound API key or a verified Shopify session token always writes its own shop and this field is ignored if sent. Must normalize to a <subdomain>.myshopify.com domain or the request answers 422. Omit to leave the row unscoped (visible only to unscoped/whole-catalogue reads)."}},"required":["model_number","title"]},"PartModelUpdate":{"type":"object","properties":{"model_number":{"type":"string"},"title":{"type":"string"},"brand":{"type":"string"},"status":{"type":"string"},"product_ref":{"type":"integer"},"superseded_by_ref":{"type":"integer"},"notes":{"type":"string"},"keywords":{"type":"string"},"shop_eid":{"type":"string","description":"Assigns this row to a shop, e.g. acme-tools.myshopify.com. Only takes effect for an UNSCOPED credential (an unscoped API key with no shop of its own) \u2014 a shop-bound API key or a verified Shopify session token always writes its own shop and this field is ignored if sent. Must normalize to a <subdomain>.myshopify.com domain or the request answers 422. Omit to leave the row unscoped (visible only to unscoped/whole-catalogue reads)."}}},"PartAlias":{"type":"object","properties":{"id":{"type":"integer"},"model_ref":{"type":"integer"},"part_ref":{"type":"integer"},"virtualpart_ref":{"type":"integer"},"alias":{"type":"string"},"alias_norm":{"type":"string"},"kind":{"type":"string"},"created_at":{"type":"string"}},"required":["alias"]},"PartAliasCreate":{"type":"object","properties":{"model_ref":{"type":"integer"},"part_ref":{"type":"integer"},"virtualpart_ref":{"type":"integer"},"alias":{"type":"string"},"kind":{"type":"string"}},"required":["alias"]},"PartAliasUpdate":{"type":"object","properties":{"model_ref":{"type":"integer"},"part_ref":{"type":"integer"},"virtualpart_ref":{"type":"integer"},"alias":{"type":"string"},"kind":{"type":"string"}}},"Fitment":{"type":"object","properties":{"id":{"type":"integer"},"partmodel_id":{"type":"integer"},"part_ref":{"type":"integer"},"virtualpart_ref":{"type":"integer"},"variant_ref":{"type":"integer"},"kind":{"type":"string"},"position":{"type":"integer"},"qty":{"type":"integer"},"note":{"type":"string"},"created_at":{"type":"string"}},"required":["partmodel_id","kind"]},"FitmentCreate":{"type":"object","properties":{"partmodel_id":{"type":"integer"},"part_ref":{"type":"integer"},"virtualpart_ref":{"type":"integer"},"variant_ref":{"type":"integer"},"kind":{"type":"string"},"position":{"type":"integer"},"qty":{"type":"integer"},"note":{"type":"string"}},"required":["partmodel_id","kind"]},"FitmentUpdate":{"type":"object","properties":{"partmodel_id":{"type":"integer"},"part_ref":{"type":"integer"},"virtualpart_ref":{"type":"integer"},"variant_ref":{"type":"integer"},"kind":{"type":"string"},"position":{"type":"integer"},"qty":{"type":"integer"},"note":{"type":"string"}}},"VirtualPart":{"type":"object","properties":{"id":{"type":"integer"},"name":{"type":"string"},"part_number":{"type":"string"},"part_number_norm":{"type":"string"},"brand":{"type":"string"},"description":{"type":"string"},"image_url":{"type":"string"},"price_estimate":{"type":"number"},"status":{"type":"string"},"notes":{"type":"string"},"keywords":{"type":"string"},"keywords_norm":{"type":"string"},"shop_eid":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["name","part_number"]},"VirtualPartCreate":{"type":"object","properties":{"name":{"type":"string"},"part_number":{"type":"string"},"brand":{"type":"string"},"description":{"type":"string"},"image_url":{"type":"string"},"price_estimate":{"type":"number"},"status":{"type":"string"},"notes":{"type":"string"},"keywords":{"type":"string"},"shop_eid":{"type":"string","description":"Assigns this row to a shop, e.g. acme-tools.myshopify.com. Only takes effect for an UNSCOPED credential (an unscoped API key with no shop of its own) \u2014 a shop-bound API key or a verified Shopify session token always writes its own shop and this field is ignored if sent. Must normalize to a <subdomain>.myshopify.com domain or the request answers 422. Omit to leave the row unscoped (visible only to unscoped/whole-catalogue reads)."}},"required":["name","part_number"]},"VirtualPartUpdate":{"type":"object","properties":{"name":{"type":"string"},"part_number":{"type":"string"},"brand":{"type":"string"},"description":{"type":"string"},"image_url":{"type":"string"},"price_estimate":{"type":"number"},"status":{"type":"string"},"notes":{"type":"string"},"keywords":{"type":"string"},"shop_eid":{"type":"string","description":"Assigns this row to a shop, e.g. acme-tools.myshopify.com. Only takes effect for an UNSCOPED credential (an unscoped API key with no shop of its own) \u2014 a shop-bound API key or a verified Shopify session token always writes its own shop and this field is ignored if sent. Must normalize to a <subdomain>.myshopify.com domain or the request answers 422. Omit to leave the row unscoped (visible only to unscoped/whole-catalogue reads)."}}},"PartAlert":{"type":"object","properties":{"id":{"type":"integer"},"email":{"type":"string"},"part_ref":{"type":"integer"},"virtualpart_ref":{"type":"integer"},"partmodel_ref":{"type":"integer"},"query_text":{"type":"string"},"status":{"type":"string"},"ip_address":{"type":"string"},"shop_eid":{"type":"string"},"created_at":{"type":"string"},"notified_at":{"type":"string"},"cancelled_at":{"type":"string"}},"required":[]},"PartAlertCreate":{"type":"object","properties":{"status":{"type":"string"}},"required":[]},"PartAlertUpdate":{"type":"object","properties":{"status":{"type":"string"}}},"ShopifyProduct":{"type":"object","properties":{"id":{"type":"integer"},"product_eid":{"type":"string"},"title":{"type":"string"},"handle":{"type":"string"},"status":{"type":"string"},"vendor":{"type":"string"},"product_type":{"type":"string"},"total_inventory":{"type":"integer"},"image_url":{"type":"string"},"sku_norm":{"type":"string"},"is_part":{"type":"boolean"},"shop_eid":{"type":"string"},"updated_at_shop":{"type":"string"},"keywords":{"type":"string"},"keywords_norm":{"type":"string"}},"required":[]},"ShopifyProductCreate":{"type":"object","properties":{"keywords":{"type":"string"}},"required":[]},"ShopifyProductUpdate":{"type":"object","properties":{"keywords":{"type":"string"}}},"ShopifyVariant":{"type":"object","properties":{"id":{"type":"integer"},"product_eid":{"type":"string"},"product_title":{"type":"string"},"variant_eid":{"type":"string"},"variant_title":{"type":"string"},"sku":{"type":"string"},"price":{"type":"number"},"stock":{"type":"integer"},"shop_eid":{"type":"string"}},"required":[]},"SearchSynonym":{"type":"object","properties":{"id":{"type":"integer"},"term":{"type":"string"},"term_norm":{"type":"string"},"canonical":{"type":"string"},"canonical_norm":{"type":"string"},"kind":{"type":"string"},"weight":{"type":"integer"},"is_active":{"type":"boolean"},"shop_eid":{"type":"string"},"notes":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["term","canonical"]},"SearchSynonymCreate":{"type":"object","properties":{"term":{"type":"string"},"canonical":{"type":"string"},"kind":{"type":"string"},"weight":{"type":"integer"},"is_active":{"type":"boolean"},"notes":{"type":"string"}},"required":["term","canonical"]},"SearchSynonymUpdate":{"type":"object","properties":{"term":{"type":"string"},"canonical":{"type":"string"},"kind":{"type":"string"},"weight":{"type":"integer"},"is_active":{"type":"boolean"},"notes":{"type":"string"}}},"SearchSuggestion":{"type":"object","properties":{"id":{"type":"integer"},"kind":{"type":"string"},"target_type":{"type":"string"},"target_ref":{"type":"integer"},"target_label":{"type":"string"},"term":{"type":"string"},"canonical":{"type":"string"},"source":{"type":"string"},"score":{"type":"number"},"evidence":{"type":"string"},"status":{"type":"string"},"shop_eid":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":[]},"SearchSuggestionCreate":{"type":"object","properties":{"status":{"type":"string"},"term":{"type":"string"},"canonical":{"type":"string"}},"required":[]},"SearchSuggestionUpdate":{"type":"object","properties":{"status":{"type":"string"},"term":{"type":"string"},"canonical":{"type":"string"}}},"SearchRedirect":{"type":"object","properties":{"id":{"type":"integer"},"term":{"type":"string"},"term_norm":{"type":"string"},"url":{"type":"string"},"notes":{"type":"string"},"is_active":{"type":"boolean"},"shop_eid":{"type":"string"},"created_at":{"type":"string"},"updated_at":{"type":"string"}},"required":["term","url"]},"SearchRedirectCreate":{"type":"object","properties":{"term":{"type":"string"},"url":{"type":"string"},"notes":{"type":"string"},"is_active":{"type":"boolean"},"shop_eid":{"type":"string","description":"Assigns this row to a shop, e.g. acme-tools.myshopify.com. Only takes effect for an UNSCOPED credential (an unscoped API key with no shop of its own) \u2014 a shop-bound API key or a verified Shopify session token always writes its own shop and this field is ignored if sent. Must normalize to a <subdomain>.myshopify.com domain or the request answers 422. Omit to leave the row unscoped (visible only to unscoped/whole-catalogue reads)."}},"required":["term","url"]},"SearchRedirectUpdate":{"type":"object","properties":{"term":{"type":"string"},"url":{"type":"string"},"notes":{"type":"string"},"is_active":{"type":"boolean"},"shop_eid":{"type":"string","description":"Assigns this row to a shop, e.g. acme-tools.myshopify.com. Only takes effect for an UNSCOPED credential (an unscoped API key with no shop of its own) \u2014 a shop-bound API key or a verified Shopify session token always writes its own shop and this field is ignored if sent. Must normalize to a <subdomain>.myshopify.com domain or the request answers 422. Omit to leave the row unscoped (visible only to unscoped/whole-catalogue reads)."}}},"ShopifyPage":{"type":"object","properties":{"id":{"type":"integer"},"page_eid":{"type":"string"},"title":{"type":"string"},"handle":{"type":"string"},"body_summary":{"type":"string"},"updated_at_shop":{"type":"string"},"shop_eid":{"type":"string"},"is_excluded":{"type":"boolean"}},"required":[]},"ShopifyPageCreate":{"type":"object","properties":{"is_excluded":{"type":"boolean"}},"required":[]},"ShopifyPageUpdate":{"type":"object","properties":{"is_excluded":{"type":"boolean"}}},"ShopifyCollection":{"type":"object","properties":{"id":{"type":"integer"},"collection_eid":{"type":"string"},"title":{"type":"string"},"handle":{"type":"string"},"description":{"type":"string"},"image_url":{"type":"string"},"updated_at_shop":{"type":"string"},"shop_eid":{"type":"string"},"is_excluded":{"type":"boolean"}},"required":[]},"ShopifyCollectionCreate":{"type":"object","properties":{"is_excluded":{"type":"boolean"}},"required":[]},"ShopifyCollectionUpdate":{"type":"object","properties":{"is_excluded":{"type":"boolean"}}}}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}],"paths":{"/models":{"get":{"summary":"List models","parameters":[{"name":"brand","in":"query","required":false,"schema":{"type":"string"}},{"name":"status","in":"query","required":false,"schema":{"type":"string"}},{"name":"model_number","in":"query","required":false,"schema":{"type":"string"}},{"name":"q","in":"query","required":false,"schema":{"type":"string"},"description":"Fuzzy text match over model_number, title, brand, keywords, OR'd with the fitment graph and model aliases \u2014 a part number surfaces the models it fits."},{"name":"shop","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict this request to one shop's rows, e.g. acme-tools.myshopify.com. Absent = every shop's rows. Ignored (overridden) for a shop-bound API key or a verified Shopify App Bridge session token \u2014 both are always scoped to their own shop regardless of this parameter. Applied on writes too: a scoped write touching another shop's row answers 404, indistinguishable from missing."},{"name":"sort","in":"query","required":false,"schema":{"type":"string","enum":["model_number","title","brand","created_at","updated_at","part_count"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PartModel"}}}}}},"security":[]},"post":{"summary":"Create a model","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartModelCreate"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartModel"}}}},"409":{"description":"Conflict \u2014 the natural key already exists."},"422":{"description":"Validation failed."}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]}},"/models/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"shop","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict this request to one shop's rows, e.g. acme-tools.myshopify.com. Absent = every shop's rows. Ignored (overridden) for a shop-bound API key or a verified Shopify App Bridge session token \u2014 both are always scoped to their own shop regardless of this parameter. Applied on writes too: a scoped write touching another shop's row answers 404, indistinguishable from missing."}],"get":{"summary":"Get one model","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartModel"}}}},"404":{"description":"Not found"}},"security":[]},"patch":{"summary":"Update a model","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartModelUpdate"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartModel"}}}},"404":{"description":"Not found"},"409":{"description":"Conflict \u2014 the natural key already exists."},"422":{"description":"Validation failed."}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]},"delete":{"summary":"Delete a model","responses":{"204":{"description":"Deleted"},"404":{"description":"Not found"}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]}},"/aliases":{"get":{"summary":"List aliass","parameters":[{"name":"model_ref","in":"query","required":false,"schema":{"type":"integer"}},{"name":"part_ref","in":"query","required":false,"schema":{"type":"integer"}},{"name":"virtualpart_ref","in":"query","required":false,"schema":{"type":"integer"}},{"name":"kind","in":"query","required":false,"schema":{"type":"string"}},{"name":"shop","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict this request to one shop's rows, e.g. acme-tools.myshopify.com. Absent = every shop's rows. Ignored (overridden) for a shop-bound API key or a verified Shopify App Bridge session token \u2014 both are always scoped to their own shop regardless of this parameter. Applied on writes too: a scoped write touching another shop's row answers 404, indistinguishable from missing."},{"name":"sort","in":"query","required":false,"schema":{"type":"string","enum":["alias","created_at"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PartAlias"}}}}}},"security":[]},"post":{"summary":"Create a alias","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartAliasCreate"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartAlias"}}}},"409":{"description":"Conflict \u2014 the natural key already exists."},"422":{"description":"Validation failed."}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]}},"/aliases/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"shop","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict this request to one shop's rows, e.g. acme-tools.myshopify.com. Absent = every shop's rows. Ignored (overridden) for a shop-bound API key or a verified Shopify App Bridge session token \u2014 both are always scoped to their own shop regardless of this parameter. Applied on writes too: a scoped write touching another shop's row answers 404, indistinguishable from missing."}],"get":{"summary":"Get one alias","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartAlias"}}}},"404":{"description":"Not found"}},"security":[]},"patch":{"summary":"Update a alias","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartAliasUpdate"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartAlias"}}}},"404":{"description":"Not found"},"409":{"description":"Conflict \u2014 the natural key already exists."},"422":{"description":"Validation failed."}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]},"delete":{"summary":"Delete a alias","responses":{"204":{"description":"Deleted"},"404":{"description":"Not found"}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]}},"/fitments":{"get":{"summary":"List fitments","parameters":[{"name":"partmodel_id","in":"query","required":false,"schema":{"type":"integer"}},{"name":"part_ref","in":"query","required":false,"schema":{"type":"integer"}},{"name":"virtualpart_ref","in":"query","required":false,"schema":{"type":"integer"}},{"name":"kind","in":"query","required":false,"schema":{"type":"string"}},{"name":"shop","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict this request to one shop's rows, e.g. acme-tools.myshopify.com. Absent = every shop's rows. Ignored (overridden) for a shop-bound API key or a verified Shopify App Bridge session token \u2014 both are always scoped to their own shop regardless of this parameter. Applied on writes too: a scoped write touching another shop's row answers 404, indistinguishable from missing."},{"name":"sort","in":"query","required":false,"schema":{"type":"string","enum":["position","kind","created_at"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Fitment"}}}}}},"security":[]},"post":{"summary":"Create a fitment","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FitmentCreate"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Fitment"}}}},"409":{"description":"Conflict \u2014 the natural key already exists."},"422":{"description":"Validation failed."}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]}},"/fitments/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"shop","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict this request to one shop's rows, e.g. acme-tools.myshopify.com. Absent = every shop's rows. Ignored (overridden) for a shop-bound API key or a verified Shopify App Bridge session token \u2014 both are always scoped to their own shop regardless of this parameter. Applied on writes too: a scoped write touching another shop's row answers 404, indistinguishable from missing."}],"get":{"summary":"Get one fitment","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Fitment"}}}},"404":{"description":"Not found"}},"security":[]},"patch":{"summary":"Update a fitment","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FitmentUpdate"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Fitment"}}}},"404":{"description":"Not found"},"409":{"description":"Conflict \u2014 the natural key already exists."},"422":{"description":"Validation failed."}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]},"delete":{"summary":"Delete a fitment","responses":{"204":{"description":"Deleted"},"404":{"description":"Not found"}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]}},"/virtualparts":{"get":{"summary":"List virtual parts","parameters":[{"name":"brand","in":"query","required":false,"schema":{"type":"string"}},{"name":"status","in":"query","required":false,"schema":{"type":"string"}},{"name":"q","in":"query","required":false,"schema":{"type":"string"},"description":"Fuzzy text match over name, part_number, brand, keywords, OR'd with the fitment graph and model aliases \u2014 a part number surfaces the models it fits."},{"name":"shop","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict this request to one shop's rows, e.g. acme-tools.myshopify.com. Absent = every shop's rows. Ignored (overridden) for a shop-bound API key or a verified Shopify App Bridge session token \u2014 both are always scoped to their own shop regardless of this parameter. Applied on writes too: a scoped write touching another shop's row answers 404, indistinguishable from missing."},{"name":"sort","in":"query","required":false,"schema":{"type":"string","enum":["name","part_number","created_at","updated_at"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/VirtualPart"}}}}}},"security":[]},"post":{"summary":"Create a virtual part","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VirtualPartCreate"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VirtualPart"}}}},"409":{"description":"Conflict \u2014 the natural key already exists."},"422":{"description":"Validation failed."}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]}},"/virtualparts/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"shop","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict this request to one shop's rows, e.g. acme-tools.myshopify.com. Absent = every shop's rows. Ignored (overridden) for a shop-bound API key or a verified Shopify App Bridge session token \u2014 both are always scoped to their own shop regardless of this parameter. Applied on writes too: a scoped write touching another shop's row answers 404, indistinguishable from missing."}],"get":{"summary":"Get one virtual part","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VirtualPart"}}}},"404":{"description":"Not found"}},"security":[]},"patch":{"summary":"Update a virtual part","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VirtualPartUpdate"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VirtualPart"}}}},"404":{"description":"Not found"},"409":{"description":"Conflict \u2014 the natural key already exists."},"422":{"description":"Validation failed."}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]},"delete":{"summary":"Delete a virtual part","responses":{"204":{"description":"Deleted"},"404":{"description":"Not found"}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]}},"/alerts":{"get":{"summary":"List alerts","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string"}},{"name":"part_ref","in":"query","required":false,"schema":{"type":"integer"}},{"name":"virtualpart_ref","in":"query","required":false,"schema":{"type":"integer"}},{"name":"partmodel_ref","in":"query","required":false,"schema":{"type":"integer"}},{"name":"shop","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict this request to one shop's rows, e.g. acme-tools.myshopify.com. Absent = every shop's rows. Ignored (overridden) for a shop-bound API key or a verified Shopify App Bridge session token \u2014 both are always scoped to their own shop regardless of this parameter. Applied on writes too: a scoped write touching another shop's row answers 404, indistinguishable from missing."},{"name":"sort","in":"query","required":false,"schema":{"type":"string","enum":["created_at","status"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PartAlert"}}}}}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]}},"/alerts/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"shop","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict this request to one shop's rows, e.g. acme-tools.myshopify.com. Absent = every shop's rows. Ignored (overridden) for a shop-bound API key or a verified Shopify App Bridge session token \u2014 both are always scoped to their own shop regardless of this parameter. Applied on writes too: a scoped write touching another shop's row answers 404, indistinguishable from missing."}],"get":{"summary":"Get one alert","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartAlert"}}}},"404":{"description":"Not found"}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]},"patch":{"summary":"Update a alert","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartAlertUpdate"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PartAlert"}}}},"404":{"description":"Not found"},"409":{"description":"Conflict \u2014 the natural key already exists."},"422":{"description":"Validation failed."}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]},"delete":{"summary":"Delete a alert","responses":{"204":{"description":"Deleted"},"404":{"description":"Not found"}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]}},"/products":{"get":{"summary":"List products","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string"}},{"name":"vendor","in":"query","required":false,"schema":{"type":"string"}},{"name":"product_type","in":"query","required":false,"schema":{"type":"string"}},{"name":"is_part","in":"query","required":false,"schema":{"type":"boolean"}},{"name":"shop","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict this request to one shop's rows, e.g. acme-tools.myshopify.com. Absent = every shop's rows. Ignored (overridden) for a shop-bound API key or a verified Shopify App Bridge session token \u2014 both are always scoped to their own shop regardless of this parameter. Applied on writes too: a scoped write touching another shop's row answers 404, indistinguishable from missing."},{"name":"sort","in":"query","required":false,"schema":{"type":"string","enum":["title","total_inventory","updated_at_shop"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ShopifyProduct"}}}}}},"security":[]}},"/products/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"shop","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict this request to one shop's rows, e.g. acme-tools.myshopify.com. Absent = every shop's rows. Ignored (overridden) for a shop-bound API key or a verified Shopify App Bridge session token \u2014 both are always scoped to their own shop regardless of this parameter. Applied on writes too: a scoped write touching another shop's row answers 404, indistinguishable from missing."}],"get":{"summary":"Get one product","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShopifyProduct"}}}},"404":{"description":"Not found"}},"security":[]},"patch":{"summary":"Update a product","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShopifyProductUpdate"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShopifyProduct"}}}},"404":{"description":"Not found"},"409":{"description":"Conflict \u2014 the natural key already exists."},"422":{"description":"Validation failed."}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]}},"/variants":{"get":{"summary":"List variants","parameters":[{"name":"product_eid","in":"query","required":false,"schema":{"type":"string"}},{"name":"sku","in":"query","required":false,"schema":{"type":"string"}},{"name":"shop","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict this request to one shop's rows, e.g. acme-tools.myshopify.com. Absent = every shop's rows. Ignored (overridden) for a shop-bound API key or a verified Shopify App Bridge session token \u2014 both are always scoped to their own shop regardless of this parameter. Applied on writes too: a scoped write touching another shop's row answers 404, indistinguishable from missing."},{"name":"sort","in":"query","required":false,"schema":{"type":"string","enum":["sku","price","stock"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ShopifyVariant"}}}}}},"security":[]}},"/variants/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"shop","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict this request to one shop's rows, e.g. acme-tools.myshopify.com. Absent = every shop's rows. Ignored (overridden) for a shop-bound API key or a verified Shopify App Bridge session token \u2014 both are always scoped to their own shop regardless of this parameter. Applied on writes too: a scoped write touching another shop's row answers 404, indistinguishable from missing."}],"get":{"summary":"Get one variant","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShopifyVariant"}}}},"404":{"description":"Not found"}},"security":[]}},"/synonyms":{"get":{"summary":"List synonyms","parameters":[{"name":"kind","in":"query","required":false,"schema":{"type":"string"}},{"name":"is_active","in":"query","required":false,"schema":{"type":"boolean"}},{"name":"shop","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict this request to one shop's rows, e.g. acme-tools.myshopify.com. Absent = every shop's rows. Ignored (overridden) for a shop-bound API key or a verified Shopify App Bridge session token \u2014 both are always scoped to their own shop regardless of this parameter. Applied on writes too: a scoped write touching another shop's row answers 404, indistinguishable from missing."},{"name":"sort","in":"query","required":false,"schema":{"type":"string","enum":["term","created_at"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SearchSynonym"}}}}}},"security":[]},"post":{"summary":"Create a synonym","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchSynonymCreate"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchSynonym"}}}},"409":{"description":"Conflict \u2014 the natural key already exists."},"422":{"description":"Validation failed."}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]}},"/synonyms/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"shop","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict this request to one shop's rows, e.g. acme-tools.myshopify.com. Absent = every shop's rows. Ignored (overridden) for a shop-bound API key or a verified Shopify App Bridge session token \u2014 both are always scoped to their own shop regardless of this parameter. Applied on writes too: a scoped write touching another shop's row answers 404, indistinguishable from missing."}],"get":{"summary":"Get one synonym","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchSynonym"}}}},"404":{"description":"Not found"}},"security":[]},"patch":{"summary":"Update a synonym","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchSynonymUpdate"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchSynonym"}}}},"404":{"description":"Not found"},"409":{"description":"Conflict \u2014 the natural key already exists."},"422":{"description":"Validation failed."}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]},"delete":{"summary":"Delete a synonym","responses":{"204":{"description":"Deleted"},"404":{"description":"Not found"}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]}},"/suggestions":{"get":{"summary":"List suggestions","parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string"}},{"name":"kind","in":"query","required":false,"schema":{"type":"string"}},{"name":"target_type","in":"query","required":false,"schema":{"type":"string"}},{"name":"shop","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict this request to one shop's rows, e.g. acme-tools.myshopify.com. Absent = every shop's rows. Ignored (overridden) for a shop-bound API key or a verified Shopify App Bridge session token \u2014 both are always scoped to their own shop regardless of this parameter. Applied on writes too: a scoped write touching another shop's row answers 404, indistinguishable from missing."},{"name":"sort","in":"query","required":false,"schema":{"type":"string","enum":["score","created_at"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SearchSuggestion"}}}}}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]}},"/suggestions/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"shop","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict this request to one shop's rows, e.g. acme-tools.myshopify.com. Absent = every shop's rows. Ignored (overridden) for a shop-bound API key or a verified Shopify App Bridge session token \u2014 both are always scoped to their own shop regardless of this parameter. Applied on writes too: a scoped write touching another shop's row answers 404, indistinguishable from missing."}],"get":{"summary":"Get one suggestion","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchSuggestion"}}}},"404":{"description":"Not found"}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]},"patch":{"summary":"Update a suggestion","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchSuggestionUpdate"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchSuggestion"}}}},"404":{"description":"Not found"},"409":{"description":"Conflict \u2014 the natural key already exists."},"422":{"description":"Validation failed."}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]},"delete":{"summary":"Delete a suggestion","responses":{"204":{"description":"Deleted"},"404":{"description":"Not found"}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]}},"/redirects":{"get":{"summary":"List redirects","parameters":[{"name":"is_active","in":"query","required":false,"schema":{"type":"boolean"}},{"name":"shop","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict this request to one shop's rows, e.g. acme-tools.myshopify.com. Absent = every shop's rows. Ignored (overridden) for a shop-bound API key or a verified Shopify App Bridge session token \u2014 both are always scoped to their own shop regardless of this parameter. Applied on writes too: a scoped write touching another shop's row answers 404, indistinguishable from missing."},{"name":"sort","in":"query","required":false,"schema":{"type":"string","enum":["term","created_at"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SearchRedirect"}}}}}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]},"post":{"summary":"Create a redirect","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchRedirectCreate"}}}},"responses":{"201":{"description":"Created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchRedirect"}}}},"409":{"description":"Conflict \u2014 the natural key already exists."},"422":{"description":"Validation failed."}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]}},"/redirects/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"shop","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict this request to one shop's rows, e.g. acme-tools.myshopify.com. Absent = every shop's rows. Ignored (overridden) for a shop-bound API key or a verified Shopify App Bridge session token \u2014 both are always scoped to their own shop regardless of this parameter. Applied on writes too: a scoped write touching another shop's row answers 404, indistinguishable from missing."}],"get":{"summary":"Get one redirect","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchRedirect"}}}},"404":{"description":"Not found"}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]},"patch":{"summary":"Update a redirect","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchRedirectUpdate"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchRedirect"}}}},"404":{"description":"Not found"},"409":{"description":"Conflict \u2014 the natural key already exists."},"422":{"description":"Validation failed."}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]},"delete":{"summary":"Delete a redirect","responses":{"204":{"description":"Deleted"},"404":{"description":"Not found"}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]}},"/pages":{"get":{"summary":"List pages","parameters":[{"name":"is_excluded","in":"query","required":false,"schema":{"type":"boolean"}},{"name":"shop","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict this request to one shop's rows, e.g. acme-tools.myshopify.com. Absent = every shop's rows. Ignored (overridden) for a shop-bound API key or a verified Shopify App Bridge session token \u2014 both are always scoped to their own shop regardless of this parameter. Applied on writes too: a scoped write touching another shop's row answers 404, indistinguishable from missing."},{"name":"sort","in":"query","required":false,"schema":{"type":"string","enum":["title","updated_at_shop"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ShopifyPage"}}}}}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]}},"/pages/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"shop","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict this request to one shop's rows, e.g. acme-tools.myshopify.com. Absent = every shop's rows. Ignored (overridden) for a shop-bound API key or a verified Shopify App Bridge session token \u2014 both are always scoped to their own shop regardless of this parameter. Applied on writes too: a scoped write touching another shop's row answers 404, indistinguishable from missing."}],"get":{"summary":"Get one page","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShopifyPage"}}}},"404":{"description":"Not found"}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]},"patch":{"summary":"Update a page","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShopifyPageUpdate"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShopifyPage"}}}},"404":{"description":"Not found"},"409":{"description":"Conflict \u2014 the natural key already exists."},"422":{"description":"Validation failed."}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]}},"/collections":{"get":{"summary":"List collections","parameters":[{"name":"is_excluded","in":"query","required":false,"schema":{"type":"boolean"}},{"name":"shop","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict this request to one shop's rows, e.g. acme-tools.myshopify.com. Absent = every shop's rows. Ignored (overridden) for a shop-bound API key or a verified Shopify App Bridge session token \u2014 both are always scoped to their own shop regardless of this parameter. Applied on writes too: a scoped write touching another shop's row answers 404, indistinguishable from missing."},{"name":"sort","in":"query","required":false,"schema":{"type":"string","enum":["title","updated_at_shop"]}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ShopifyCollection"}}}}}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]}},"/collections/{id}":{"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer"}},{"name":"shop","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict this request to one shop's rows, e.g. acme-tools.myshopify.com. Absent = every shop's rows. Ignored (overridden) for a shop-bound API key or a verified Shopify App Bridge session token \u2014 both are always scoped to their own shop regardless of this parameter. Applied on writes too: a scoped write touching another shop's row answers 404, indistinguishable from missing."}],"get":{"summary":"Get one collection","responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShopifyCollection"}}}},"404":{"description":"Not found"}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]},"patch":{"summary":"Update a collection","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShopifyCollectionUpdate"}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ShopifyCollection"}}}},"404":{"description":"Not found"},"409":{"description":"Conflict \u2014 the natural key already exists."},"422":{"description":"Validation failed."}},"security":[{"bearerAuth":[]},{"apiKeyHeader":[]}]}},"/search":{"get":{"summary":"Search the catalogue both directions from one query","description":"Tries the query as a machine number (resolve) AND as a part number (exact SKU + reverse fitment + ranked candidates) in one call, mirroring the /parts/find page. `outcome` summarises both directions: exact, alias, superseded (machine hit \u2014 `result` carries the detail), part (SKU hit \u2014 `matched_part`/`fits_models`), suggestions (only candidates), or none.","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string"},"description":"A machine number, a part number, or free text."},{"name":"brand","in":"query","required":false,"schema":{"type":"string"},"description":"Disambiguates an alias that names different machines under different brands."},{"name":"shop","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict this request to one shop's rows, e.g. acme-tools.myshopify.com. Absent = every shop's rows. Ignored (overridden) for a shop-bound API key or a verified Shopify App Bridge session token \u2014 both are always scoped to their own shop regardless of this parameter. Applied on writes too: a scoped write touching another shop's row answers 404, indistinguishable from missing."}],"responses":{"200":{"description":"OK \u2014 note a miss is success:true with outcome \"none\", not a 404.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"query":{"type":"string"},"brand":{"type":["string","null"]},"outcome":{"type":"string","enum":["exact","alias","superseded","part","suggestions","none"]},"result":{"oneOf":[{"type":"null"},{"type":"object","description":"The resolve() payload: outcome (exact|alias|superseded|suggestions), model (shapeModel), alias (shapeAlias), matched_via + chain (superseded only), or candidates (suggestions only)."}]},"parts":{"type":"array","items":{"type":"object","description":"shapePart() rows \u2014 the /parts/lookup parts contract. Empty unless result is a confident model hit."}},"matched_part":{"oneOf":[{"type":"null"},{"type":"object","description":"shapeFoundPart() \u2014 the /parts/fits product object."}]},"fits_models":{"type":"array","items":{"type":"object","description":"shapeFitModel() rows \u2014 the /parts/fits models contract."}},"part_hits":{"type":"array","items":{"type":"object","description":"shapeSuggestedPart() rows \u2014 ranked part candidates, up to 12; url is the storefront product page (null for virtual/non-ACTIVE parts)."}}}}}}},"400":{"description":"Missing q, or a shop= that is not a myshopify.com domain."}},"security":[]}},"/suggest":{"get":{"summary":"Type-ahead suggestions for one search box","description":"Up to 8 matching machines, 8 matching parts, and 4 matching Pages/Collections, trimmed for a dropdown, plus up to 6 \"did you mean\" term chips. The models/parts/terms legs are identical in shape to the public /parts/suggest endpoint \u2014 /parts/suggest is deliberately frozen and does NOT get `content`, this endpoint is the only one that does. `content` is unconditional (no kind param exists here to gate it, unlike /find). Under 2 characters of q returns empty arrays (content included) rather than an error.","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string"},"description":"At least 2 characters, or the response is empty by design."},{"name":"brand","in":"query","required":false,"schema":{"type":"string"},"description":"Accepted for parameter parity with /partsapi/search; unused \u2014 q already matches brand text."},{"name":"shop","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict this request to one shop's rows, e.g. acme-tools.myshopify.com. Absent = every shop's rows. Ignored (overridden) for a shop-bound API key or a verified Shopify App Bridge session token \u2014 both are always scoped to their own shop regardless of this parameter. Applied on writes too: a scoped write touching another shop's row answers 404, indistinguishable from missing."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"query":{"type":"string"},"models":{"type":"array","items":{"type":"object","description":"shapeSuggestion() \u2014 id, model_number, title, brand."}},"parts":{"type":"array","items":{"type":"object","description":"shapeSuggestedPart() \u2014 id, source, title, sku, image, purchasable, url (storefront /products/<handle> for an ACTIVE Shopify product, else null)."}},"content":{"type":"array","items":{"type":"object","description":"shapeSuggestedContent() \u2014 content_kind (page|collection), id, title, excerpt, url. id is the same stable local row id documented on /find's items."}},"terms":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string","description":"The chip label, e.g. \"mower blade\"."},"kind":{"type":"string","enum":["phrase","keyword","synonym","model"]},"count":{"type":"integer","description":"Document frequency \u2014 how many candidate rows produced this phrase."}}}}}}}}}},"security":[]}},"/find":{"get":{"summary":"Ranked, faceted, paged search over parts and models","description":"One call over PartFinder::rankedSearch() \u2014 unions parts and models into a single ranked, paged, faceted list. A plural and its singular (e.g. blade/blades) return the same rows and the same recommendations. A zero-result query is success:true, total:0, never a 404.","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string"},"description":"Free text, a part number, or a machine number."},{"name":"category","in":"query","required":false,"schema":{"type":"string"},"description":"Narrows to a shopifyproduct category key; excludes virtual parts."},{"name":"subtype","in":"query","required":false,"schema":{"type":"string"},"description":"Narrows to a shopifyproduct subtype key; excludes virtual parts."},{"name":"fits","in":"query","required":false,"schema":{"type":"string"},"description":"A machine number or alias \u2014 restricts to parts fitted to the resolved model. Fails to resolve \u2192 zero results, named in fits_model, not a 400."},{"name":"source","in":"query","required":false,"schema":{"type":"string","enum":["shopify","virtual"]}},{"name":"in_stock","in":"query","required":false,"schema":{"type":"boolean"},"description":"Shopify parts only \u2014 a virtual part has no stock column and is excluded when set."},{"name":"kind","in":"query","required":false,"schema":{"type":"string","enum":["part","model","all"],"default":"all"},"description":"`part`/`model` also excludes Page/Collection content rows entirely \u2014 content only ever appears when kind=all (the default)."},{"name":"sort","in":"query","required":false,"schema":{"type":"string","enum":["relevance","title","price","stock"],"default":"relevance"}},{"name":"page","in":"query","required":false,"schema":{"type":"integer","default":1}},{"name":"per_page","in":"query","required":false,"schema":{"type":"integer","default":24,"maximum":100},"description":"Governs parts/models ONLY. Content rows are a separate budget \u2014 see content_per_page \u2014 and are never counted against this."},{"name":"content_per_page","in":"query","required":false,"schema":{"type":"integer","default":4,"minimum":0,"maximum":20},"description":"Separate budget for Page/Collection rows \u2014 NOT part of per_page, so raising or lowering it never changes how many parts/models come back. Ignored (no content returned) when kind is part or model, or on page > 1 \u2014 content is not itself paginated. 0 is honored as a real \"no content\" switch; omitting the param is the only way to get the 4 default, every other value (negatives included) clamps into [0, 20]."},{"name":"search_eid","in":"query","required":false,"schema":{"type":"string"},"description":"A search_eid the caller already holds from a moment ago (window.PdnaTracking.getSearchEid()). When it names a `search` event for this exact query on this shop, recorded in the last 30 minutes (SearchAnalytics::reuseSearchEid()), that id is echoed back in `analytics.search_eid` and no new `search` row is written \u2014 avoids one real shopper search (a live type-ahead, then a results page on Enter, or a reload/Back) being counted as several. Silently ignored otherwise; this call records a fresh search_eid exactly as if the param were never sent."},{"name":"shop","in":"query","required":false,"schema":{"type":"string"},"description":"Restrict this request to one shop's rows, e.g. acme-tools.myshopify.com. Absent = every shop's rows. Ignored (overridden) for a shop-bound API key or a verified Shopify App Bridge session token \u2014 both are always scoped to their own shop regardless of this parameter. Applied on writes too: a scoped write touching another shop's row answers 404, indistinguishable from missing."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"query":{"type":"string"},"expanded":{"type":"array","items":{"type":"string"},"description":"expandQuery()'s synonym-expanded tokens."},"stemmed":{"type":"array","items":{"type":"string"},"description":"stemTokens()'s stem-only token additions."},"recommendations":{"type":"array","items":{"type":"object","properties":{"text":{"type":"string","description":"The chip label, e.g. \"mower blade\"."},"kind":{"type":"string","enum":["phrase","keyword","synonym","model"]},"count":{"type":"integer","description":"Document frequency \u2014 how many candidate rows produced this phrase."}}}},"shop":{"type":["string","null"]},"filters":{"type":"object","description":"category, subtype, fits, source, in_stock, kind, sort as applied."},"page":{"type":"integer"},"per_page":{"type":"integer"},"content_per_page":{"type":"integer","description":"The content_per_page actually applied (default 4 when the param was omitted) \u2014 see that parameter's own description."},"total":{"type":"integer","description":"Full match count of parts/models only \u2014 never includes content, which is not paginated."},"pages_total":{"type":"integer","description":"TRUE count of matching Pages, independent of content_per_page \u2014 for a caller building its own tabbed UI (Products | Pages | Collections) that needs a real tab-label count, not just how many content rows this response happened to carry. 0 when kind is part/model (content does not apply at all \u2014 see the kind parameter's description); accurate on every page, even though only page=1 actually carries content items."},"collections_total":{"type":"integer","description":"Same as pages_total, for Collections."},"items":{"type":"array","items":{"type":"object","description":"shapeSuggestedPart() (kind:part), shapeFitModel() (kind:model), or a Page/Collection row (kind:content \u2014 content_kind: page|collection, id, title, excerpt, url), plus rank and matched_via (sku|alias|keyword|stem|synonym|content|text). A content row's `id` is the local shopifypage/shopifycollection row id \u2014 stable across re-syncs of the shopify-content pipeline (upserts match on shop_eid + the Shopify id and update in place), so it is safe to key click analytics on via /partsapi/event's content_ref."}},"facets":{"type":"object","description":"category/subtype/source/availability tally, unpaged."},"fits_model":{"oneOf":[{"type":"null"},{"type":"object","description":"shapeFitModel() of the fits= model, or {resolved:false, input} when it failed to resolve."}]}}}}}},"400":{"description":"Missing q, an unknown kind/source/sort, a malformed in_stock, or a shop= that is not a myshopify.com domain."}},"security":[]}}}}