Culizen Sales API

Push POS sales into Culizen. Auth, JSON & XML payloads, recipe mapping, deduplication, error codes.

Version: 1.1 Last updated: 2026-09-01 Contact: support@culizen.gr

The Culizen Sales API lets a POS (point-of-sale) or order-management system push end-of-day sales data into a Culizen restaurant account. Sales arrive as a daily Z-report batch and are reconciled against the restaurant's recipe catalog so margin, food-cost, drain analysis, and inventory deductions stay accurate.

This document is the canonical partner-facing reference. It describes every endpoint, both authentication modes, the JSON and XML payload formats, pricing semantics, error handling, and idempotency. See the Changelog at the bottom for what's changed between versions.


Contents

  1. At a glance
  2. Quick start
  3. Authentication
  4. Endpoints
  5. Sales payload — JSON
  6. Sales payload — XML
  7. Item field reference
  8. Sales classifications
  9. Pricing and VAT
  10. Recipe mapping
  11. Idempotency
  12. Rate limits
  13. Error codes
  14. Versioning
  15. Changelog

At a glance

Property Value
Base URL https://api.culizen.gr
Path prefix /api/v1/pos
Content types accepted application/json, application/xml
Response content type application/json (always)
Rate limit 1,200 requests per minute per integration (≈ 20/sec)
Auth Bearer API key (two modes — see Authentication)
Submission cadence Once daily at shift close (Z-report). Partial-window updates also supported — see mode.
Idempotency By batch_id and by payload hash
Service channels dineIn, takeaway, inHouseDelivery, platformDelivery
Pricing Gross (VAT-inclusive) — the price the customer pays at the till

Quick start

  1. Obtain an API key. Two paths:

    • Restaurant mode (recommended for most POS vendors): the restaurant owner opens Settings → Integrations in Culizen, picks "Custom (Culizen Sales API)", and generates a key. They paste this key into your POS configuration. The key starts with clzn_.
      • Note: Generating and managing self-serve API keys requires the Professional (€119/mo) or Executive tier (see ADR 0033). Essential and Starter tier accounts have read-only access to view integration settings.
      • Pre-built connectors: If your restaurant uses Delivery Manager or W-POS, native one-click integrations are available in Settings → Integrations without needing custom API scripts.
    • Partner mode: Culizen issues a long-lived partner key to your company (starts with clzn_partner_). Each restaurant's internal ID on your side is linked to a Culizen account manually by Culizen support after contract signing.
    • Pick the right mode for your situation in Authentication.
  2. (Optional) Fetch the recipe catalog with GET /api/v1/pos/recipes so you can pre-map your internal item codes (PLU/SKU) to Culizen recipe_ids. If you skip this, Culizen will attempt to auto-match by name and price on the first batch.

  3. Push the day's sales at shift close with POST /api/v1/pos/sales. Send one batch per (restaurant, sales_date) per register; Culizen returns a batch_id and a processing summary.

  4. Check status (optional) with GET /api/v1/pos/status/{batch_id} — useful when you want to surface "synced ✓" to the restaurant from your own UI.

# Verify connectivity (no auth)
curl https://api.culizen.gr/api/v1/pos/health

# Push a minimal sales batch
curl -X POST https://api.culizen.gr/api/v1/pos/sales \
  -H "Authorization: Bearer clzn_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "batch_id": "Z-2026-03-04-001",
    "sales_date": "2026-03-04",
    "items": [
      { "pos_item_code": "PLU-001", "pos_item_name": "Margherita", "quantity": 12, "service_type": "dineIn", "unit_price": 9.61 }
    ]
  }'

Authentication

All POS endpoints (except GET /pos/health) require a Bearer token in the Authorization header. There are two authentication modes, distinguished by the key prefix.

Restaurant mode

Used by single-restaurant integrations and by POS vendors who want each restaurant to manage its own credential.

  • Key prefix: clzn_
  • Issuance: the restaurant generates the key from Settings → Integrations in Culizen and pastes it into your POS. One key per restaurant; the restaurant can revoke and re-issue at any time.
  • Headers:
    Authorization: Bearer clzn_<key>
    
  • No X-Restaurant-Id header needed — the key already identifies the restaurant.

