openapi: 3.1.1
info:
  title: TrustRails API
  version: 0.1.0
  description: >
    UK electronics product search API for AI assistants.
    Covers 26,000+ products from major UK retailers across 19 categories.
    All prices are in GBP.


    HOW TO CALL searchProducts — read before every call:

    Decompose the user's request into filters first. Only what's left over goes in query.
    STEP 1: brand name → brand filter. STEP 2: product category → category filter. STEP 3: price → min_price/max_price. STEP 4: RAM, storage, screen size, resolution, refresh rate, wattage, Wi-Fi generation → constraints. STEP 5: what remains → query.

    BAD: query='Sony headphones under £200' | GOOD: brand='Sony', category='Headphones', max_price=200, no query.
    BAD: query='tablet' | GOOD: category='Tablets', no query.
    BAD: query='smartwatch' | GOOD: category='Wearables', no query.
    BAD: query='macbook neo' | GOOD: brand='Apple', category='Laptops', query='neo'.
    BAD: query='Samsung QLED TV' | GOOD: brand='Samsung', category='TVs', query='qled'.

    Only put differentiating terms in query: model lines (neo, ultra, oled), variants, model numbers (WH-1000XM5, s25 ultra).
    If brand+category alone cover what the user wants, omit query entirely.
    Query words must appear in the title, except words naming the category ("router" in Networking) and bare numbers or specs
    ("4070", "16GB"), which are ignored when finding products and only rank them. Put a model number with its prefix
    ("RTX 4070", not "4070") and check each title for it. Leave out use-case words like gaming, cheap or best.
    Always set lite=true. If 0 results, broaden the query or drop filters.
    Use getProduct for a product's attributes, description and every offer.

    Valid categories: Laptops, Desktops, Tablets, Phones, TVs, Monitors, Headphones, Speakers,
    Cameras, Keyboards, Mice, Printers, Networking, Storage, Gaming, Wearables, Drones, Audio, Cables & Chargers.
    'Smartphones' is not valid — use 'Phones'. 'Televisions' is not valid — use 'TVs'.

    For RAM, storage, screen size, resolution, refresh rate, wattage and Wi-Fi generation, use the constraints parameter,
    and always set category (and brand if known) with it: a search of only specs, with no category, brand or search words, returns 400.
    If a requirement is ambiguous (e.g. "16GB" could be RAM or storage), ask the user or search without that constraint.
    With constraints, each result has constraint_status per name: 'matched' = a retailer's title states a value that meets it.
    'unverified' = not known to meet it: check attributes[name], where conflicting means retailers disagree and missing means unknown.
    It is never a match, so tell the user it is unconfirmed.
    With constraints, total counts the products that match every constraint and unverified_total the unverified products that passed the other filters (only some may be in products).
    If total is 0, say none is known to meet the requirement.
    A spec written in query only counts when it says what it is ("24GB RAM", "1TB"); a bare "24GB" does not.
    If candidates_truncated is true, the first 2,000 candidates in the chosen sort order were checked and more exist:
    add a brand or category, or a narrower query, and search again. If the search was already narrowed, tell the user the results may be incomplete.
    For specs not in attributes (ports, weight, battery), search first then call getProduct on top 3-5 results; don't guess them from titles.
    getProduct's specs.description is the retailer's prose, for details attributes do not cover; it never overrides or fills in an attribute.
    If offer_count > 1, call getProduct for the 1-3 products you will recommend, not for every result, and show the cheapest retailer,
    the other prices with the difference and the exact saving. Only claim a saving between offers of the same configuration:
    if any attribute is conflicting, check offers[].title first and never claim a saving between different sizes or configurations.

servers:
  - url: https://trustrails.app

