{"openapi":"3.1.0","info":{"title":"Kettle & Loom concierge API","version":"0.1.0","summary":"The JSON endpoints behind a demo shop and its AI product-finder.","description":"Kettle & Loom is a fictional homeware shop built to show a product-finding concierge at work. Nothing ships and no payment is taken. These are the endpoints the storefront itself uses: the streamed concierge, the cart, order status, a health check and the demo admin sign-in. They are public, rate limited per address, and every JSON error uses one envelope. Cart changes are made by the storefront (server actions) and by the concierge, not by a public write endpoint.","license":{"name":"MIT","identifier":"MIT"}},"servers":[{"url":"https://shop.phoenixcodes.org","description":"This deployment"}],"tags":[{"name":"Concierge","description":"Ask the shop a question and get a streamed, tool-grounded answer."},{"name":"Cart","description":"Read the cookie-bound cart."},{"name":"Orders","description":"Look up an order by number."},{"name":"Platform","description":"Health."},{"name":"Admin","description":"The demo admin. One shared password; no accounts."}],"paths":{"/api/concierge":{"post":{"operationId":"askConcierge","summary":"Ask the concierge","tags":["Concierge"],"description":"Sends one shopper message and streams the answer as a Server-Sent Events response in the AI SDK UI message stream format. The concierge calls tools (search, product, size guide, add to cart, cart, order status) and the results arrive as typed data parts alongside the text. Messages are limited to 500 characters, conversations to 60 messages and each address to 20 messages per 10 minutes. A global daily token budget applies: when it is spent the endpoint answers 503 budget_exhausted with the time it resets. The first call sets the kl_cart cookie (httpOnly) that ties the conversation and the cart to one browser; start a conversation by omitting conversationId and continue it by sending the id from the data-meta part.","security":[{},{"cartCookie":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"},"conversationId":{"type":"string","format":"uuid","pattern":"^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"}},"required":["message"]},"example":{"message":"A dutch oven under £120 that works on induction"}}}},"responses":{"200":{"description":"The answer, streamed. Parts are listed in x-stream-parts.","headers":{"Set-Cookie":{"description":"kl_cart: the cart cookie, set when the caller had none.","schema":{"type":"string"}},"X-Request-Id":{"description":"Identifies the request in the server logs. Quote it when reporting a problem.","schema":{"type":"string"}},"Cache-Control":{"description":"Always no-store: these responses are per visitor and per moment.","schema":{"type":"string","const":"no-store"}}},"content":{"text/event-stream":{"schema":{"type":"string"},"example":"data: {\"type\":\"start\",\"messageId\":\"m1\"}\n\ndata: {\"type\":\"data-activity\",\"data\":{\"tool\":\"search_products\",\"label\":\"Searching the catalogue\"},\"transient\":true}\n\ndata: {\"type\":\"data-products\",\"data\":{\"products\":[{\"id\":\"cw-ember-dutch-oven\",\"name\":\"Ember Dutch Oven\",\"price\":\"£99–£149\"}],\"query\":\"dutch oven, up to £120, induction-safe\"}}\n\ndata: {\"type\":\"text-start\",\"id\":\"t1\"}\n\ndata: {\"type\":\"text-delta\",\"id\":\"t1\",\"delta\":\"Best match: the Ember Dutch Oven, \"}\n\ndata: {\"type\":\"text-delta\",\"id\":\"t1\",\"delta\":\"which works on induction.\"}\n\ndata: {\"type\":\"text-end\",\"id\":\"t1\"}\n\ndata: {\"type\":\"data-meta\",\"data\":{\"conversationId\":\"6c1a0c1e-5f0e-4d9e-9a57-2f1d3c6d9b10\",\"outcome\":\"answered\"}}\n\ndata: {\"type\":\"finish\"}\n\ndata: [DONE]\n"}}},"400":{"description":"The body is not JSON, or the message is empty, too long or malformed (invalid_request).","headers":{"X-Request-Id":{"description":"Identifies the request in the server logs. Quote it when reporting a problem.","schema":{"type":"string"}},"Cache-Control":{"description":"Always no-store: these responses are per visitor and per moment.","schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The conversation has reached its message limit (conversation_full). Start a new one.","headers":{"X-Request-Id":{"description":"Identifies the request in the server logs. Quote it when reporting a problem.","schema":{"type":"string"}},"Cache-Control":{"description":"Always no-store: these responses are per visitor and per moment.","schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"The request body is too large (invalid_request).","headers":{"X-Request-Id":{"description":"Identifies the request in the server logs. Quote it when reporting a problem.","schema":{"type":"string"}},"Cache-Control":{"description":"Always no-store: these responses are per visitor and per moment.","schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many messages from this address (rate_limited).","headers":{"X-Request-Id":{"description":"Identifies the request in the server logs. Quote it when reporting a problem.","schema":{"type":"string"}},"Cache-Control":{"description":"Always no-store: these responses are per visitor and per moment.","schema":{"type":"string","const":"no-store"}},"Retry-After":{"description":"Seconds to wait before trying again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The daily token budget is spent (budget_exhausted, with resetsAt) or no model could answer (upstream_unavailable).","headers":{"X-Request-Id":{"description":"Identifies the request in the server logs. Quote it when reporting a problem.","schema":{"type":"string"}},"Cache-Control":{"description":"Always no-store: these responses are per visitor and per moment.","schema":{"type":"string","const":"no-store"}},"Retry-After":{"description":"Seconds to wait before trying again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-stream-parts":[{"type":"start","description":"The stream opens. Carries the message id."},{"type":"data-activity","description":"Transient. What the concierge is doing right now, such as \"Searching the catalogue\". Never stored."},{"type":"data-products","description":"Product cards from a search: the products, which variants matched, and what was searched for. Includes a note when the shop does not sell the thing."},{"type":"data-specs","description":"A size or spec table for one product, from the catalogue."},{"type":"data-cart","description":"The outcome of an add to cart: added, needs_confirmation (quantities above 3), or refused, with the cart afterwards."},{"type":"data-order","description":"An order card: status and timeline."},{"type":"text-start","description":"The answer text begins."},{"type":"text-delta","description":"A piece of the answer text. Concatenate the deltas."},{"type":"text-end","description":"The answer text is complete."},{"type":"data-spec-source","description":"The footnote \"from the product spec\", built by the server from the spec values the answer used. Only when the answer stated a spec."},{"type":"data-meta","description":"Conversation id, message id, provider, model, token counts, latency, outcome and prompt version. Send the conversation id back to continue the conversation."},{"type":"error","description":"The answer failed after the stream began. The text is safe to show."},{"type":"finish","description":"The stream is complete. The connection then sends [DONE] and closes."}]}},"/api/cart":{"get":{"operationId":"getCart","summary":"Read the cart","tags":["Cart"],"description":"The cart for the kl_cart cookie, priced against live stock. A caller with no cookie gets an empty cart with a null id. Amounts are pence in GBP.","security":[{},{"cartCookie":[]}],"responses":{"200":{"description":"The cart.","headers":{"Cache-Control":{"description":"Always no-store: these responses are per visitor and per moment.","schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Cart"},"example":{"id":null,"lines":[],"itemCount":0,"subtotalPence":0,"shippingPence":0,"totalPence":0,"freeShippingGapPence":0}}}},"503":{"description":"The cart could not be read (upstream_unavailable).","headers":{"X-Request-Id":{"description":"Identifies the request in the server logs. Quote it when reporting a problem.","schema":{"type":"string"}},"Cache-Control":{"description":"Always no-store: these responses are per visitor and per moment.","schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/orders/{number}":{"get":{"operationId":"getOrder","summary":"Look up an order","tags":["Orders"],"description":"Status, items and timeline for one order. The order number is the only credential, so lookups are limited to 30 per 10 minutes per address and the response never says who the order is for. Try KL-2041 (on its way).","parameters":[{"name":"number","in":"path","required":true,"description":"An order number such as KL-2041. Case does not matter.","schema":{"type":"string","pattern":"^[Kk][Ll]-?\\d{4,7}$","examples":["KL-2041"]}}],"responses":{"200":{"description":"The order.","headers":{"Cache-Control":{"description":"Always no-store: these responses are per visitor and per moment.","schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Order"},"example":{"number":"KL-2041","status":"shipped","placedAt":"2026-10-19T09:12:00.000Z","carrier":"Northline Couriers","trackingRef":"NL4417209","etaDate":"2026-10-23","demo":false,"items":[{"productSlug":"ember-dutch-oven","name":"Ember Dutch Oven","variantLabel":"24 cm · Forest","sku":"K001-24-FOR","unitPence":11900,"qty":1}],"currency":"GBP","subtotalPence":11900,"shippingPence":0,"totalPence":11900,"timeline":[{"key":"placed","title":"Order placed","detail":null,"location":null,"at":"2026-10-19T09:12:00.000Z","state":"done"},{"key":"shipped","title":"Collected by courier","detail":null,"location":"Leeds depot","at":"2026-10-20T14:30:00.000Z","state":"current"},{"key":"out_for_delivery","title":"Out for delivery","detail":null,"location":null,"at":null,"state":"upcoming"},{"key":"delivered","title":"Delivered","detail":null,"location":null,"at":null,"state":"upcoming"}]}}}},"400":{"description":"Not an order number (invalid_request).","headers":{"X-Request-Id":{"description":"Identifies the request in the server logs. Quote it when reporting a problem.","schema":{"type":"string"}},"Cache-Control":{"description":"Always no-store: these responses are per visitor and per moment.","schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such order (not_found).","headers":{"X-Request-Id":{"description":"Identifies the request in the server logs. Quote it when reporting a problem.","schema":{"type":"string"}},"Cache-Control":{"description":"Always no-store: these responses are per visitor and per moment.","schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many lookups from this address (rate_limited).","headers":{"X-Request-Id":{"description":"Identifies the request in the server logs. Quote it when reporting a problem.","schema":{"type":"string"}},"Cache-Control":{"description":"Always no-store: these responses are per visitor and per moment.","schema":{"type":"string","const":"no-store"}},"Retry-After":{"description":"Seconds to wait before trying again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The order could not be read (upstream_unavailable).","headers":{"X-Request-Id":{"description":"Identifies the request in the server logs. Quote it when reporting a problem.","schema":{"type":"string"}},"Cache-Control":{"description":"Always no-store: these responses are per visitor and per moment.","schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/health":{"get":{"operationId":"getHealth","summary":"Health check","tags":["Platform"],"description":"Liveness plus one catalogue query. 200 when the database answers, 503 when it does not.","responses":{"200":{"description":"Healthy.","headers":{"Cache-Control":{"description":"Always no-store: these responses are per visitor and per moment.","schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Health"},"example":{"status":"ok","catalogue":{"source":"postgres","products":80,"variants":332},"latencyMs":41}}}},"503":{"description":"The database cannot be reached.","headers":{"Cache-Control":{"description":"Always no-store: these responses are per visitor and per moment.","schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Degraded"},"example":{"status":"degraded","catalogue":null}}}}}}},"/api/admin/login":{"post":{"operationId":"adminLogin","summary":"Sign in to the demo admin","tags":["Admin"],"description":"Checks the shared demo password and sets the kl_admin session cookie (httpOnly, SameSite=Strict, scoped to /admin, 8 hours). Every attempt counts against 5 per 10 minutes per address, right or wrong. Cross-site posts are refused. The admin page posts a form here and follows the 303; send JSON to get JSON. While ADMIN_PASSWORD is unset on the deployment the endpoint answers 404.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminLogin"},"example":{"password":"the demo password"}},"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/AdminLogin"}}}},"responses":{"200":{"description":"Signed in (JSON request). The session cookie is set.","headers":{"Set-Cookie":{"description":"kl_admin: signed, httpOnly, SameSite=Strict, Path=/admin.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminLoginResult"},"example":{"ok":true,"expiresInSeconds":28800}}}},"303":{"description":"Form request: redirects to /admin/concierge, with the session cookie when the password was right, or with ?login=wrong, ?login=missing or ?login=limited when it was not.","headers":{"Location":{"description":"Where to go next.","schema":{"type":"string"}}}},"400":{"description":"The body is missing the password (invalid_request).","headers":{"X-Request-Id":{"description":"Identifies the request in the server logs. Quote it when reporting a problem.","schema":{"type":"string"}},"Cache-Control":{"description":"Always no-store: these responses are per visitor and per moment.","schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Wrong password (unauthorized).","headers":{"X-Request-Id":{"description":"Identifies the request in the server logs. Quote it when reporting a problem.","schema":{"type":"string"}},"Cache-Control":{"description":"Always no-store: these responses are per visitor and per moment.","schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The request came from another site (forbidden).","headers":{"X-Request-Id":{"description":"Identifies the request in the server logs. Quote it when reporting a problem.","schema":{"type":"string"}},"Cache-Control":{"description":"Always no-store: these responses are per visitor and per moment.","schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The admin is switched off on this deployment (not_found).","headers":{"X-Request-Id":{"description":"Identifies the request in the server logs. Quote it when reporting a problem.","schema":{"type":"string"}},"Cache-Control":{"description":"Always no-store: these responses are per visitor and per moment.","schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"413":{"description":"The request body is too large (invalid_request).","headers":{"X-Request-Id":{"description":"Identifies the request in the server logs. Quote it when reporting a problem.","schema":{"type":"string"}},"Cache-Control":{"description":"Always no-store: these responses are per visitor and per moment.","schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many attempts from this address (rate_limited).","headers":{"X-Request-Id":{"description":"Identifies the request in the server logs. Quote it when reporting a problem.","schema":{"type":"string"}},"Cache-Control":{"description":"Always no-store: these responses are per visitor and per moment.","schema":{"type":"string","const":"no-store"}},"Retry-After":{"description":"Seconds to wait before trying again.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/admin/logout":{"post":{"operationId":"adminLogout","summary":"Sign out of the demo admin","tags":["Admin"],"description":"Expires the kl_admin cookie and redirects to the admin sign-in page. Cross-site posts are refused.","responses":{"303":{"description":"Signed out. Redirects to /admin/concierge.","headers":{"Location":{"description":"Where to go next.","schema":{"type":"string"}},"Set-Cookie":{"description":"kl_admin, expired.","schema":{"type":"string"}}}},"403":{"description":"The request came from another site (forbidden).","headers":{"X-Request-Id":{"description":"Identifies the request in the server logs. Quote it when reporting a problem.","schema":{"type":"string"}},"Cache-Control":{"description":"Always no-store: these responses are per visitor and per moment.","schema":{"type":"string","const":"no-store"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"schemas":{"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["invalid_request","rate_limited","budget_exhausted","conversation_full","upstream_unavailable","unauthorized","forbidden","not_found","internal_error"],"description":"Stable, machine-readable. Branch on this, not on message."},"message":{"type":"string","description":"Plain English, safe to show to a person."},"requestId":{"type":"string","description":"Quote this when reporting a problem; it is in the server logs."},"retryAfterSeconds":{"description":"Present on 429: seconds until a retry can succeed. Also sent as Retry-After.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"resetsAt":{"description":"Present on budget_exhausted: when the daily token budget resets (UTC).","type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"}},"required":["code","message","requestId"],"additionalProperties":false}},"required":["error"],"additionalProperties":false,"description":"The error envelope every JSON endpoint uses."},"Cart":{"type":"object","properties":{"id":{"description":"Null until the first item is added.","type":["string","null"]},"lines":{"type":"array","items":{"type":"object","properties":{"variantId":{"type":"string"},"productSlug":{"type":"string"},"productName":{"type":"string"},"variantLabel":{"type":"string","description":"For example \"24 cm · Forest\"."},"sku":{"type":"string"},"unitPence":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"qty":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"What will be bought: the requested quantity, trimmed to stock."},"requestedQty":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"What was asked for before trimming to stock."},"stock":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"maxQty":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The most the quantity stepper offers: 10, or what is in stock."},"linePence":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"art":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{},"description":"Illustration parameters for the storefront. Not a stable part of the API."},"body":{"type":"string","description":"Illustration body colour for the storefront. Not a stable part of the API."},"issue":{"anyOf":[{"type":"string","enum":["sold_out","reduced"]},{"type":"null"}],"description":"Why the line cannot be bought as it stands, if it cannot."}},"required":["variantId","productSlug","productName","variantLabel","sku","unitPence","qty","requestedQty","stock","maxQty","linePence","art","body","issue"],"additionalProperties":false}},"itemCount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"subtotalPence":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"shippingPence":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"totalPence":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"freeShippingGapPence":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Pence to add for free delivery; 0 when already free or the cart is empty."}},"required":["id","lines","itemCount","subtotalPence","shippingPence","totalPence","freeShippingGapPence"],"additionalProperties":false,"description":"A cart priced against live stock. Amounts are pence (GBP)."},"Order":{"type":"object","properties":{"number":{"type":"string","description":"For example KL-2041."},"status":{"type":"string","enum":["placed","packed","shipped","out_for_delivery","delivered","cancelled"]},"placedAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"},"carrier":{"type":["string","null"]},"trackingRef":{"type":["string","null"]},"etaDate":{"anyOf":[{"type":"string","format":"date","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$"},{"type":"null"}],"description":"Expected delivery day; null once delivered or cancelled."},"demo":{"type":"boolean","description":"True for orders placed through the demo checkout."},"items":{"type":"array","items":{"type":"object","properties":{"productSlug":{"type":"string"},"name":{"type":"string"},"variantLabel":{"type":"string"},"sku":{"type":"string"},"unitPence":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"qty":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["productSlug","name","variantLabel","sku","unitPence","qty"],"additionalProperties":false}},"currency":{"type":"string","const":"GBP"},"subtotalPence":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"shippingPence":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"totalPence":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"timeline":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"title":{"type":"string","description":"For example \"Out for delivery\"."},"detail":{"type":["string","null"]},"location":{"type":["string","null"]},"at":{"anyOf":[{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"},{"type":"null"}],"description":"Null for steps that have not happened yet."},"state":{"type":"string","enum":["done","current","upcoming","cancelled"]}},"required":["key","title","detail","location","at","state"],"additionalProperties":false},"description":"What happened, then what comes next."}},"required":["number","status","placedAt","carrier","trackingRef","etaDate","demo","items","currency","subtotalPence","shippingPence","totalPence","timeline"],"additionalProperties":false,"description":"An order. It never includes who it is for: no name, town or postcode."},"Health":{"type":"object","properties":{"status":{"type":"string","const":"ok"},"catalogue":{"type":"object","properties":{"source":{"type":"string","description":"Where the catalogue is read from, e.g. \"postgres\"."},"products":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"variants":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["source","products","variants"],"additionalProperties":false},"latencyMs":{"type":"number","description":"How long the catalogue check took."}},"required":["status","catalogue","latencyMs"],"additionalProperties":false},"Degraded":{"type":"object","properties":{"status":{"type":"string","const":"degraded"},"catalogue":{"type":"null"}},"required":["status","catalogue"],"additionalProperties":false},"AdminLogin":{"type":"object","properties":{"password":{"type":"string","minLength":1,"maxLength":200,"description":"The shared demo password."}},"required":["password"]},"AdminLoginResult":{"type":"object","properties":{"ok":{"type":"boolean","const":true},"expiresInSeconds":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["ok","expiresInSeconds"],"additionalProperties":false}},"securitySchemes":{"cartCookie":{"type":"apiKey","in":"cookie","name":"kl_cart","description":"Set by the first concierge or cart call. It ties a conversation and a cart to one browser; it is httpOnly."}}}}