Partner mode

Used by POS vendors who want one credential to serve many restaurants.

  • Key prefix: clzn_partner_
  • Issuance: Culizen issues a single partner key to your company, manually, after a contract. Treat it as a long-lived secret.
  • Per-restaurant linking: for each restaurant you onboard, you give Culizen your internal restaurant identifier; Culizen support links it to the corresponding Culizen account. From then on you pass that identifier in X-Restaurant-Id on every request.
  • Headers:
    Authorization: Bearer clzn_partner_<key>
    X-Restaurant-Id: <your_internal_restaurant_id>
    
  • X-Restaurant-Id may alternatively be supplied as restaurant_id in the JSON body.

Choosing a mode

Scenario Recommended mode
One-off integration for a single restaurant Restaurant
Multi-tenant POS where each restaurant is a separate Culizen account Restaurant (each restaurant pastes their own key)
Multi-tenant POS, you prefer a single credential and centralized restaurant management on your side Partner
You want to centrally enable/disable a restaurant from your end Partner

If you're undecided, default to restaurant mode. It needs no contract work; restaurants self-serve in minutes.


Endpoints

POST /api/v1/pos/sales

Push a sales batch. Accepts JSON or XML.

Headers

Header Required Notes
Authorization Yes Bearer clzn_<key> or Bearer clzn_partner_<key>
X-Restaurant-Id Yes (partner mode only) External restaurant identifier from your POS
Content-Type Yes application/json or application/xml

Body: see Sales payload — JSON or Sales payload — XML.

Response (200) — the response is always JSON regardless of request format:

{
  "batch_id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "completed",
  "summary": {
    "regular_sales": 25,
    "discounted_sales": 3,
    "comps": 1,
    "voids_with_waste": 1,
    "voids_skipped": 1,
    "total_revenue": 168.0,
    "total_discount": 7.5,
    "total_food_loss_value": 20.5
  },
  "unmapped_items": []
}

Status values

Value Meaning
completed All items processed; every line mapped to a recipe.
partial Batch accepted, but some POS item codes had no recipe mapping. The items are stored as unmapped and can be linked later — either by the restaurant in Settings → POS Mappings, or automatically on the next batch via the auto-match flow.
already_processed Idempotent return — this batch_id (or an identical payload hash) was processed before.

unmapped_items is an array of { pos_item_code, pos_item_name } for codes Culizen could not auto-match. Empty array on completed. Useful to surface a "needs attention" badge in your own UI.

GET /api/v1/pos/status/{batch_id}

Fetch the current state of a previously submitted batch.

Response (200)

{
  "batch_id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "completed",
  "sales_date": "2026-03-08",
  "processed_items": 45,
  "total_items": 50,
  "unmapped_items": 5,
  "total_revenue": 2500.0,
  "total_discount": 150.0,
  "total_comps": 3,
  "total_voids": 2,
  "errors": null,
  "created_at": "2026-03-08T23:32:11.183Z",
  "completed_at": "2026-03-08T23:32:14.917Z"
}

GET /api/v1/pos/recipes

Returns the restaurant's full main-recipe catalog so your POS can pre-map its internal codes to Culizen recipe_ids. Returns every main recipe (no menu-active filter), so codes can be pre-mapped before the chef actually publishes a menu.

Response (200)

{
  "recipes": [
    {
      "id": "5b34e1f0-0d4c-4f1a-8a3d-7f2c6e0d8c11",
      "name": "Carbonara",
      "description": "Spaghetti with guanciale, egg, pecorino, pepper",
      "vat_category": "food",
      "allergens": ["gluten", "egg", "dairy"],
      "prices": {
        "dine_in": 12.0,
        "takeaway": 11.0,
        "in_house_delivery": null,
        "platform_delivery": 13.5
      },
      "created_at": "2026-04-12T08:14:22.183Z"
    }
  ],
  "count": 42
}

