{"openapi":"3.0.3","info":{"title":"ExtraLatte — API de Integración","description":"API OpenCore para conectar el software propio de una cafetería con ExtraLatte por SKU: sincronizar catálogo e inventario, mandar comandas a la cocina y pedir la precuenta de un pedido (requiere el scope printing:write). Guía legible en /integration/guia. Auth: header Authorization: Bearer elk_<client_id>_<secret> (API key del negocio, creada en Configuración → API keys).","version":"1"},"paths":{"/products":{"get":{"summary":"List Products","operationId":"list_products_products_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":100,"title":"Limit"}},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/products/{sku}":{"put":{"summary":"Upsert Product","operationId":"upsert_product_products__sku__put","parameters":[{"name":"sku","in":"path","required":true,"schema":{"type":"string","title":"Sku"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Disable Product","operationId":"disable_product_products__sku__delete","parameters":[{"name":"sku","in":"path","required":true,"schema":{"type":"string","title":"Sku"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/products:batch":{"post":{"summary":"Upsert Products Batch","operationId":"upsert_products_batch_products_batch_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProductBatchIn"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/products/{sku}/options/{option_sku}":{"delete":{"summary":"Delete Product Option","description":"Retire one add-on from a product.\n\nUnlike ``DELETE /products/{sku}`` (which only flips ``active``), an option\nhas no visibility flag, so this really deletes the row — but only while it\nis still safe to: ``OrderItemModifier.option_id`` is a real FK, and erasing\nan add-on that a past order used would take a piece of that sale with it.\nOnce an option has been sold it stays, and the answer is 409.","operationId":"delete_product_option_products__sku__options__option_sku__delete","parameters":[{"name":"sku","in":"path","required":true,"schema":{"type":"string","title":"Sku"}},{"name":"option_sku","in":"path","required":true,"schema":{"type":"string","title":"Option Sku"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/orders":{"post":{"summary":"Create Comanda","operationId":"create_comanda_orders_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"summary":"List Comandas","description":"List the business's orders, newest first, by cursor.\n\nNO line items, on purpose: a list that carried them would invite pulling\n500 complete orders to answer \"which tables are open\". The lines live in\n`GET /orders/{order_id}`.\n\nCursor and not offset. Offset paginates by POSITION, and position moves:\na comanda rung up between page 1 and page 2 pushes every row down one, so\nthe caller sees one order twice and never sees another. The keyset below\npaginates by the sort key of the last row delivered, which does not move —\na new order sorts ABOVE the cursor (it is newer) and is simply not in the\npages still being walked. That is the difference between a report and a\ncafé at peak hour.\n\nEvery filter is a filter WITHIN the business. `table` in particular\nresolves through a business-scoped lookup, so another café's table number\nmatches nothing here and returns an empty list — never that café's orders.","operationId":"list_comandas_orders_get","parameters":[{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"}},{"name":"table","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Table"}},{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Since"}},{"name":"until","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Until"}},{"name":"service_type","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Service Type"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"title":"Limit"}},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/orders/{order_id}/items":{"post":{"summary":"Append Order Items","description":"Add a ROUND to an order that is still open, printing only what is new.\n\nThis is the endpoint whose absence forced the workaround: with no way to\nappend, a waiter who wanted the kitchen to skip what was already dispatched\nhad to open a NEW order per round — and on our side each order is its own\naccount, its own precuenta and its own payment. A table that ordered three\ntimes became three bills. Here the table stays ONE order: the total, the\nprecuenta and the payment cover every round.\n\nWhat the kitchen gets is the new lines alone, with the round in the header\n(\"Mesa 2 · ronda 2\"). ``print_full_order`` prints the whole table instead,\nmarking what is new.\n\nIdempotency, doubled like every other write here:\n\n1. ``Idempotency-Key`` replays the original response verbatim — the ONLY\n   thing that makes a retry safe, because two identical rounds are a real\n   thing (a table can order the same coffee twice) and we will not guess.\n   **Without the header, calling twice adds the lines twice**, on purpose.\n2. The round's comanda has its own ``event_key``\n   (``ronda:<order>:r<n>:<station>:comanda_sector``), so the same round can\n   never be put on paper twice and can never collide with the previous\n   one's.","operationId":"append_order_items_orders__order_id__items_post","parameters":[{"name":"order_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Order Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderItemsIn"}}}},"responses":{"201":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/orders/{order_id}/precuenta":{"post":{"summary":"Print Precuenta","description":"Queue the PRECUENTA (pre-payment bill) of an order at the counter.\n\nNot a boleta: it is the \"what do I owe\" ticket. The server owns every\namount — the caller sends percentages at most, never money.\n\nIdempotency is doubled on purpose (no double prints):\n\n1. ``Idempotency-Key`` replays the original response verbatim.\n2. The job's ``event_key`` (``precuenta:<order>:<split|full>``) means a\n   second request for the same precuenta returns the SAME\n   ``print_job_id`` with ``deduplicated: true`` — even with no header, or\n   a different one. There is no reprint through this API by design.","operationId":"print_precuenta_orders__order_id__precuenta_post","parameters":[{"name":"order_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Order Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PrecuentaIn"}}}},"responses":{"202":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/orders/{order_id}/payment":{"post":{"summary":"Register Payment","description":"Register the payment of an order (and close it) from outside.\n\nSemantics worth stating out loud:\n\n- **Payment is not delivery.** ``close_order`` stamps ``closed_at``, which\n  is what drains the kitchen board (both KDS queries filter\n  ``closed_at IS NULL``). It never writes ``preparation_status =\n  \"delivered\"``: nobody here witnessed a hand-off.\n- **No partial payments.** ``amount`` must be the full outstanding balance.\n  ``close_order`` refuses unpaid orders, so closing only follows a\n  complete payment.\n- **``print_receipt`` defaults to false**: the integrator prints its own\n  slip and two papers is worse than one. This suppresses ONLY the thermal\n  receipt and the customer display — the cash sale still posts to an open\n  drawer, which is why this uses ``suppress_physical_outputs`` and not\n  ``backdated`` (that flag also gates the till by the order's date).\n- **Idempotency is doubled**: ``Idempotency-Key`` replays the original\n  response, and underneath it ``process_payment``/``pay_split`` take a\n  ``SELECT ... FOR UPDATE`` on the order and answer 409 to a second charge\n  even with no header at all.\n\nCaveat for multi-register tenants: a cash sale posts to the NEWEST open\ndrawer session at the location, and a headless integrator carries no\ncashier — so with two cashiers open at once the sale may land in a session\nthe other one never sees. Pre-existing behaviour, documented here because\nthis endpoint makes it easier to hit.","operationId":"register_payment_orders__order_id__payment_post","parameters":[{"name":"order_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Order Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/orders/{order_id}":{"get":{"summary":"Get Comanda","operationId":"get_comanda_orders__order_id__get","parameters":[{"name":"order_id","in":"path","required":true,"schema":{"type":"string","format":"uuid","title":"Order Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/ingredients":{"get":{"summary":"List Ingredients","operationId":"list_ingredients_ingredients_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":100,"title":"Limit"}},{"name":"cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Cursor"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/ingredients/{sku}":{"put":{"summary":"Upsert Ingredient","operationId":"upsert_ingredient_ingredients__sku__put","parameters":[{"name":"sku","in":"path","required":true,"schema":{"type":"string","title":"Sku"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngredientIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/ingredients:batch":{"post":{"summary":"Upsert Ingredients Batch","operationId":"upsert_ingredients_batch_ingredients_batch_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IngredientBatchIn"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/ingredients/{sku}/stock":{"put":{"summary":"Set Stock","operationId":"set_stock_ingredients__sku__stock_put","parameters":[{"name":"sku","in":"path","required":true,"schema":{"type":"string","title":"Sku"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StockIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/ingredients/{sku}/stock/adjust":{"post":{"summary":"Adjust Stock","operationId":"adjust_stock_ingredients__sku__stock_adjust_post","parameters":[{"name":"sku","in":"path","required":true,"schema":{"type":"string","title":"Sku"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StockAdjustIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/payment-methods":{"get":{"summary":"List Payment Methods","description":"The business's active payment methods — the values `method` accepts.\n\nOnly active ones: a deactivated method is exactly what\n`POST /orders/{order_id}/payment` refuses, so listing it would be an\ninvitation to a 422.\n\nThere is deliberately NO `is_default` field: ExtraLatte has no\n\"predeterminado\" flag on a payment method — no column, no panel switch —\nand a field with that name would be read as a setting the café made. It\nwould have to be derived (the single cash method, say), and a derivation\npublished under that name teaches a caller something false: a café whose\nreal default is Débito would be told it is Efectivo. The honest surface is\nthe list, and a caller that must name `method` explicitly. If a real\ndefault is ever wanted it is a column, a panel switch and a migration.","operationId":"list_payment_methods_payment_methods_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/tables":{"get":{"summary":"List Tables","description":"The business's tables, and which order is sitting on each one.\n\n`current_order_id` is the field that turns \"cobra la mesa 5\" into a call\nthat can be made: read the table, get the order, charge it.\n\n`status` is DERIVED from the orders, not read from `tables.status`. That\ncolumn is maintained by the POS and the QR flow and is known to drift —\nthe KDS auto-archive job exists in part because tables were found stuck\n\"occupied\" for months after nothing closed their order. Deriving it keeps\n`status` and `current_order_id` from ever contradicting each other, which\nis the pair an agent actually reasons about. `reserved` and `blocked` are\npassed through from the stored column: they are decisions a person made,\nnot facts about orders, so there is nothing to derive them from.","operationId":"list_tables_tables_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/ping":{"get":{"summary":"Integration Ping","description":"Whoami / health check — confirms the API key and shows its scopes.","operationId":"integration_ping_ping_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}}},"components":{"schemas":{"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"IngredientBatchIn":{"properties":{"items":{"items":{"$ref":"#/components/schemas/IngredientWithSku"},"type":"array","title":"Items"}},"type":"object","required":["items"],"title":"IngredientBatchIn"},"IngredientIn":{"properties":{"name":{"type":"string","title":"Name"},"unit":{"type":"string","title":"Unit"},"cost":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Cost"},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"}},"type":"object","required":["name","unit"],"title":"IngredientIn"},"IngredientWithSku":{"properties":{"name":{"type":"string","title":"Name"},"unit":{"type":"string","title":"Unit"},"cost":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Cost"},"category":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Category"},"sku":{"type":"string","title":"Sku"}},"type":"object","required":["name","unit","sku"],"title":"IngredientWithSku"},"OptionGroupIn":{"properties":{"name":{"type":"string","title":"Name"},"min_selected":{"type":"integer","title":"Min Selected","default":0},"max_selected":{"type":"integer","title":"Max Selected","default":1},"is_required":{"type":"boolean","title":"Is Required","default":false},"options":{"items":{"$ref":"#/components/schemas/OptionIn"},"type":"array","title":"Options","default":[]}},"type":"object","required":["name"],"title":"OptionGroupIn","description":"A set of add-ons the customer picks from, e.g. \"Leche\"."},"OptionIn":{"properties":{"sku":{"type":"string","title":"Sku"},"name":{"type":"string","title":"Name"},"price":{"type":"number","title":"Price","default":0.0},"sort_order":{"type":"integer","title":"Sort Order","default":0}},"type":"object","required":["sku","name"],"title":"OptionIn","description":"One paid add-on. ``price`` is the surcharge added to the product."},"OrderIn":{"properties":{"service_type":{"type":"string","title":"Service Type","default":"takeaway"},"table":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Table"},"items":{"items":{"$ref":"#/components/schemas/OrderItemIn"},"type":"array","title":"Items"},"customer_name":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Customer Name"},"mark_paid":{"type":"boolean","title":"Mark Paid","default":false}},"type":"object","required":["items"],"title":"OrderIn"},"OrderItemIn":{"properties":{"sku":{"type":"string","title":"Sku"},"quantity":{"type":"integer","title":"Quantity"},"notes":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Notes"},"modifiers":{"items":{"type":"string"},"type":"array","title":"Modifiers","default":[]}},"type":"object","required":["sku","quantity"],"title":"OrderItemIn"},"OrderItemsIn":{"properties":{"items":{"items":{"$ref":"#/components/schemas/OrderItemIn"},"type":"array","title":"Items"},"print_full_order":{"type":"boolean","title":"Print Full Order","default":false}},"type":"object","required":["items"],"title":"OrderItemsIn","description":"Extra lines for an order that is already open (a new round)."},"PaymentIn":{"properties":{"method":{"type":"string","title":"Method"},"amount":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Amount"},"tip":{"type":"number","title":"Tip","default":0.0},"split_number":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Split Number"},"card_token":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Card Token"},"close_order":{"type":"boolean","title":"Close Order","default":true},"print_receipt":{"type":"boolean","title":"Print Receipt","default":false}},"type":"object","required":["method"],"title":"PaymentIn","description":"Field names are English; ``method`` VALUES are the tenant's own data.\n\nThat distinction is the whole naming rule of this API: keys and error codes\nare English (an integrator codes against them), while a payment method is\ncalled whatever the café called it — \"Efectivo\", \"Débito\"."},"PrecuentaIn":{"properties":{"split_number":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Split Number"},"tip_suggested_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Tip Suggested Pct"},"discount_pct":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Discount Pct"},"note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Note"}},"type":"object","title":"PrecuentaIn","description":"Everything is optional: the plain call prints the whole order's bill."},"ProductBatchIn":{"properties":{"items":{"items":{"$ref":"#/components/schemas/ProductWithSku"},"type":"array","title":"Items"}},"type":"object","required":["items"],"title":"ProductBatchIn"},"ProductIn":{"properties":{"name":{"type":"string","title":"Name"},"price":{"type":"number","title":"Price"},"category":{"type":"string","title":"Category"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"active":{"type":"boolean","title":"Active","default":true},"option_groups":{"anyOf":[{"items":{"$ref":"#/components/schemas/OptionGroupIn"},"type":"array"},{"type":"null"}],"title":"Option Groups"}},"type":"object","required":["name","price","category"],"title":"ProductIn"},"ProductWithSku":{"properties":{"name":{"type":"string","title":"Name"},"price":{"type":"number","title":"Price"},"category":{"type":"string","title":"Category"},"description":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Description"},"active":{"type":"boolean","title":"Active","default":true},"option_groups":{"anyOf":[{"items":{"$ref":"#/components/schemas/OptionGroupIn"},"type":"array"},{"type":"null"}],"title":"Option Groups"},"sku":{"type":"string","title":"Sku"}},"type":"object","required":["name","price","category","sku"],"title":"ProductWithSku"},"StockAdjustIn":{"properties":{"location":{"type":"string","title":"Location"},"delta":{"type":"number","title":"Delta"},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason"}},"type":"object","required":["location","delta"],"title":"StockAdjustIn"},"StockIn":{"properties":{"location":{"type":"string","title":"Location"},"quantity":{"type":"number","title":"Quantity"},"min_quantity":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Min Quantity"}},"type":"object","required":["location","quantity"],"title":"StockIn"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"}},"securitySchemes":{"ApiKeyBearer":{"type":"http","scheme":"bearer","description":"API key del negocio: elk_<client_id>_<secret>."}}},"security":[{"ApiKeyBearer":[]}],"servers":[{"url":"https://extralatte.cl/api/v1/integration","description":"Este host"},{"url":"/api/v1/integration","description":"Relativo"}]}