# EcsCart Storefront Standalone shop frontend on the shared OXID-compatible database. Admin backend: /ecsdesk/ ## For AI agents This shop is designed to be fully usable by autonomous agents. There are two interfaces: ### Catalog sitemap When enabled by the operator, `/sitemap.xml` lists public catalog URLs (home, visible products including variants, categories with products or editorial long descriptions, manufacturers). CMS snippets, legal pages, account and checkout are excluded. With the optional "Blogbeiträge aufnehmen" setting and an enabled shop blog, the blog home and published posts are included; drafts, tag and archive pages are excluded. Blog entries include lastmod when a valid modified/publication date is available. The operator explicitly generates it with `GET /api/sitemap/generate?secret=...`. The secret and complete generation link are managed in EcsDesk > EcsCart > Sitemap. Never guess or publish that secret. The route returns `{ok,urls,files,url}`; 403 = invalid secret, 404 = disabled, 409 = generation already running. Ordinary shop visits and reads of `/sitemap.xml` never regenerate the files. ### 1. Machine API (preferred) Base: `/api/` — JSON in/out. `GET /api` returns an endpoint index. Public reads do not create a session. Stateful operations use the single `PHPSESSID` cookie (keep cookies between requests). `GET /api/session` explicitly starts/resumes a session and returns `{csrf,ui}`. Without a session, `csrf` in discovery/shop/cart responses is empty. Obtain the token from `/api/session` before writing without same-origin headers. `GET /api/session/ui` reads effective UI defaults/state without creating a session. `POST /api/session/ui` saves explicit actions `{chat_open?:bool,chat_hint_dismissed?:bool,theme?:"light"|"dark"}` with the normal write protection, but only for fields configured for session storage. URL-mode fields return `400 ui_storage_mode` without starting a session. The default for language, theme and chat-panel state is URL storage (`lang`, `theme=light|dark`, `chat=open`, `hint=seen`). No cookie or local storage is used for these URL preferences, even if a cart/login session already exists. `hint=seen` means the hint has already been displayed or dismissed. The effective `ui.theme` is always light or dark. Chat handovers from search, suggestions and products default to editable drafts; the operator can enable automatic sending separately for each source. URL state never contains conversation credentials, message text or drafts. - `GET /api/shop` — shop meta (currency, api version) - `GET /api/categories` — all categories, flat list with `id,title,parent_id,url,image` (roots: `parent_id:""`) - `GET /api/categories/{id}/products?page=&per_page=` — products in category **including all descendant categories** (`category_ids` lists the resolved ids); unknown id → `404 category_not_found` - `GET /api/products?q=&page=&per_page=` — product list/search - `GET /api/suggest?q=` — autocomplete suggestions (≥3 chars): `{articles:[{id,sku,title,url,image,price}], categories:[{id,title,url,image}]}`, umlaut- and typo-tolerant - `GET /api/products/{id}` — product detail incl. variants, attributes, images - `GET /api/banners` — active homepage banners `{id,title,description,image,url}` (url resolves to linked article or free link) - `GET /api/countries` — shipping countries - `GET /api/shipping?country={oxcountry-id}&total={gross}` — shipping methods + cost - `GET /api/payments?country={oxcountry-id}&total={gross}&shipset={oxdeliveryset-id}` — payment methods + fees. Filtered per customer context (session or Bearer token): customer-group restrictions (OXID `oxobject2group`), `OXFROMBONI` credit check, amount range and shipping-set linkage all apply; anonymous callers see unrestricted methods only. - `GET /api/totals?ship={oxdeliveryset-id}&pay={oxpayments-id}&country={oxcountry-id}&deldiff=1&delcountry={oxcountry-id}&ustid=` — one authoritative response for options+selection+totals from the single calculation: `tot{goods_net/brut, del_net/brut, pay_net/brut, vat_rows{rate:amount}, net_total, brut_total}` plus top-level `ship`/`pay` option lists (with costs), server-validated `ship_sel`/`pay_sel` and `html` = pre-rendered checkout sum block. What you see here is exactly what a created order stores. Gebühren werden einzeln auf Cent gerundet. Rabatt und Gutschein werden proportional verteilt; `vat_rows` enthält alle Steuersätze einschließlich Nachkommastellen und mehr als zwei Sätzen. - `GET /api/content?loadid={oxloadid}` — CMS fragment `{loadid,title,html}` for modal legal texts (oxagb, oxrightofwithdrawal, oximpressum, …); no page layout - `GET /api/cart` · `POST /api/cart/items {product_id,qty}` · `PUT|DELETE /api/cart/items/{product_id}` · `DELETE /api/cart` Cart payload: `{count,total,total_net,vat{rate:amount},display_net,currency,csrf,ded_html,summary_html,items[]}` (`summary_html` = complete cart summary with checkout labels and totals after deductions, excluding shipping/payment fees; `ded_html` = pre-rendered discount/voucher lines; `csrf` = session token, send as `X-EC-CSRF` on write requests without same-origin headers). Item errors: `400 invalid_product_id|invalid_qty|invalid_json`, `404 product_not_found`, `422 product_not_available` (variant required or not buyable). - `POST /api/order` — create order in one call. **Customer API tokens (recommended for agents):** Customers create an order-only token (`ectk_…`) in their account under *API access* (`/account/apitokens`). Send it as `Authorization: Bearer ectk_…` — no session cookie needed. Token orders run on the token owner's account: billing/delivery addresses come from the stored account only — sending `billing`/`delivery` objects is rejected (`400 token_order_account_addresses`). A stored delivery address may be chosen via `delivery_address_id` (an `oxaddress` id of that customer). The customer configures the limits: max gross total per order (default 1000.00) and max orders per rolling window (default 1 per 24 h). Violations → `422 {"error":"token_limit_sum","total":,"max_order_sum":,"max_orders":,"period_hours":}` or `422 {"error":"token_limit_rate","orders_in_window":,…same fields…}`. Unknown/revoked token → `401 {"error":"invalid_token"}`. Anonymous/session mode (no token): ```json { "items": [{"product_id": "demo_art_007", "qty": 2}], "billing": {"email": "a@b.c", "fname": "Max", "lname": "Muster", "street": "Str.", "streetnr": "1", "zip": "12345", "city": "Stadt", "countryid": ""}, "delivery": { "...same fields, optional..." }, "shipping_id": "oxidstandard", "payment_id": "oxidpayadvance", "remark": "optional" } ``` → `201 {order_id, order_number, total, status, payment_status, payment_url}` (`payment_url` is a PayPal retry link while the order is `NOT_FINISHED`, else `null`). `items[]` is validated up front — any invalid position rejects the whole call with `400 {"error":"invalid_items","items":[{index,product_id,error}]}`; without `items` the session cart is used. `400 invalid_json` on malformed bodies. - `POST /api/auth/login {email,password}` — customer login (session cookie); `POST /api/auth/logout` - `GET /api/order/{order_id}` — order status for logged-in customer or matching Bearer token owner. Full detail: `status` (OXFOLDER), `total`/`total_net`/`total_brut`, `vat{rate:amount}` (rate keys are strings, e.g. `"19"`, `"12.9"`), `shipping_id/title/cost`, `payment_id/title/desc/cost`, `payment_status`, `payment_url` (PayPal retry link while `NOT_FINISHED`, else `null`), `billing`/`delivery` address blocks, `remark`, `paid`/`shipped` (timestamps or `null`), `cancelled`, `items[]` `grand_total_net` enthält die gesamte Nettosumme mit Gebühren; `total_net`/`total_brut` bleiben die Warenwerte nach Abzügen. `vat` enthält alle Steuergruppen; stornierte Positionen werden ausgelassen. - `GET /api/token` — Bearer token self-check: `{label,max_order_sum,max_orders,period_hours,orders_in_window,orders_remaining,last_used}` — call this *before* ordering to learn the limits - `GET /api/token/addresses` — token owner's billing address + selectable delivery addresses (`delivery[].id` → `delivery_address_id`) - `GET /api/token/orders` — orders placed with this token (order_number, total, paid/shipped flags) Errors: `{"error": "code"}` with matching HTTP status. Chat endpoints return `{"error":{"code","message"}}`. #### Visitor chat (`/api/chat/*`) — if enabled The shop may run an AI advisor ("KI-Berater") for visitors. It is controlled by a single master switch in EcsDesk; when off, all `chat/*` endpoints answer `404 CHAT_DISABLED` and no UI is rendered. - `GET /api/chat/config` — `{open,in_grace,next_open_at,hours_text,bot_online,texts,max_length}` - `POST /api/chat/messages {text,client_msg_id,page,context_product_id?,hp}` — creates the conversation on first call and returns `{message,token,duplicate}` (201). API clients may keep the token and send it as `X-Chat-Token` afterwards. The storefront keeps it server-side in the PHP session; no chat token or UI state is persisted in localStorage. `context_product_id` is auto-resolved from `page` when omitted. `423 CHAT_CLOSED` outside opening hours. - `GET /api/chat/messages?after=N` — poll for new messages: `{messages:[{id,sender,body,created_at,cards?}],status,close_reason,typing,chat}`. Bot messages may carry `cards[]` = validated product cards `{id,sku,title,price_html,vat_note,image,url,buyable}`. - `POST /api/chat/hint {hp}` — "visitor is typing" pre-wake (best effort). - `POST /api/chat/close {hp}` — end the conversation. The first message and any changed privacy policy require `privacy:{version,accepted:true,quality:false}`. Fetch `/api/chat/config`, display `privacy.notice`, and make `privacy.more_notice` and provider links available before approval (expandable details are supported). Obtain an explicit affirmative action before sending. `quality` is a separate optional permission for the shop quality archive and must never be preselected. It is not needed to chat. Provider name, privacy URL and description are optional. Hide empty values without placeholders; missing provider fields do not restrict sending. `privacy.ready` remains true for compatibility. 428 = approval missing/stale. Explicit approval remains required with or without provider details. No customer account ID or persistent IP hash is stored in a conversation. Public catalog context only; account/order/checkout paths and URL query/fragment are discarded. Message text is NOT redacted: personal information entered by a visitor is stored and forwarded as message content. Providers are configurable; possible storage and training use are assumed unless the operator explicitly configures otherwise. This configuration and approval flow are not a blanket legal clearance for arbitrary providers. No bot pre-wake occurs before approved conversation use. `DELETE /api/chat/conversation` deletes only the authenticated local conversation and its approvals. It stops local use, but cannot automatically erase an external provider's copies or reverse training. Quality archives default to off. Without optional quality approval only a temporary operational buffer is used (default 24h without activity, configurable 1..168h). A configured quality archive requires visitor approval and a positive retention period. Previous approval periods are not silently extended by later configuration changes. Run `php ecscart/tools/chat_cleanup.php` regularly (e.g. every 15 minutes) using the hosting scheduler; request-triggered cleanup is only a fallback. Bot inbox/history endpoints do not expose legacy conversations without documented approval. #### Bot chat (`/api/bot/*`) — `Authorization: Bearer ecsbot_…` External bot service, keyed by EcsDesk-issued `ecsbot_` keys (hashed at rest, optional IP allowlist). `GET /api/bot/status` and `/api/bot/health` always work; all other bot endpoints answer `423 CHAT_DISABLED` when the master switch is off and `423 CHAT_CLOSED` outside opening hours (with `retry_after`). - `GET /api/bot/health` · `GET /api/bot/status` - `GET /api/bot/articles?search=&article_number=&ean=&parent_id=&lang=&quantity=&limit=&offset=` — visible catalog - `GET /api/bot/articles/{id}?lang=&quantity=` — product detail (OXID or article number) - `GET /api/bot/inbox?after_id=&wait=&limit=` — long-poll new visitor messages; `messages[].context` carries the product the visitor is looking at - `GET /api/bot/conversations?status=&limit=&offset=` - `GET|POST /api/bot/conversations/{id}/messages` — POST `{text,client_msg_id,product_ids[]}`; `product_ids` become product cards (validated against the visible catalog; product links in the text are resolved as fallback) - `POST /api/bot/conversations/{id}/typing|close` · `POST …/mail` (forward transcript) ### 2. HTML storefront Product pages carry schema.org/JSON microdata (Product, Offer). All catalog URLs are SEO paths from `oxseo`. Search: `/search?q=`. Cart: `/cart`. Checkout: `/checkout` (CSRF token field `_csrf`, available from any page's session). ## Notes - Every product payload carries `price` (gross, incl. VAT), `price_net`, `vat_rate`, `effective_price`/`effective_price_net` (after tier/customer-group/item discounts in the caller's context), `display_price` (effective price in the storefront display unit) and `display_net` (true when the storefront itself shows net prices, e.g. B2B). - Write requests in cookie-session mode are CSRF-guarded: send a same-origin `Origin`/`Referer` or `Sec-Fetch-Site` header, or the session `csrf` token as `X-EC-CSRF` header (from `GET /api`, `/api/shop`, `/api/cart`, `POST /api/auth/login`). Bearer-token requests skip this check; an unrecognized Bearer token is a hard `401 invalid_token`, never silently anonymous. - `stock` may be negative while `available` stays true (OXID `OXSTOCKFLAG` semantics, e.g. standard/dropship articles) — `stock_text` carries the storefront wording (also on cart items), `stock_flag` the raw flag; schema.org markup uses `BackOrder` for that state. - IDs are opaque strings from `OXID` columns (letters, digits, dots, underscores — e.g. `oxidstandard`, `demo_art_007`, `a7c40f631fc920687.20179984`). - Full spec: `/openapi.json`