Prices are gross (VAT-inclusive) — the amount the customer pays at the till. When you push a sale, the unit_price you report must also be gross. To benefit from automatic name+price mapping on first sight, the price you send must match the recipe's gross price for the relevant channel within €0.01. See Pricing and VAT.

A null price for a channel means the recipe is not sold via that channel. Do not substitute a different channel's price — the auto-matcher will reject the line. If a recipe has only dine_in set, only send dineIn sales for it.

POST /api/v1/pos/applied-order-uids

Order-level deduplication and replay detection (ISS-394).

When running asynchronous POS pollers or streaming sync connectors, you can check which order UIDs have already been ingested and applied to inventory/sales before submitting a new batch. This prevents double-deductions and lets pollers identify missing gaps safely.

  • Authentication: Bearer token (clzn_... or partner key).
  • Scope: Read-only lookup scoped strictly to the caller's own integration.
  • Limit: Up to 10,000 order UIDs per request.

Request (JSON)

{
  "order_uids": ["pos-ord-2026-0901-001", "pos-ord-2026-0901-002", "pos-ord-2026-0901-003"]
}

Response (200)

{
  "applied": ["pos-ord-2026-0901-001", "pos-ord-2026-0901-002"],
  "count": 2
}

POST /api/v1/pos/integration-paused

Poller error budget pause notification (ISS-130).

When automated sync middleware encounters consecutive errors tripping its error budget, it calls this endpoint to notify the restaurant owner by email immediately, rather than letting the failure go silent and having the team discover missing sales days later.

  • Authentication: Bearer token (clzn_... or partner key).
  • Timing requirement: Must be called before the integration is marked inactive. Once an integration's status flips to inactive, the token will be rejected with 403 Forbidden.
  • Design: Best-effort; answers 200 OK and reports notification delivery in the JSON payload body so it never masks the underlying POS poll failure.

Request (JSON)

{
  "reason": "Repeated timeout connecting to POS terminal endpoint",
  "consecutive_failures": 5
}

Response (200)

{
  "notified": true,
  "recipient": "owner@restaurant.gr"
}

GET /api/v1/pos/health

Connectivity check. No authentication required.

Response (200)

{
  "status": "ok",
  "service": "culizen-pos-api",
  "version": "1.0",
  "timestamp": "2026-05-19T08:14:22.183Z"
}

Sales payload — JSON

{
  "batch_id": "Z-2026-03-04-001",
  "sales_date": "2026-03-04",
  "restaurant_id": "12345",
  "mode": "push",
  "items": [
    {
      "pos_item_code": "PLU-001",
      "pos_item_name": "Margherita",
      "quantity": 12,
      "service_type": "dineIn",
      "unit_price": 9.61,
      "total": 115.32
    },
    {
      "pos_item_code": "PLU-001",
      "pos_item_name": "Margherita",
      "quantity": 3,
      "service_type": "dineIn",
      "unit_price": 6.78,
      "original_price": 9.61,
      "discount_amount": 2.83,
      "discount_type": "promo",
      "discount_name": "Happy Hour -30%",
      "total": 20.34
    },
    {
      "pos_item_code": "PLU-001",
      "pos_item_name": "Margherita",
      "quantity": 1,
      "service_type": "dineIn",
      "is_comp": true,
      "comp_reason": "VIP table",
      "food_prepared": true,
      "unit_price": 0,
      "original_price": 9.61
    },
    {
      "pos_item_code": "PLU-042",
      "pos_item_name": "Coca Cola 330ml",
      "quantity": 2,
      "service_type": "dineIn",
      "is_void": true,
      "void_reason": "Wrong order",
      "food_prepared": false,
      "unit_price": 0
    },
    {
      "pos_item_code": "PLU-015",
      "pos_item_name": "Chicken Souvlaki",
      "quantity": 5,
      "service_type": "platformDelivery",
      "unit_price": 4.52,
      "total": 22.6
    }
  ],
  "metadata": {
    "register_id": "REG-01",
    "z_report_number": 1542,
    "shift_closed_at": "2026-03-04T23:30:00+02:00"
  }
}