paths:
  /api/search:
    get:
      operationId: searchProducts
      summary: Search UK electronics products
      description: >
        Search UK electronics by brand, category, price and spec constraints. Returns title, brand,
        price, availability and purchase link. Use constraints for spec requirements; use total for
        how many matched. Use getProduct for attributes, the retailer's description and all offers.
      parameters:
        - in: query
          name: query
          description: >
            Refinement terms ONLY — model lines, series, variants, model numbers (e.g. 'neo', 'ultra', 'oled', 'WH-1000XM5', 's25 ultra').
            NEVER a category name: BAD query='tablet', query='smartwatch', query='laptop'. Set the category filter instead.
            NEVER a brand name: BAD query='Sony'. Set the brand filter instead.
            NEVER a price.
            Omit entirely when browsing a category or brand.
          schema:
            type: string
            example: neo
        - in: query
          name: min_price
          description: Minimum price in GBP. Use this instead of putting prices in the query.
          schema:
            type: number
            example: 400
        - in: query
          name: max_price
          description: Maximum price in GBP. Use this instead of putting prices in the query.
          schema:
            type: number
            example: 800
        - in: query
          name: brand
          description: >
            Filter by brand name (exact match, case-insensitive).
            Use this instead of putting brand names in the query.
            Examples: Apple, Samsung, Sony, HP, Dell, Lenovo, Anker, Bose, LG.
          schema:
            type: string
            example: HP
        - in: query
          name: category
          description: >
            Filter by product category. Use ONLY these exact values:
            Laptops, Desktops, Tablets, Phones, TVs, Monitors,
            Headphones, Speakers, Cameras, Keyboards, Mice, Printers, Networking,
            Storage, Gaming, Wearables, Drones, Audio, Cables & Chargers.
            NOTE: 'Smartphones' is not valid — use 'Phones'. 'Televisions' is not valid — use 'TVs'.
          schema:
            type: string
            example: Laptops
        - in: query
          name: constraints
          description: >
            Hard spec requirements as a JSON string. Shape {name: {op: number}}, op eq, gte or lte;
            a range is {"gte": 13, "lte": 15}. Names and units: memory_gb (RAM, GB), storage_gb (GB),
            screen_in (inches), resolution_p (pixels high, 4K = 2160), refresh_hz (Hz), power_w (W),
            wifi_gen (6, 6E = 6.5, 7). Always set category. Each result's constraint_status is
            matched (a title states a value that meets it) or unverified (never a match).
          schema:
            type: string
            example: '{"memory_gb":{"gte":24},"storage_gb":{"gte":1000},"screen_in":{"eq":15}}'
        - in: query
          name: limit
          description: Maximum number of products to return (default 50, max 100).
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
            example: 50
        - in: query
          name: sort
          description: >
            Sort order for results. Use 'price_asc' for cheapest first, 'price_desc' for most expensive first.
            Default is 'relevance' (best match first).
            When comparing prices or finding the best deal, use 'price_asc'. With constraints, matched products still come first.
          schema:
            type: string
            enum: [relevance, price_asc, price_desc]
            default: relevance
            example: price_asc
        - in: query
          name: lite
          description: >
            Return trimmed product objects with only essential fields
            (id, title, brand, price, currency, availability, image_url, purchase_url, offer_count and, with constraints,
            constraint_status and the status and value of the constrained names, without sources).
            Always set to true unless the user specifically needs full product objects.
          schema:
            type: boolean
            default: false
            example: true
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  products:
                    type: array
                    items:
                      $ref: '#/components/schemas/Product'
                  total:
                    type: integer
                    description: >
                      Total number of matching products (may exceed the returned limit). With constraints, the products
                      that match every constraint.
                  constraints:
                    type: object
                    description: The constraints applied (from the constraints parameter or specs written in query). Only present when there were any.
                    example: {memory_gb: {gte: 24}}
                  excluded_by_constraints:
                    type: integer
                    description: Products left out because their stated value fails a constraint.
                  unverified_total:
                    type: integer
                    description: With constraints, the unverified products that passed the other filters; only some may be in products. Never a match.
                  candidates_truncated:
                    type: boolean
                    description: >
                      Present and true when constraints were applied and more candidates exist than the 2,000 checked:
                      the first 2,000 in the chosen sort order were checked. Add a brand or category, or a narrower query,
                      and search again before concluding that nothing matches. If the search was already narrowed,
                      the results may be incomplete.
        '400':
          description: Invalid parameter, including invalid constraints
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: 'Unknown constraint "ram_gb". Valid names: memory_gb, storage_gb, power_w, refresh_hz, resolution_p, screen_in, wifi_gen.'
      security:
        - ApiKeyAuth: []

  /api/product/{id}:
    get:
      operationId: getProduct
      summary: Get full product details by ID
      description: >
        Full product details by ID: attributes (confirmed, inferred or conflicting; the source of
        truth for specs), specs.description (the retailer's prose; never overrides attributes) and
        offers with per-retailer prices. Accepts canonical product IDs or retailer offer IDs.
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
            example: '43740928768'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Product'
        '404':
          description: Product not found
      security:
        - ApiKeyAuth: []

  /api/health:
    get:
      operationId: getHealth
      summary: Service health
      description: Quick status check showing product count and last sync time.
      responses:
        '200':
          description: Basic service info
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    example: ok
                  items:
                    type: integer
                    example: 26000
                  last_update:
                    type: string
                    format: date-time
                    example: '2026-02-23T10:00:00Z'

components:
  securitySchemes:
    ApiKeyAuth:
      type: http
      scheme: bearer
      bearerFormat: API_KEY
      description: 'Use Bearer token authentication with your API key'

  schemas:
    Product:
      type: object
      required:
        - id
        - title
        - price
        - currency
        - availability
        - purchase_url
      properties:
        id:
          type: string
          example: '43740928768'
        title:
          type: string
          example: Apple iPhone 16 Plus 256GB Ultramarine
        brand:
          type: string
          example: Apple
        price:
          type: number
          description: Price in GBP
          example: 923.99
        currency:
          type: string
          example: GBP
        availability:
          type: string
          enum: [in_stock, low_stock, out_of_stock]
          example: in_stock
        stock:
          type: integer
          example: 34
        delivery_time:
          type: string
          example: '3-5 working days'
        image_url:
          type: string
          format: uri
          description: Product image URL
        category:
          type: string
          description: >
            Canonical product category. One of: Laptops, Desktops, Tablets, Phones, TVs,
            Monitors, Headphones, Speakers, Cameras, Keyboards, Mice, Printers, Networking,
            Storage, Gaming, Wearables, Drones, Audio, Cables & Chargers.
          example: Laptops
        product_type:
          type: string
          enum: [product, accessory]
          description: Whether this is a primary product or an accessory (e.g. a phone case).
          example: product
        specs:
          type: object
          description: >
            Retailer-supplied details. RAM, storage, screen size, resolution, refresh rate, power and
            Wi-Fi generation are in attributes, not here. specs.description is the retailer's prose,
            useful for what attributes do not cover (processor, ports, GPU); it can describe another
            configuration and never overrides or fills in an attribute. Other fields: model_number, dimensions.
          properties:
            description:
              type: string
              description: The retailer's own prose. Supporting detail for what attributes do not cover, such as processor, ports and GPU. It can describe another configuration or a maximum ("up to 32GB"), so it never overrides or fills in an attribute.
              example: 'Intel Core Ultra 7 266V, Intel Arc 140V graphics, 2 x Thunderbolt 4, HDMI 2.1, Windows 11 Pro'
            model_number:
              type: string
              example: MXY23QN/A
            dimensions:
              type: string
              example: '340 x 240 x 17mm'
        attributes:
          type: object
          description: >
            Structured specs read from retailer titles (getProduct, and searches that pass constraints;
            a lite search holds only the constrained names, without sources).
            Keys: memory_gb (RAM, GB), storage_gb (GB, 1TB = 1000), screen_in (inches),
            resolution_p (pixels high: 4K = 2160, QHD = 1440, Full HD = 1080), refresh_hz (Hz), power_w (W), wifi_gen (Wi-Fi generation).
            A key nobody states is absent (unknown). status confirmed = two or more retailers state the same value;
            inferred = one retailer's title states it; conflicting = retailers state different values and none is picked
            (a retailer can appear under two values, so read offers[].title to see which listing states which).
            specs.description is retailer prose and never overrides or fills in an attribute.
          additionalProperties:
            $ref: '#/components/schemas/Attribute'
          example:
            memory_gb:
              status: confirmed
              value: 24
              sources:
                - {retailer: Back to the Office, field: title}
                - {retailer: AO.com, field: title}
        constraint_status:
          type: object
          description: >
            Only in searches that pass constraints (or write specs in query). Per constraint name: matched = a retailer's title
            states a value that meets it; unverified = not known to meet it, never treat as a match
            (attributes[name].status conflicting means retailers disagree, missing means unknown).
          additionalProperties:
            type: string
            enum: [matched, unverified]
          example: {memory_gb: matched, storage_gb: unverified}
        provenance:
          type: object
          description: Data source and freshness information
          required: [source, last_updated]
          properties:
            source:
              type: string
              example: AO
            last_updated:
              type: string
              format: date-time
              example: '2026-02-23T10:00:00Z'
        purchase_url:
          type: string
          format: uri
          description: Direct link to purchase from the cheapest retailer
          example: 'https://trustrails.app/go/43740928768'
        offer_count:
          type: integer
          description: "Number of retailer offers available for this product. If >1, call getProduct for the 1-3 products you will recommend and show the cheapest retailer, the other prices with the difference and the exact saving; never mention multiple offers without retailer pricing details. Only claim savings between offers of the same configuration: if any attribute is conflicting, check offers[].title first."
          example: 3
        offers:
          type: array
          description: Per-retailer offers sorted by price (only in full mode / getProduct response)
          items:
            $ref: '#/components/schemas/Offer'

    Attribute:
      type: object
      required: [status]
      properties:
        status:
          type: string
          enum: [confirmed, inferred, conflicting]
        value:
          type: number
          description: The stated value (confirmed and inferred). Absent when conflicting.
          example: 24
        sources:
          type: array
          description: Retailers whose listing states the value (confirmed and inferred). Not in lite search results.
          items:
            $ref: '#/components/schemas/AttributeSource'
        values:
          type: array
          description: Each different value with its retailers (conflicting only). Retailers are not in lite search results.
          items:
            type: object
            properties:
              value:
                type: number
              sources:
                type: array
                items:
                  $ref: '#/components/schemas/AttributeSource'

    AttributeSource:
      type: object
      properties:
        retailer:
          type: string
          example: AO.com
        field:
          type: string
          enum: [title]

    Offer:
      type: object
      required:
        - id
        - source
        - title
        - price
        - purchase_url
      properties:
        id:
          type: string
          example: '43740928768'
        source:
          type: string
          description: Retailer name
          example: AO.com
        title:
          type: string
        price:
          type: number
          description: Price in GBP at this retailer
          example: 889.00
        currency:
          type: string
          example: GBP
        availability:
          type: string
          enum: [in_stock, low_stock, out_of_stock]
        stock:
          type: integer
        delivery_time:
          type: string
        purchase_url:
          type: string
          format: uri
          description: Direct link to purchase from this retailer
        image_url:
          type: string
          format: uri
        last_updated:
          type: string
          format: date-time
