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
- At a glance
- Quick start
- Authentication
- Endpoints
- Sales payload — JSON
- Sales payload — XML
- Item field reference
- Sales classifications
- Pricing and VAT
- Recipe mapping
- Idempotency
- Rate limits
- Error codes
- Versioning
- 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
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.
- 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
(Optional) Fetch the recipe catalog with
GET /api/v1/pos/recipesso you can pre-map your internal item codes (PLU/SKU) to Culizenrecipe_ids. If you skip this, Culizen will attempt to auto-match by name and price on the first batch.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 abatch_idand a processing summary.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-Idheader 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-Idon every request. - Headers:
Authorization: Bearer clzn_partner_<key> X-Restaurant-Id: <your_internal_restaurant_id> X-Restaurant-Idmay alternatively be supplied asrestaurant_idin 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_priceyou 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
nullprice 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 onlydine_inset, only senddineInsales 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 OKand 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
deliveryvalue. The current API rejectsdelivery; useinHouseDeliveryorplatformDeliveryto 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:
- The recipe's
vat_category(food,softDrink, oralcohol— returned byGET /pos/recipes). - The item's
service_type(onlydineInvs 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:
Explicit
recipe_idon each item (recommended when you can). If you've prefetched the recipe catalog withGET /pos/recipesand let the restaurant operator confirm "your PLUX→ Culizen recipeY" 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_mappingsrow is upserted at the same time, so subsequent calls can drop therecipe_idfield and resolve via the existing-mapping path. An unknownrecipe_idis ignored gracefully — the line falls through to path 2 — so partial rollout is safe.Server-side auto-match on push. When a previously unseen
pos_item_codearrives without an explicitrecipe_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.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:
By
batch_id. If you POST a sales batch with abatch_idCulizen has already processed for this restaurant, the response isstatus: "already_processed"and no duplicate data is written.By payload hash. Even without a
batch_id, Culizen computes a SHA-256 hash of the canonical payload. An identical re-submission returnsalready_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 429with 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
Changelogbelow 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_idon each item. Sales items now accept arecipe_id(UUID) field. When set to a recipe ID fromGET /pos/recipes, Culizen skips name+price auto-match and resolves the line directly to that recipe, while also upserting the persistentpos_item_mappingsrow 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_amounton each item. Sales items now also accept avat_amount(number, non-negative) — the per-unit VAT portion baked intounit_price. Currently informational; today's pipeline still computes VAT from the recipe'svat_categoryand 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/recipesnow returns gross (VAT-inclusive) prices. Previously the response echoed back the restaurant's netselling_pricecolumns, which silently broke the auto-match-by-name+price flow becausepushSalescompares the POS'sunit_priceagainst the recipe's gross price within €0.01. Partners who fedprices.dine_inback asunit_priceverbatim would miss by the VAT delta on every line. The endpoint now joins the user's VAT profile, runs each net price through the samegetRecipeGrossPricehelper 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/recipesare now the values you should send back asunit_price.
2026-05 (prior, condensed)
- Recipe price columns split per channel —
dine_in,takeaway,in_house_delivery,platform_delivery. The legacy single-deliverychannel was removed; partners must distinguish in-house from platform delivery inservice_type. mode: 'poll'reserved for Culizen-managed polling middleware. External partners should leave it unset.POST /pos/integrations/restaurant-keyself-serve flow shipped in the Culizen UI so restaurants can issue their ownclzn_<key>without contacting support.
Questions or integration support: support@culizen.gr.