Root fields

Field Type Required Notes
batch_id string Recommended Unique identifier for this Z-report. Used for idempotency. If omitted, Culizen derives one from the payload hash.
sales_date string YYYY-MM-DD Yes The business date being reported.
restaurant_id string Partner mode only The POS's internal restaurant identifier. May be supplied here instead of in the X-Restaurant-Id header.
mode push | poll Default push Use push for end-of-day Z-reports. poll is reserved for Culizen-managed polling middleware that pulls partial windows throughout the day and accumulates them; external partners should not set this.
items array Yes At least one item. See Item field reference.
metadata object No Free-form. register_id, z_report_number, shift_closed_at are recommended for traceability.

Sales payload — XML

Submit XML with Content-Type: application/xml. The XML root element is <SalesReport>; tags use PascalCase. Culizen normalises PascalCase → snake_case internally, so a <PosItemCode> value lands in the same place as JSON's pos_item_code.

<?xml version="1.0" encoding="UTF-8"?>
<SalesReport>
  <BatchId>Z-2026-03-04-001</BatchId>
  <SalesDate>2026-03-04</SalesDate>
  <RestaurantId>12345</RestaurantId>
  <Items>

    <!-- Regular sale -->
    <Item>
      <PosItemCode>PLU-001</PosItemCode>
      <PosItemName>Margherita</PosItemName>
      <Quantity>12</Quantity>
      <ServiceType>dineIn</ServiceType>
      <UnitPrice>9.61</UnitPrice>
      <Total>115.32</Total>
    </Item>

    <!-- Discounted sale -->
    <Item>
      <PosItemCode>PLU-001</PosItemCode>
      <PosItemName>Margherita</PosItemName>
      <Quantity>3</Quantity>
      <ServiceType>dineIn</ServiceType>
      <UnitPrice>6.78</UnitPrice>
      <OriginalPrice>9.61</OriginalPrice>
      <DiscountAmount>2.83</DiscountAmount>
      <DiscountType>promo</DiscountType>
      <DiscountName>Happy Hour -30%</DiscountName>
      <Total>20.34</Total>
    </Item>

    <!-- Comp (on the house) -->
    <Item>
      <PosItemCode>PLU-001</PosItemCode>
      <PosItemName>Margherita</PosItemName>
      <Quantity>1</Quantity>
      <ServiceType>dineIn</ServiceType>
      <IsComp>true</IsComp>
      <CompReason>VIP table</CompReason>
      <FoodPrepared>true</FoodPrepared>
      <UnitPrice>0</UnitPrice>
      <OriginalPrice>9.61</OriginalPrice>
    </Item>

    <!-- Void BEFORE preparation (skipped, no inventory impact) -->
    <Item>
      <PosItemCode>PLU-042</PosItemCode>
      <PosItemName>Coca Cola 330ml</PosItemName>
      <Quantity>2</Quantity>
      <ServiceType>dineIn</ServiceType>
      <IsVoid>true</IsVoid>
      <VoidReason>Wrong order</VoidReason>
      <FoodPrepared>false</FoodPrepared>
      <UnitPrice>0</UnitPrice>
    </Item>

    <!-- Void AFTER preparation (logged as waste, inventory deducted) -->
    <Item>
      <PosItemCode>PLU-007</PosItemCode>
      <PosItemName>Moussaka</PosItemName>
      <Quantity>1</Quantity>
      <ServiceType>dineIn</ServiceType>
      <IsVoid>true</IsVoid>
      <VoidReason>Customer complaint</VoidReason>
      <FoodPrepared>true</FoodPrepared>
      <UnitPrice>0</UnitPrice>
      <OriginalPrice>13.56</OriginalPrice>
    </Item>

  </Items>
  <Metadata>
    <RegisterId>REG-01</RegisterId>
    <ZReportNumber>1542</ZReportNumber>
    <ShiftClosedAt>2026-03-04T23:30:00+02:00</ShiftClosedAt>
  </Metadata>
