{
 "openapi": "3.0.3",
 "info": {
  "title": "EcsCart Storefront API",
  "version": "1.0.0",
  "description": "Machine-readable shop API for AI agents. Public reads do not create a session. Stateful operations use the single PHPSESSID cookie. GET /session explicitly starts a session and returns its CSRF token. IDs are opaque OXID strings (letters, digits, dots, underscores — e.g. oxidstandard, demo_art_007, a7c40f631fc920687.20179984). GET /api returns an endpoint index. Write requests in cookie-session mode require a same-origin Origin/Referer or Sec-Fetch-Site header, or the session CSRF token as X-EC-CSRF header (obtain a token with GET /session; csrf in public responses is empty without an existing session). Bearer token requests need no CSRF check."
 },
 "servers": [
  {
   "url": "/api"
  }
 ],
 "paths": {
  "/": {
   "get": {
    "summary": "API discovery index",
    "responses": {
     "200": {
      "description": "{api,docs,openapi,endpoints[]}"
     }
    }
   }
  },
  "/shop": {
   "get": {
    "summary": "Shop metadata",
    "responses": {
     "200": {
      "description": "name, currency, api version"
     }
    }
   }
  },
  "/categories": {
   "get": {
    "summary": "All categories as flat list with parent_id (roots have parent_id \"\")",
    "responses": {
     "200": {
      "description": "{items:[{id,title,parent_id,url,image}]}"
     }
    }
   }
  },
  "/categories/{id}/products": {
   "get": {
    "summary": "Products in category, including all descendant categories. Response carries category_ids (resolved set). Unknown id → 404 category_not_found.",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "page",
      "in": "query",
      "schema": {
       "type": "integer",
       "default": 1
      }
     },
     {
      "name": "per_page",
      "in": "query",
      "schema": {
       "type": "integer",
       "default": 24,
       "maximum": 100
      }
     }
    ],
    "responses": {
     "200": {
      "description": "{total,page,category_ids,items:[Product]}"
     },
     "404": {
      "description": "{error:\"category_not_found\"}"
     }
    }
   }
  },
  "/products": {
   "get": {
    "summary": "List or search products",
    "parameters": [
     {
      "name": "q",
      "in": "query",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "page",
      "in": "query",
      "schema": {
       "type": "integer"
      }
     },
     {
      "name": "per_page",
      "in": "query",
      "schema": {
       "type": "integer",
       "maximum": 100
      }
     }
    ],
    "responses": {
     "200": {
      "description": "{total,page,items:[Product]}"
     }
    }
   }
  },
  "/banners": {
   "get": {
    "summary": "Active homepage banners (EcsDesk Lager > Banner, oxactions OXTYPE=3)",
    "responses": {
     "200": {
      "description": "{items:[{id,title,description,image,url}]} — url is a resolved product/category link"
     }
    }
   }
  },
  "/suggest": {
   "get": {
    "summary": "Autocomplete suggestions (umlaut- and typo-tolerant). Min 3 chars.",
    "parameters": [
     {
      "name": "q",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string",
       "minLength": 3
      }
     }
    ],
    "responses": {
     "200": {
      "description": "{articles:[{id,sku,title,url,image,price}], categories:[{id,title,url,image}]}"
     }
    }
   }
  },
  "/products/{id}": {
   "get": {
    "summary": "Product detail incl. variants/attributes/images",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "Product + description, images[], attributes[], variants[]"
     },
     "404": {
      "description": "not found"
     }
    }
   }
  },
  "/countries": {
   "get": {
    "summary": "Active countries",
    "responses": {
     "200": {
      "description": "{items:[{id,title}]}"
     }
    }
   }
  },
  "/shipping": {
   "get": {
    "summary": "Shipping methods with cost for a country/basket total",
    "parameters": [
     {
      "name": "country",
      "in": "query",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "total",
      "in": "query",
      "schema": {
       "type": "number"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "{items:[{id,title,cost}]}"
     }
    }
   }
  },
  "/payments": {
   "get": {
    "summary": "Payment methods for a country",
    "parameters": [
     {
      "name": "country",
      "in": "query",
      "schema": {
       "type": "string"
      },
      "description": "oxcountry id"
     },
     {
      "name": "total",
      "in": "query",
      "schema": {
       "type": "number"
      },
      "description": "gross cart total — enables OXFROMAMOUNT/OXTOAMOUNT filtering"
     },
     {
      "name": "shipset",
      "in": "query",
      "schema": {
       "type": "string"
      },
      "description": "oxdeliveryset id — only methods linked to this shipping set"
     }
    ],
    "responses": {
     "200": {
      "description": "{items:[{id,title,fee,fee_type}]}"
     }
    },
    "description": "Payment methods for a country, filtered like OXID PaymentList: customer-group restrictions (oxobject2group), OXFROMBONI credit check, OXFROMAMOUNT/OXTOAMOUNT range and shipping-set linkage apply. With a Bearer token or customer session the customer's groups/credit are used; anonymous callers see unrestricted methods only."
   }
  },
  "/totals": {
   "get": {
    "summary": "Full order totals for the session cart (central pricing calculation)",
    "parameters": [
     {
      "name": "ship",
      "in": "query",
      "schema": {
       "type": "string"
      },
      "description": "oxdeliveryset id — selected shipping method"
     },
     {
      "name": "pay",
      "in": "query",
      "schema": {
       "type": "string"
      },
      "description": "oxpayments id — selected payment method"
     },
     {
      "name": "country",
      "in": "query",
      "schema": {
       "type": "string"
      },
      "description": "oxcountry id — billing country (defaults to account/home country)"
     },
     {
      "name": "deldiff",
      "in": "query",
      "schema": {
       "type": "string"
      },
      "description": "'1' when a different delivery address applies"
     },
     {
      "name": "delcountry",
      "in": "query",
      "schema": {
       "type": "string"
      },
      "description": "oxcountry id — delivery country (only with deldiff=1)"
     },
     {
      "name": "ustid",
      "in": "query",
      "schema": {
       "type": "string"
      },
      "description": "VAT ID — EU B2B orders are billed 0% VAT"
     }
    ],
    "responses": {
     "200": {
      "description": "{tot:{goods_net,goods_brut,del_net,del_brut,pay_net,pay_brut,vat_rows,net_total,brut_total,ship,ship_sel,pay,pay_sel,...}, html, ship:[{id,title,brut,net}], pay:[{id,title,desc,fee,fee_type,fee_brut,fee_net}], ship_sel, pay_sel} — one authoritative response: option lists, server-validated selection and rendered sums all come from the same calculation; html is the pre-rendered checkout sum block (partials/checkout_sum)"
     }
    },
    "description": "Runs the single authoritative calculation (ecComputeTotals, lib/pricing.php) used by the checkout page and by order creation: goods totals net/gross, available shipping/payment methods with costs, selected fees, VAT rows grouped by rate including fee shares, net and gross totals. The storefront checkout swaps the returned html fragment into #sumLines on every shipping/payment/country change, so the displayed sum always matches the stored order. Gebühren werden einzeln auf Cent gerundet; Rabatt und Gutschein werden proportional verteilt. vat_rows enthält alle Steuersätze, auch mehr als zwei und Sätze mit Nachkommastellen."
   }
  },
  "/content": {
   "get": {
    "summary": "CMS content fragment by OXLOADID (title + raw HTML, no page layout)",
    "parameters": [
     {
      "name": "loadid",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string"
      },
      "description": "oxcontents.OXLOADID, e.g. oxagb, oxrightofwithdrawal, oximpressum"
     }
    ],
    "responses": {
     "200": {
      "description": "{loadid,title,html} — localized active CMS row; used by the checkout legal-text modal"
     },
     "404": {
      "description": "unknown or inactive loadid"
     }
    }
   }
  },
  "/cart": {
   "get": {
    "summary": "Current cart",
    "responses": {
     "200": {
      "description": "{count,total,total_net,vat{rate:amount},display_net,currency,csrf,ded_html,summary_html,items[]} — summary_html is the complete cart summary using checkout labels and totals after deductions (excluding shipping/payment fees); ded_html is the pre-rendered discount/voucher lines block from ecComputeTotals; csrf is the session token for X-EC-CSRF on write requests"
     }
    }
   },
   "delete": {
    "summary": "Clear cart",
    "responses": {
     "200": {
      "description": "cart"
     }
    }
   }
  },
  "/cart/items": {
   "post": {
    "summary": "Add to cart",
    "requestBody": {
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "product_id"
        ],
        "properties": {
         "product_id": {
          "type": "string"
         },
         "qty": {
          "type": "integer",
          "default": 1
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "cart"
     },
     "400": {
      "description": "{error} invalid_json, invalid_product_id or invalid_qty"
     },
     "404": {
      "description": "product_not_found"
     },
     "422": {
      "description": "{error:\"product_not_available\"} — variant required or not buyable"
     }
    }
   }
  },
  "/cart/items/{product_id}": {
   "put": {
    "summary": "Set item qty",
    "parameters": [
     {
      "name": "product_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "requestBody": {
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "qty": {
          "type": "integer"
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "cart"
     }
    }
   },
   "delete": {
    "summary": "Remove item",
    "parameters": [
     {
      "name": "product_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "cart"
     }
    }
   }
  },
  "/order": {
   "post": {
    "summary": "Create order. Guest/session flow, or customer account order via Bearer API token.",
    "description": "Successful orders also create an internal [EcsCart] customer-history entry in EcsDesk (oxremark), including the order reference and API origin, without access tokens.",
    "requestBody": {
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [],
        "properties": {
         "items": {
          "type": "array",
          "items": {
           "type": "object",
           "properties": {
            "product_id": {
             "type": "string"
            },
            "qty": {
             "type": "integer"
            }
           }
          }
         },
         "billing": {
          "type": "object",
          "required": [
           "email",
           "fname",
           "lname",
           "street",
           "streetnr",
           "zip",
           "city"
          ],
          "properties": {
           "email": {
            "type": "string"
           },
           "fname": {
            "type": "string"
           },
           "lname": {
            "type": "string"
           },
           "company": {
            "type": "string"
           },
           "street": {
            "type": "string"
           },
           "streetnr": {
            "type": "string"
           },
           "addinfo": {
            "type": "string"
           },
           "zip": {
            "type": "string"
           },
           "city": {
            "type": "string"
           },
           "countryid": {
            "type": "string"
           },
           "stateid": {
            "type": "string"
           },
           "fon": {
            "type": "string"
           },
           "sal": {
            "type": "string"
           },
           "ustid": {
            "type": "string"
           }
          }
         },
         "delivery": {
          "type": "object",
          "description": "optional, same fields as billing"
         },
         "shipping_id": {
          "type": "string"
         },
         "payment_id": {
          "type": "string"
         },
         "remark": {
          "type": "string"
         },
         "delivery_address_id": {
          "type": "string",
          "description": "token mode only: oxaddress.OXID of a delivery address stored on the customer account"
         }
        }
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "{order_id,order_number,total,status:\"ORDERFOLDER_NEW\",payment_status,payment_url} — payment_url carries a fresh PayPal retry link when the order is open/NOT_FINISHED, else null"
     },
     "422": {
      "description": "{error} e.g. token_limit_sum, token_limit_rate, missing billing fields. Limit errors carry total (attempted gross), max_order_sum, max_orders, period_hours and, for rate limits, orders_in_window."
     },
     "400": {
      "description": "{error} e.g. invalid_json, invalid_items (with items[] error list: invalid_product_id, invalid_qty, product_not_found, variant_required, product_not_available), items_required, token_order_account_addresses"
     },
     "401": {
      "description": "{error:\"invalid_token\"} when the Bearer token is unknown or revoked"
     },
     "404": {
      "description": "{error:\"delivery_address_not_found\"}"
     }
    },
    "description": "Two modes: (1) anonymous/session — provide billing (and optional delivery); (2) Bearer token — send items[] only; billing/delivery are taken from the token owner's stored account addresses. With a token, billing/delivery objects are rejected (400); a stored delivery address may be selected via delivery_address_id. Token limits: per-order gross total and orders-per-window are enforced by the shop (422 token_limit_sum/token_limit_rate). Tokens are created by the customer under /account/apitokens.",
    "security": [
     {
      "bearerAuth": []
     },
     {}
    ]
   }
  },
  "/order/{order_id}": {
   "get": {
    "summary": "Order status (own orders; session login or Bearer token of the owning account)",
    "parameters": [
     {
      "name": "order_id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "order detail: {order_id,order_number,date,status(OXFOLDER),total,total_net,total_brut,currency,vat{rate:amount},shipping_id,shipping_title,shipping_cost,payment_id,payment_title,payment_desc,payment_cost,payment_status,payment_url,billing,delivery,remark,paid,shipped,cancelled,items[]}. paid/shipped are timestamps or null; payment_url is a PayPal retry link while the order is NOT_FINISHED, else null. vat keys are rate strings (e.g. \"19\", \"12.9\"). grand_total_net enthält die gesamte Nettosumme einschließlich Gebühren; total_net/total_brut bleiben die Warenwerte nach Abzügen. vat umfasst sämtliche Steuergruppen. Stornierte Positionen werden ausgelassen."
     },
     "404": {
      "description": "not_found"
     }
    }
   }
  },
  "/chat/config": {
   "get": {
    "summary": "KI assistant state (public). Feature flag, opening hours, bot availability.",
    "responses": {
     "200": {
      "description": "{data:{open,in_grace,next_open_at,hours_text,bot_online,texts,max_length,privacy:{ready,version,provider,provider_url,details,stores,trains,notice[],more_notice[],buffer_hours,archive,archive_days,quality_label,accept_label,accept_text,accepted,quality_accepted}}}. Public read; does not create a session. Show notice prominently and make more_notice and provider links accessible before approval. Provider name, URL and details are optional; omit empty values from the UI. ready is retained as true for compatibility and does not gate sending."
     },
     "404": {
      "description": "CHAT_DISABLED when the feature is switched off"
     }
    }
   }
  },
  "/chat/messages": {
   "get": {
    "summary": "Poll visitor conversation (requires X-Chat-Token or an existing chat session).",
    "parameters": [
     {
      "name": "after",
      "in": "query",
      "schema": {
       "type": "integer",
       "default": 0
      }
     }
    ],
    "responses": {
     "200": {
      "description": "{messages:[{id,sender,body,created_at,cards?[]}],status,close_reason,message_limit_reached,typing,chat}"
     },
     "428": {
      "description": "PRIVACY_CONSENT_REQUIRED; current privacy policy in error.privacy. No new message/session is created."
     }
    },
    "security": [
     {
      "chatToken": []
     },
     {
      "sessionCookie": []
     }
    ]
   },
   "post": {
    "summary": "Send a visitor message. The first message or changed privacy policy requires explicit prior approval of the displayed provider policy. Conversation access uses a secret random token, never customer ID or IP.",
    "requestBody": {
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "text",
         "client_msg_id"
        ],
        "properties": {
         "text": {
          "type": "string"
         },
         "client_msg_id": {
          "type": "string"
         },
         "page": {
          "type": "string",
          "description": "Optional public catalog context only. Account, order, checkout and other private paths are discarded. URL query and fragment are never forwarded."
         },
         "context_product_id": {
          "type": "string",
          "description": "Optional public article ID; may be resolved from an allowed public catalog page."
         },
         "lang": {
          "type": "string"
         },
         "hp": {
          "type": "string",
          "description": "honeypot, must be empty"
         },
         "privacy": {
          "type": "object",
          "required": [
           "version",
           "accepted",
           "quality"
          ],
          "properties": {
           "version": {
            "type": "string",
            "description": "Current privacy.version from GET /chat/config; show notice and make more_notice and provider links accessible before approval."
           },
           "accepted": {
            "type": "boolean",
            "enum": [
             true
            ]
           },
           "quality": {
            "type": "boolean",
            "description": "Separate optional consent for the shop quality archive; false by default. Chat works without quality consent."
           }
          }
         }
        }
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "{message:{id,sender,body,created_at},token,duplicate} — send X-Chat-Token on all follow-ups"
     },
     "200": {
      "description": "duplicate client_msg_id → same message returned"
     },
     "423": {
      "description": "CHAT_CLOSED outside opening hours"
     },
     "428": {
      "description": "PRIVACY_CONSENT_REQUIRED; current privacy policy in error.privacy. No new message/session is created."
     },
     "503": {
      "description": "CHAT_UNAVAILABLE if the chat storage capacity is exhausted. Optional provider fields do not restrict sending."
     }
    },
    "security": [
     {
      "chatToken": []
     },
     {
      "sessionCookie": []
     }
    ]
   }
  },
  "/chat/hint": {
   "post": {
    "summary": "Best-effort bot pre-wake only after an existing conversation has approved the current provider policy. Before approval this is a no-op.",
    "requestBody": {
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "hp": {
          "type": "string"
         },
         "page": {
          "type": "string"
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "{ok:true}"
     }
    },
    "security": [
     {
      "chatToken": []
     }
    ]
   }
  },
  "/chat/close": {
   "post": {
    "summary": "Visitor ends the conversation.",
    "requestBody": {
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "hp": {
          "type": "string"
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "{status:'closed',close_reason}"
     }
    },
    "security": [
     {
      "chatToken": []
     },
     {
      "sessionCookie": []
     }
    ]
   }
  },
  "/bot/health": {
   "get": {
    "summary": "Bot liveness check.",
    "responses": {
     "200": {
      "description": "{status:'ok',version}"
     }
    },
    "security": [
     {
      "botKey": []
     }
    ]
   }
  },
  "/bot/status": {
   "get": {
    "summary": "Chat status incl. enabled flag; works while disabled.",
    "responses": {
     "200": {
      "description": "{active,in_grace,enabled,next_open_at,hours_text,retry_after,...}"
     }
    },
    "security": [
     {
      "botKey": []
     }
    ]
   }
  },
  "/bot/articles": {
   "get": {
    "summary": "Visible catalog for the bot. ?search= word search (all words must match), ?article_number=, ?ean=, ?parent_id= variants, ?lang=, ?quantity= stock check, ?limit=<=20, ?offset=.",
    "parameters": [
     {
      "name": "search",
      "in": "query",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "article_number",
      "in": "query",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "ean",
      "in": "query",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "parent_id",
      "in": "query",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "lang",
      "in": "query",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "quantity",
      "in": "query",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "limit",
      "in": "query",
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "offset",
      "in": "query",
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "{items:[{id,article_number,title,price,stock,url,manufacturer,attributes}]}"
     }
    },
    "security": [
     {
      "botKey": []
     }
    ]
   }
  },
  "/bot/articles/{id}": {
   "get": {
    "summary": "Product detail for the bot (OXID or article number).",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "{id,article_number,title,price,stock,url,description,attributes,variants,manufacturer,category}"
     }
    },
    "security": [
     {
      "botKey": []
     }
    ]
   }
  },
  "/bot/inbox": {
   "get": {
    "summary": "Long-poll new visitor messages. ?after_id= cursor, ?wait=0..max, ?limit=.",
    "parameters": [
     {
      "name": "after_id",
      "in": "query",
      "schema": {
       "type": "integer"
      }
     },
     {
      "name": "wait",
      "in": "query",
      "schema": {
       "type": "integer"
      }
     },
     {
      "name": "limit",
      "in": "query",
      "schema": {
       "type": "integer"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "{active,in_grace,cursor,messages:[{id,conversation_id,text,page,created_at,context?}],conversations:{id:{history}},next_close_at}"
     },
     "423": {
      "description": "CHAT_CLOSED outside opening hours"
     }
    },
    "security": [
     {
      "botKey": []
     }
    ]
   }
  },
  "/bot/conversations": {
   "get": {
    "summary": "List conversations (?status=open|closed&limit=&offset=).",
    "responses": {
     "200": {
      "description": "[{id,status,lang,page,created_at,last_visitor_at,last_bot_at}]"
     }
    },
    "security": [
     {
      "botKey": []
     }
    ]
   }
  },
  "/bot/conversations/{id}/messages": {
   "get": {
    "summary": "Conversation history (?after_id=).",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     },
     {
      "name": "after_id",
      "in": "query",
      "schema": {
       "type": "integer"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "{status,messages:[{id,sender,text,created_at}]}"
     }
    },
    "security": [
     {
      "botKey": []
     }
    ]
   },
   "post": {
    "summary": "Bot answers a conversation. product_ids → validated product cards.",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "requestBody": {
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "text"
        ],
        "properties": {
         "text": {
          "type": "string"
         },
         "client_msg_id": {
          "type": "string"
         },
         "product_ids": {
          "type": "array",
          "items": {
           "type": "string"
          }
         }
        }
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "{id,duplicate}"
     }
    },
    "security": [
     {
      "botKey": []
     }
    ]
   }
  },
  "/bot/conversations/{id}/typing": {
   "post": {
    "summary": "Mark bot as typing (~10s).",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "{ok:true}"
     }
    },
    "security": [
     {
      "botKey": []
     }
    ]
   }
  },
  "/bot/conversations/{id}/mail": {
   "post": {
    "summary": "Forward conversation via e-mail (requires mail_enabled).",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "requestBody": {
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "subject": {
          "type": "string"
         },
         "to": {
          "type": "string"
         }
        }
       }
      }
     }
    },
    "responses": {
     "201": {
      "description": "{sent,duplicate}"
     }
    },
    "security": [
     {
      "botKey": []
     }
    ]
   }
  },
  "/bot/conversations/{id}/close": {
   "post": {
    "summary": "Bot ends the conversation.",
    "parameters": [
     {
      "name": "id",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      }
     }
    ],
    "requestBody": {
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "properties": {
         "reason": {
          "type": "string"
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "{status,close_reason}"
     }
    },
    "security": [
     {
      "botKey": []
     }
    ]
   }
  },
  "/token": {
   "get": {
    "summary": "Bearer token self-check: configured limits and current window usage. Call before ordering to learn the caps.",
    "responses": {
     "200": {
      "description": "{label,max_order_sum,max_orders,period_hours,orders_in_window,orders_remaining,last_used}"
     },
     "401": {
      "description": "token_required / invalid_token"
     }
    },
    "security": [
     {
      "bearerAuth": []
     }
    ]
   }
  },
  "/token/addresses": {
   "get": {
    "summary": "Billing address of the token owner plus selectable delivery addresses (delivery[].id → delivery_address_id on POST /order)",
    "responses": {
     "200": {
      "description": "{billing:{…fields…}, delivery:[{id,…fields…}]}"
     },
     "401": {
      "description": "token_required / invalid_token"
     }
    },
    "security": [
     {
      "bearerAuth": []
     }
    ]
   }
  },
  "/token/orders": {
   "get": {
    "summary": "Orders placed with this Bearer token",
    "responses": {
     "200": {
      "description": "{items:[{order_id,order_number,date,total,currency,status,cancelled,paid,shipped}]}"
     },
     "401": {
      "description": "token_required / invalid_token"
     }
    },
    "security": [
     {
      "bearerAuth": []
     }
    ]
   }
  },
  "/auth/login": {
   "post": {
    "summary": "Customer login (session cookie)",
    "requestBody": {
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "email",
         "password"
        ],
        "properties": {
         "email": {
          "type": "string"
         },
         "password": {
          "type": "string"
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "{ok,user_id}"
     },
     "401": {
      "description": "invalid_credentials"
     }
    }
   }
  },
  "/auth/logout": {
   "post": {
    "summary": "Logout",
    "responses": {
     "200": {
      "description": "{ok}"
     }
    }
   }
  },
  "/sitemap/generate": {
   "get": {
    "summary": "Generate the public catalog XML sitemap on explicit request",
    "description": "Enabled and configured in EcsDesk > EcsCart > Sitemap. GET with the separate sitemap secret; no login, customer token or CSRF required. Writes source/sitemap.xml and, for large catalogs, numbered parts. Home, visible products/variants, categories with products or editorial long descriptions, and manufacturers. Optional setting blSitemapBlog adds the blog home and published posts if the shop blog is enabled, with lastmod where a valid modified/publication date is available. No drafts, tag/archive pages, CMS/account/checkout pages. No automatic generation on shop visits or sitemap reads.",
    "security": [
     {
      "sitemapSecret": []
     }
    ],
    "responses": {
     "200": {
      "description": "{ok:true,urls,files,url}; url is the public /sitemap.xml"
     },
     "403": {
      "description": "invalid_secret"
     },
     "404": {
      "description": "sitemap_disabled"
     },
     "405": {
      "description": "method_not_allowed; GET only"
     },
     "409": {
      "description": "sitemap_busy; another generation or settings change is in progress"
     },
     "500": {
      "description": "sitemap_generation_failed; previous sitemap retained"
     }
    }
   }
  },
  "/session": {
   "get": {
    "summary": "Explicitly start/resume a session and obtain its CSRF token. Keep the PHPSESSID cookie.",
    "responses": {
     "200": {
      "description": "{csrf,ui:{chat_open,chat_hint_dismissed,theme}}"
     }
    }
   }
  },
  "/session/ui": {
   "get": {
    "summary": "Read effective UI state without creating a session. URL-mode preferences ignore stored session preferences.",
    "responses": {
     "200": {
      "description": "{ui:{chat_open,chat_hint_dismissed,theme},csrf}; defaults and empty csrf without a session. theme is light or dark. URL preferences: theme=light|dark, chat=open|closed, hint=seen. chat_hint_dismissed includes already displayed hints."
     }
    }
   },
   "post": {
    "summary": "Persist an explicit UI action only for fields configured for session storage. All fields default to URL storage. Same-origin headers or X-EC-CSRF required. Does not send chat messages.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "minProperties": 1,
        "additionalProperties": false,
        "properties": {
         "chat_open": {
          "type": "boolean"
         },
         "chat_hint_dismissed": {
          "type": "boolean"
         },
         "theme": {
          "type": "string",
          "enum": [
           "light",
           "dark"
          ]
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "{ui,csrf}. Creates PHPSESSID only if needed."
     },
     "400": {
      "description": "invalid_ui_state or ui_storage_mode (field configured for URL storage); no new session is created"
     },
     "403": {
      "description": "csrf_failed"
     }
    }
   }
  },
  "/chat/conversation": {
   "delete": {
    "summary": "Withdraw permission for further messages and delete the authenticated local conversation, messages and approvals. Other conversations are untouched. Does not promise deletion at an external provider; operator must handle those requests.",
    "security": [
     {
      "chatToken": []
     },
     {
      "sessionCookie": []
     }
    ],
    "responses": {
     "200": {
      "description": "{data:{deleted:true}}"
     },
     "404": {
      "description": "CONVERSATION_NOT_FOUND"
     }
    }
   }
  }
 },
 "components": {
  "schemas": {
   "Product": {
    "type": "object",
    "properties": {
     "id": {
      "type": "string"
     },
     "sku": {
      "type": "string"
     },
     "title": {
      "type": "string"
     },
     "price": {
      "type": "number",
      "description": "gross price incl. VAT (base/list price, undiscounted)"
     },
     "price_net": {
      "type": "number"
     },
     "vat_rate": {
      "type": "number"
     },
     "effective_price": {
      "type": "number",
      "description": "effective gross price after tier/customer-group/item discounts in the caller's context"
     },
     "effective_price_net": {
      "type": "number"
     },
     "display_price": {
      "type": "number",
      "description": "effective price in the storefront display unit (net when display_net is true)"
     },
     "currency": {
      "type": "string"
     },
     "stock": {
      "type": "number",
      "description": "may be negative while available stays true (OXSTOCKFLAG semantics)"
     },
     "stock_flag": {
      "type": "integer"
     },
     "stock_text": {
      "type": "string",
      "description": "storefront stock wording (e.g. delivery time / out-of-stock note)"
     },
     "display_net": {
      "type": "boolean",
      "description": "true when the storefront itself shows net prices (B2B)"
     },
     "available": {
      "type": "boolean"
     },
     "url": {
      "type": "string"
     },
     "image": {
      "type": "string"
     }
    }
   }
  },
  "securitySchemes": {
   "bearerAuth": {
    "type": "http",
    "scheme": "bearer",
    "description": "Customer API token (ectk_...). Customers create tokens in their account under \"API access\" (/account/apitokens). Scope: placing orders + read-only self-service (/token, /token/addresses, /token/orders, /order/{id})."
   },
   "chatToken": {
    "type": "apiKey",
    "in": "header",
    "name": "X-Chat-Token",
    "description": "Visitor conversation token returned by first POST /chat/messages. API clients may supply X-Chat-Token; the storefront instead keeps the token in its PHP session, with no localStorage."
   },
   "botKey": {
    "type": "http",
    "scheme": "bearer",
    "description": "Bot API key (ecsbot_...) created in EcsDesk. Hash stored server-side; optional IP allowlist. Endpoints answer 423 CHAT_CLOSED outside opening hours and 423 CHAT_DISABLED when the master switch is off."
   },
   "sitemapSecret": {
    "type": "apiKey",
    "in": "query",
    "name": "secret",
    "description": "Separate operator secret (32-128 URL-safe characters), configured in EcsDesk > EcsCart > Sitemap. Never publish the generation link; the generated sitemap itself is public."
   },
   "sessionCookie": {
    "type": "apiKey",
    "in": "cookie",
    "name": "PHPSESSID",
    "description": "Existing storefront session; chat tokens and UI preferences are kept server-side."
   }
  }
 }
}
