{
  "openapi": "3.1.0",
  "info": {
    "title": "DPS Product Feed API",
    "version": "1.0.0",
    "description": "Public, read-only, unauthenticated feed of every product Digital Power Systems (DPS) sells: identity (SKU/GTIN/MPN), localized content, net EUR pricing with the full quantity-tier ladder, live stock, and customs/logistics data (weight, dimensions, TARIC/HS code, country of origin). Built for a barcode-scanning label tool, third-party integrations, and LLM-based product lookup. There is no API key — everything here is already public on the storefront; this is the same data in a machine-readable shape. All prices are NET (VAT-exclusive) in EUR at quantity 1 for the default (non-account-specific) price tier; the 'tiers' array gives the same ladder shown on the product page. Stock is refreshed at most once per minute — see 'stock_as_of' on every response that includes availability. A full re-sync (to catch hard deletions, which 'updated_since' cannot see) should be run periodically with include_removed=true; see the 'X-Generated-At' response header for the underlying data's age. This feed can be disabled entirely by the shop operator (kill switch) — a 404 on every route below simultaneously most likely means the feed is temporarily off, not that the API moved."
  },
  "servers": [
    { "url": "https://digitalpowersystems.eu" }
  ],
  "tags": [
    { "name": "products", "description": "Product identity, content, pricing, logistics." },
    { "name": "stock", "description": "Live stock levels." },
    { "name": "meta", "description": "Machine discovery: this spec, and the linkset at /.well-known/api-catalog." }
  ],
  "paths": {
    "/api/v1/products": {
      "get": {
        "operationId": "listProducts",
        "tags": ["products"],
        "summary": "List every product, paginated",
        "description": "Returns every product (active by default) sorted by SKU in byte order. Use 'updated_since' plus a stored watermark for incremental sync, and periodically run a FULL sync with include_removed=true and no updated_since, since 'updated_since' can only see updates to still-existing rows, never hard deletions.",
        "parameters": [
          { "name": "lang", "in": "query", "description": "Active language code (case-insensitive). Defaults to the shop's default language. Every product's 'i18n' object always contains ALL active languages regardless of this parameter; it is only used to validate/scope pagination cursors.", "schema": { "type": "string" } },
          { "name": "updated_since", "in": "query", "description": "Strict RFC 3339 timestamp. Only products whose updated_at is >= this value are returned. Compares against the MAX updated_at across the product, its translations, price breaks, images, downloads, packaging, category assignment, and manufacturer.", "schema": { "type": "string", "format": "date-time" } },
          { "name": "include_removed", "in": "query", "description": "When 'true', inactive/removed products updated at or after 'updated_since' are also returned, as a minimal tombstone: {sku, active:false, updated_at}. A hard-deleted product row is invisible even with this flag — only a full periodic sync (no updated_since) catches that; compare the full SKU set against your own stored one.", "schema": { "type": "boolean", "default": false } },
          { "name": "limit", "in": "query", "description": "Page size, 1-500.", "schema": { "type": "integer", "minimum": 1, "maximum": 500, "default": 100 } },
          { "name": "cursor", "in": "query", "description": "Opaque keyset pagination cursor from a previous response's meta.next_cursor. Tied to the exact lang/updated_since filters it was issued under — reusing it with different filters, or after the server's signing secret has rotated, returns 400.", "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "A page of products (and tombstones).",
            "headers": {
              "ETag": { "schema": { "type": "string" }, "description": "Weak ETag over the exact response bytes." },
              "X-Generated-At": { "schema": { "type": "string", "format": "date-time" }, "description": "When the underlying master-data snapshot was built." },
              "X-Stock-As-Of": { "schema": { "type": "string", "format": "date-time" }, "description": "When the availability figures in this response were last refreshed (at most 60s old)." }
            },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProductListEnvelope" } } }
          },
          "304": { "description": "Not Modified — the ETag in If-None-Match still matches." },
          "400": { "description": "Invalid lang, updated_since, limit, or cursor.", "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } } },
          "404": { "description": "The product feed is currently disabled by the shop operator.", "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } } },
          "429": { "description": "Rate limit exceeded (120 requests/minute/IP).", "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } } }
        }
      }
    },
    "/api/v1/products/{sku}": {
      "get": {
        "operationId": "getProductBySKU",
        "tags": ["products"],
        "summary": "Fetch one product by its exact SKU",
        "description": "SKU matching is case-sensitive and exact (no fuzzy/partial matching). Percent-encode the SKU if it contains '/'.",
        "parameters": [
          { "name": "sku", "in": "path", "required": true, "description": "The product's SKU, percent-encoded if necessary. Maximum 100 characters.", "schema": { "type": "string", "maxLength": 100 } }
        ],
        "responses": {
          "200": {
            "description": "The product.",
            "headers": {
              "ETag": { "schema": { "type": "string" } },
              "X-Generated-At": { "schema": { "type": "string", "format": "date-time" } },
              "X-Stock-As-Of": { "schema": { "type": "string", "format": "date-time" } }
            },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProductRecord" } } }
          },
          "304": { "description": "Not Modified." },
          "400": { "description": "SKU exceeds 100 characters or is malformed percent-encoding.", "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } } },
          "404": { "description": "No active product with this exact SKU (or the feed is disabled).", "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } } }
        }
      }
    },
    "/api/v1/products/by-gtin/{gtin}": {
      "get": {
        "operationId": "getProductByGTIN",
        "tags": ["products"],
        "summary": "Look up a product (or its multi-pack packaging) by scanned barcode",
        "description": "The primary endpoint for a barcode-scanning label tool: scan a product's OR a carton/pallet's barcode and resolve it back to the product. Accepts EAN-8, UPC-A/EAN-12, EAN-13, or GTIN-14 (8/12/13/14 digits with a valid GS1 Mod-10 check digit); all are normalized to GTIN-14 before matching, so any of the four lengths for the same underlying code resolve identically. Matches against both the product's own GTIN and every Gebinde (multi-pack) GTIN.",
        "parameters": [
          { "name": "gtin", "in": "path", "required": true, "description": "8, 12, 13, or 14 ASCII digits with a valid GS1 check digit.", "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "Exactly one match.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProductByGTINResponse" } } }
          },
          "400": { "description": "Not a valid 8/12/13/14-digit GTIN/EAN.", "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } } },
          "404": { "description": "No product or packaging carries this GTIN.", "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } } },
          "409": { "description": "Data-quality problem: more than one product/packaging carries this GTIN. See 'skus' in the response body for the affected SKUs; report to the shop operator.", "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/DuplicateGTINProblem" } } } }
        }
      }
    },
    "/api/v1/stock": {
      "get": {
        "operationId": "listStock",
        "tags": ["stock"],
        "summary": "Live stock levels for every active product",
        "description": "Refreshed at most once per minute (see stock_as_of). Poll this instead of re-fetching /api/v1/products just to watch stock — it is much cheaper to build.",
        "responses": {
          "200": {
            "description": "Stock levels.",
            "headers": {
              "ETag": { "schema": { "type": "string" } },
              "X-Stock-As-Of": { "schema": { "type": "string", "format": "date-time" } }
            },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StockResponse" } } }
          },
          "304": { "description": "Not Modified." },
          "404": { "description": "The product feed is currently disabled.", "content": { "application/problem+json": { "schema": { "$ref": "#/components/schemas/Problem" } } } }
        }
      }
    },
    "/api/v1/openapi.json": {
      "get": {
        "operationId": "getOpenAPISpec",
        "tags": ["meta"],
        "summary": "This document",
        "responses": { "200": { "description": "This OpenAPI 3.1 document.", "content": { "application/json": { "schema": { "type": "object" } } } } }
      }
    }
  },
  "components": {
    "schemas": {
      "Problem": {
        "type": "object",
        "description": "RFC 9457 Problem Details. 'type' is always \"about:blank\"; the actual explanation is in 'title'.",
        "properties": {
          "type": { "type": "string" },
          "title": { "type": "string" },
          "status": { "type": "integer" }
        },
        "required": ["type", "title", "status"]
      },
      "DuplicateGTINProblem": {
        "allOf": [
          { "$ref": "#/components/schemas/Problem" },
          {
            "type": "object",
            "properties": {
              "skus": { "type": "array", "items": { "type": "string" } }
            },
            "required": ["skus"]
          }
        ]
      },
      "ProductListEnvelope": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "oneOf": [
                { "$ref": "#/components/schemas/ProductRecord" },
                { "$ref": "#/components/schemas/ProductTombstone" }
              ]
            }
          },
          "meta": { "$ref": "#/components/schemas/ProductListMeta" },
          "links": { "$ref": "#/components/schemas/ProductListLinks" }
        },
        "required": ["data", "meta", "links"]
      },
      "ProductListMeta": {
        "type": "object",
        "properties": {
          "count": { "type": "integer", "description": "Number of items in this page's data array." },
          "total": { "type": "integer", "description": "Total number of items matching the filters, across all pages." },
          "next_cursor": { "type": ["string", "null"], "description": "Cursor for the next page; null on the last page." }
        },
        "required": ["count", "total"]
      },
      "ProductListLinks": {
        "type": "object",
        "properties": {
          "self": { "type": "string", "format": "uri" },
          "next": { "type": ["string", "null"], "format": "uri", "description": "URL of the next page; null on the last page." }
        },
        "required": ["self"]
      },
      "ProductTombstone": {
        "type": "object",
        "description": "A removed/deactivated product returned only when include_removed=true.",
        "properties": {
          "sku": { "type": "string" },
          "active": { "type": "boolean", "enum": [false] },
          "updated_at": { "type": "string", "format": "date-time" }
        },
        "required": ["sku", "active", "updated_at"]
      },
      "ProductByGTINResponse": {
        "allOf": [
          { "$ref": "#/components/schemas/ProductRecord" },
          {
            "type": "object",
            "properties": {
              "matched_packaging": {
                "oneOf": [
                  { "$ref": "#/components/schemas/Packaging" },
                  { "type": "null" }
                ],
                "description": "The specific Gebinde/packaging variant that matched, or null when the scanned code was the product's own EAN."
              }
            },
            "required": ["matched_packaging"]
          }
        ]
      },
      "StockResponse": {
        "type": "object",
        "properties": {
          "data": { "type": "array", "items": { "$ref": "#/components/schemas/StockEntry" } },
          "stock_as_of": { "type": "string", "format": "date-time" }
        },
        "required": ["data", "stock_as_of"]
      },
      "StockEntry": {
        "type": "object",
        "properties": {
          "sku": { "type": "string" },
          "gtin": { "type": "string" },
          "stock": { "type": "integer" },
          "status": { "type": "string", "enum": ["in_stock", "out_of_stock", "made_to_order"] }
        },
        "required": ["sku", "stock", "status"]
      },
      "ProductRecord": {
        "type": "object",
        "description": "One product, with all its localized content, pricing, availability, and logistics data.",
        "properties": {
          "id": { "type": "string" },
          "sku": { "type": "string" },
          "gtin": { "type": "string", "description": "Mod-10-validated EAN/GTIN in its original length (8/12/13/14 digits). Omitted when the product has none, or its stored EAN failed validation." },
          "gtin14": { "type": "string", "description": "Same code as 'gtin', zero-padded to canonical GTIN-14 form — use this to compare against a scanned barcode of any length." },
          "mpn": { "type": "string" },
          "brand": { "type": "string" },
          "active": { "type": "boolean" },
          "updated_at": { "type": "string", "format": "date-time" },
          "manufacturer": { "oneOf": [ { "$ref": "#/components/schemas/Manufacturer" }, { "type": "null" } ] },
          "i18n": {
            "type": "object",
            "description": "Keyed by active language code (e.g. \"en\", \"de\", \"fr\").",
            "additionalProperties": { "$ref": "#/components/schemas/Localized" }
          },
          "price": { "oneOf": [ { "$ref": "#/components/schemas/Price" }, { "type": "null" } ], "description": "Null when the product has no default price break." },
          "availability": { "oneOf": [ { "$ref": "#/components/schemas/Availability" }, { "type": "null" } ], "description": "Null only in the extremely unusual case that no stock data could be resolved at all." },
          "logistics": { "$ref": "#/components/schemas/Logistics" },
          "packaging": { "type": "array", "items": { "$ref": "#/components/schemas/Packaging" } }
        },
        "required": ["id", "sku", "active", "updated_at", "i18n", "price", "availability", "logistics", "packaging"]
      },
      "Manufacturer": {
        "type": "object",
        "description": "GPSR manufacturer-of-record, exactly as shown on the product page.",
        "properties": {
          "name": { "type": "string" },
          "website": { "type": "string" },
          "postal_address": { "type": "string" },
          "email": { "type": "string" },
          "eu_responsible_name": { "type": "string" },
          "eu_responsible_address": { "type": "string" },
          "eu_responsible_email": { "type": "string" }
        }
      },
      "Localized": {
        "type": "object",
        "properties": {
          "name": { "type": "string" },
          "subtitle": { "type": "string" },
          "description": { "type": "string" },
          "features": { "type": "string" },
          "safety_warnings": { "type": "string" },
          "content_language": { "type": "string", "description": "Present only when this language has no translation: the language code the name, texts and url actually come from (usually the default language)." },
          "url": { "type": "string", "format": "uri", "description": "Canonical public product page URL in this language." },
          "category_path": { "type": "string" },
          "images": { "type": "array", "items": { "$ref": "#/components/schemas/Image" } },
          "specs": { "type": "array", "items": { "$ref": "#/components/schemas/Spec" } },
          "documents": { "type": "array", "items": { "$ref": "#/components/schemas/Document" } }
        },
        "required": ["name", "url", "images", "specs", "documents"]
      },
      "Image": {
        "type": "object",
        "properties": {
          "url": { "type": "string", "format": "uri" },
          "alt": { "type": "string" }
        },
        "required": ["url"]
      },
      "Spec": {
        "type": "object",
        "description": "One display-ready spec row. 'value' is the bare value (e.g. \"24\"); 'display' merges the unit in (e.g. \"24 V\").",
        "properties": {
          "key": { "type": "string" },
          "label": { "type": "string" },
          "value": { "type": "string" },
          "unit": { "type": "string" },
          "display": { "type": "string" }
        },
        "required": ["key", "label", "value", "display"]
      },
      "Document": {
        "type": "object",
        "properties": {
          "name": { "type": "string" },
          "category": { "type": "string" },
          "language": { "type": "string" },
          "url": { "type": "string", "format": "uri" }
        },
        "required": ["name", "url"]
      },
      "Price": {
        "type": "object",
        "description": "Net EUR price at quantity 1, default (non-account-specific) pricing.",
        "properties": {
          "currency": { "type": "string", "enum": ["EUR"] },
          "model": { "type": "string", "enum": ["tiered", "interpolated"], "description": "'tiered': the price is a step function of the tiers below. 'interpolated': quantities between two tiers are computed by linear interpolation between them; the tiers array still gives the exact breakpoints." },
          "net": { "type": "number" },
          "tiers": { "type": "array", "items": { "$ref": "#/components/schemas/Tier" }, "description": "Default price-group breaks only, sorted by min_qty ascending." },
          "vat": { "$ref": "#/components/schemas/VAT" }
        },
        "required": ["currency", "model", "net", "tiers", "vat"]
      },
      "Tier": {
        "type": "object",
        "properties": {
          "min_qty": { "type": "integer" },
          "net": { "type": "number" }
        },
        "required": ["min_qty", "net"]
      },
      "VAT": {
        "type": "object",
        "description": "Computed from the shop's home country, never from the requester's location.",
        "properties": {
          "country": { "type": "string" },
          "rate": { "type": "number" },
          "gross": { "type": "number" }
        },
        "required": ["country", "rate", "gross"]
      },
      "Availability": {
        "type": "object",
        "properties": {
          "stock": { "type": "integer" },
          "status": { "type": "string", "enum": ["in_stock", "out_of_stock", "made_to_order"] },
          "lead_time": { "type": "string", "description": "Free-text delivery-time range, e.g. \"1-3\" business days or \"2-4\" weeks for backorder/made-to-order items." },
          "stock_as_of": { "type": "string", "format": "date-time" }
        },
        "required": ["stock", "status", "stock_as_of"]
      },
      "Logistics": {
        "type": "object",
        "properties": {
          "weight_net_kg": { "type": "number" },
          "dimensions_mm": { "oneOf": [ { "$ref": "#/components/schemas/Dimensions" }, { "type": "null" } ] },
          "taric_code": { "type": "string", "description": "10-digit EU customs tariff code." },
          "hs_code": { "type": "string", "description": "The first 6 digits of taric_code — the international Harmonized System code." },
          "country_of_origin": { "type": "string" },
          "contains_battery": { "type": "boolean" },
          "made_to_order": { "type": "boolean" },
          "google_product_category": { "type": "string" }
        },
        "required": ["contains_battery", "made_to_order", "dimensions_mm"]
      },
      "Dimensions": {
        "type": "object",
        "description": "Millimetres. Only ever present when length, width, AND height are all known and positive.",
        "properties": {
          "length": { "type": "integer" },
          "width": { "type": "integer" },
          "height": { "type": "integer" }
        },
        "required": ["length", "width", "height"]
      },
      "Packaging": {
        "type": "object",
        "description": "One multi-pack (Gebinde) variant — carton, pallet, etc. — each independently barcoded.",
        "properties": {
          "type": { "type": "string", "description": "e.g. \"Karton\", \"Palette\"." },
          "sku": { "type": "string" },
          "gtin": { "type": "string" },
          "gtin14": { "type": "string" },
          "units": { "type": "integer", "description": "How many of the base product this packaging contains." },
          "weight_kg": { "type": "number" },
          "dimensions_mm": { "oneOf": [ { "$ref": "#/components/schemas/Dimensions" }, { "type": "null" } ] }
        },
        "required": ["type", "units", "dimensions_mm"]
      }
    }
  }
}