</SalesReport>

Item field reference

Field (JSON) Field (XML) Type Required Notes
pos_item_code PosItemCode string Yes Your internal product code (PLU/SKU). Used to look up the Culizen recipe mapping for this code.
pos_item_name PosItemName string Recommended Human-readable name. Used for the auto-match fallback and shown in the restaurant's UI when the code is unmapped.
recipe_id RecipeId UUID No Optional explicit pre-mapping. When set to a recipe ID from GET /pos/recipes, Culizen skips name+price auto-match and resolves the line directly to that recipe. The persistent mapping for pos_item_code is upserted at the same time, so future calls can drop the field. An unknown id is ignored gracefully (line falls through to normal auto-match). See Recipe mapping.
quantity Quantity number > 0 Yes Number of units sold on this line.
service_type ServiceType enum Recommended One of dineIn (default), takeaway, inHouseDelivery, platformDelivery. Drives which channel's price + VAT rate applies.
unit_price UnitPrice number ≥ 0 Recommended The price actually charged per unit, gross (VAT-inclusive). Required for auto-match.
vat_amount VatAmount number ≥ 0 No Per-unit VAT portion baked into unit_price. Currently informational — Culizen computes VAT from the recipe's vat_category + the restaurant's VAT profile. Accepted now so partners can send it forward-compatibly; a future feature (partner-authoritative VAT for cross-border or custom-rate cases) may consume it without a schema change.
original_price OriginalPrice number ≥ 0 No The menu price before any discount, gross.
total Total number ≥ 0 No Line total (quantity × unit_price). Will be recomputed if omitted.
discount_amount DiscountAmount number ≥ 0 No Per-unit discount applied.
discount_type DiscountType enum No One of percentage, fixed, promo.
discount_name DiscountName string No Display name of the promo / coupon.
is_comp IsComp boolean No true if comped (on-the-house). Default false.
comp_reason CompReason string No Free text.
is_void IsVoid boolean No true if the order was voided. Default false.
void_reason VoidReason string No Free text.
food_prepared FoodPrepared boolean No true if the kitchen had already prepped the item before void. Default true. Determines inventory impact — see Sales classifications.

Service-type semantics

Value Meaning
dineIn Consumed in the restaurant.
takeaway Customer picks up from the counter.
inHouseDelivery Delivered by the restaurant's own drivers.
platformDelivery Delivered via a third-party platform (Wolt, efood, etc.).

Migration note. Earlier drafts accepted a single delivery value. The current API rejects delivery; use inHouseDelivery or platformDelivery to keep the distinction the cost-of-goods reports require.


Sales classifications

Each item is classified server-side based on its flags. The classification determines whether the line counts as revenue, deducts inventory, or both.

Item flags Classification Revenue Inventory deducted Notes
Plain item, no flags regular_sale + unit_price Yes The standard case.
With discount_amount or discount_type discounted_sale + unit_price (post-discount) Yes Discount recorded for promo analytics.
is_comp: true comp 0 Yes Logged as cost-of-comps; inventory still moves.
is_void: true, food_prepared: false skip 0 No Order cancelled before kitchen touched it — invisible to reports beyond a logged note.
is_void: true, food_prepared: true void_waste 0 Yes Order made and then thrown — logged as waste.

Pricing and VAT

All prices in the API are gross (VAT-inclusive) — both the prices block on GET /pos/recipes and the unit_price you submit on POST /pos/sales. This is the price the customer pays at the till.

VAT rates are taken from the restaurant's profile:

  • The country's standard rates (e.g. Greece dine-in food 13%, takeaway food 13%, alcohol 24%), or
  • The custom rates the restaurant configured in Settings → VAT.

The applied rate is determined by two inputs:

  1. The recipe's vat_category (food, softDrink, or alcohol — returned by GET /pos/recipes).
  2. The item's service_type (only dineIn vs takeaway-family matters in most countries).

Auto-match tolerance

For the auto-match-by-name+price flow to succeed (see Recipe mapping), the unit_price you send must match the recipe's gross price for that channel within €0.01. Rounding to cents on your side is fine — Culizen does the same.

If a recipe has a null price for a channel (e.g. no takeaway price set), the restaurant is not selling that recipe via that channel. Do not substitute another channel's price; the line will simply stay unmapped.


Recipe mapping

POS item codes (pos_item_code) need to be linked to Culizen recipe_ids for sales to flow into menu engineering, drain analysis, and inventory deductions. Three paths get you there, in order of preference:

  1. Explicit recipe_id on each item (recommended when you can). If you've prefetched the recipe catalog with GET /pos/recipes and let the restaurant operator confirm "your PLU X → Culizen recipe Y" inside your own POS UI, you can send the resolved Culizen recipe ID directly on each item:

    {
      "pos_item_code": "PLU-001", // your internal PLU (still required)
      "recipe_id": "5b34e1f0-0d4c-...", // the recipe ID you resolved on your side
      "pos_item_name": "Margherita",
      "quantity": 1,
      "service_type": "dineIn",
      "unit_price": 9.61,
    }
    

    Culizen skips name+price matching entirely and resolves the line to that recipe directly. The persistent pos_item_mappings row is upserted at the same time, so subsequent calls can drop the recipe_id field and resolve via the existing-mapping path. An unknown recipe_id is ignored gracefully — the line falls through to path 2 — so partial rollout is safe.

  2. Server-side auto-match on push. When a previously unseen pos_item_code arrives without an explicit recipe_id, Culizen tries to auto-match it against the restaurant's recipe catalog using case-insensitive name match AND gross-price match within €0.01 for every observation of that code in the batch. If exactly one recipe matches, the mapping is applied automatically. Ambiguous or missing matches fall through to path 3.

  3. Manual mapping in the Culizen UI. Unmapped codes appear in Settings → POS Mappings for the restaurant to resolve. Once a mapping is created, historic batches that contained that code are automatically backfilled into daily_menu_sales.

unmapped_items in the push response tells you exactly which codes fell through. You can surface this in your own POS UI as a "needs attention" badge.


Idempotency

The API is idempotent on two layers:

  1. By batch_id. If you POST a sales batch with a batch_id Culizen has already processed for this restaurant, the response is status: "already_processed" and no duplicate data is written.

  2. By payload hash. Even without a batch_id, Culizen computes a SHA-256 hash of the canonical payload. An identical re-submission returns already_processed.

Best practice: always set a unique batch_id per Z-report — e.g. Z-<YYYY-MM-DD>-<register>-<z_number>. This makes retries trivially safe and makes troubleshooting in the restaurant's audit log clearer.

A failed submission (HTTP 5xx, network timeout) can be retried with the same payload; the idempotency guarantees apply.


Rate limits

  • 1,200 requests per minute per integration (≈ 20 / second). Returned HTTP 429 with body { "error": "rate_limited" } when exceeded.
  • The limit covers all POS endpoints combined (/sales, /status, /recipes).
  • This is intentionally generous — it exists as a safety valve against runaway loops or abuse, not as a throttle on real traffic. A webhook-per-sale partner at a busy restaurant typically generates well under 1 req/sec; daily Z-reports and 5-min pollers sit orders of magnitude below the cap. If a legitimate integration is bumping into it, contact us and we'll raise the ceiling for your integration — the number isn't a contract.

Implement exponential backoff on 429 responses (e.g. 2s → 5s → 10s, max 3 attempts) as standard hygiene — but expect to almost never see one in practice.


Error codes

All errors are returned as JSON:

{
  "error": "validation_error",
  "message": "sales_date is required",
  "details": [{ "field": "sales_date", "message": "Required" }]
}
HTTP error code Meaning
200 Success. See status field for sub-states.
400 validation_error The body failed schema validation. details lists the failing fields.
401 unauthorized Missing or invalid API key.
401 restaurant_not_linked Partner mode: no linked restaurant for the given X-Restaurant-Id. The restaurant has not been linked by Culizen support (or the link has been removed).
403 integration_inactive The restaurant or partner integration is disabled.
403 partner_inactive The partner key has been revoked.
429 rate_limited Rate limit exceeded.
500 internal_error Server-side failure. Safe to retry.

Versioning

  • The path prefix /api/v1/pos/... is v1 and is stable.
  • Additive changes (new fields, new endpoints, new optional values) may ship at any time. Consumers should ignore unknown fields.
  • Breaking changes (renames, removed fields, semantic changes) will be released under /api/v2/pos/... with at least 6 months of overlap before v1 is sunset. Partners on record will be notified in advance.
  • The Changelog below tracks both additive changes and clarifications.

Changelog

2026-09-01

  • Order-level deduplication endpoint (POST /pos/applied-order-uids). Added read-only lookup endpoint supporting up to 10,000 order UIDs so POS pollers and async push connectors can verify which orders have already been ingested into Culizen before submitting batches (ISS-394).
  • Poller error budget notification endpoint (POST /pos/integration-paused). Added automated alert trigger enabling sync pollers to notify restaurant owners via email when consecutive sync failures trip error budgets, preventing silent sync drift (ISS-130).
  • POS integration tier gating documentation. Clarified that issuing and managing self-serve POS integration keys requires the Professional (€119/mo) or Executive plan tier (ADR 0033). Essential and Starter tier accounts have read-only integration access.
  • Native connectors reference. Documented native zero-code connector availability for Delivery Manager (/pos/integrations/dm) and W-POS.

2026-05-20

  • Optional recipe_id on each item. Sales items now accept a recipe_id (UUID) field. When set to a recipe ID from GET /pos/recipes, Culizen skips name+price auto-match and resolves the line directly to that recipe, while also upserting the persistent pos_item_mappings row so subsequent calls can drop the field. Unknown ids are ignored — the line falls through to normal auto-match — so partial / gradual rollout is safe. Recommended for partners that maintain a (your PLU → Culizen recipe) table on their side.
  • Optional vat_amount on each item. Sales items now also accept a vat_amount (number, non-negative) — the per-unit VAT portion baked into unit_price. Currently informational; today's pipeline still computes VAT from the recipe's vat_category and the restaurant's VAT profile. Accepted, validated, and persisted in the raw payload so a future feature (e.g. partner-authoritative VAT for cross-border or custom-rate cases) can read it without a schema change. Safe to start sending whenever convenient.
  • Per-integration rate limit raised to 1,200 req/min (≈ 20/sec). Previously documented as 10/min — that figure was stale (the live limit had been bumped to 180/min some time ago without doc sync). The cap now sits well above any realistic per-restaurant traffic so partners should effectively never encounter a 429 through normal use. It remains a safety valve against runaway loops, not a throttle — write to us if a legitimate integration is bumping into it.

2026-05-19

  • GET /pos/recipes now returns gross (VAT-inclusive) prices. Previously the response echoed back the restaurant's net selling_price columns, which silently broke the auto-match-by-name+price flow because pushSales compares the POS's unit_price against the recipe's gross price within €0.01. Partners who fed prices.dine_in back as unit_price verbatim would miss by the VAT delta on every line. The endpoint now joins the user's VAT profile, runs each net price through the same getRecipeGrossPrice helper the auto-matcher uses, and rounds to cents. Auto-match against the new gross values is the supported contract going forward. No payload change required on the partner side — the values returned by /pos/recipes are now the values you should send back as unit_price.

2026-05 (prior, condensed)

  • Recipe price columns split per channel — dine_in, takeaway, in_house_delivery, platform_delivery. The legacy single-delivery channel was removed; partners must distinguish in-house from platform delivery in service_type.
  • mode: 'poll' reserved for Culizen-managed polling middleware. External partners should leave it unset.
  • POST /pos/integrations/restaurant-key self-serve flow shipped in the Culizen UI so restaurants can issue their own clzn_<key> without contacting support.

Questions or integration support: support@culizen.gr.