Interface Reference

Every field, every unit,
every default we invent.

Ordering, the first sub-product of Synapse, takes one request and returns one order. This page is its whole interface: the 118 fields across 7 datasets, the CSV columns that map onto them, the routes, the errors, and a plan computed in your browser from a request you can download.

The tables below are rendered directly from the engine’s own field table. They are not transcribed, so they cannot drift away from what the service actually accepts.

contract 1.2.0 engine 0.2.1 7 datasets · 118 fields · 17 required 116 CSV columns

Host. Access is provisioned per account, so the examples below write the base URL as $SYNAPSE_HOST. The request and response shapes are fixed by the contract and the engine, the same code that produced this page.

Section 1

One request in.
One order out.

A plan request describes four things: what you sell, what has been selling, what is on hand right now, and the building it goes in. Ordering returns how much to buy, where each line will physically go, and what bound every line it had to cut.

The quantity is economics first: history with stockout days read as demand, cover walked to the weekday the supplier actually delivers, each unit priced unsold-against-missed-sale, then the shelf-life cap. The building competes last - space rarely binds - and a line it does cut names the zone, and the zone carries a shadow price.

Nothing is stored between calls. Send the same body tomorrow and you get the same answer, including the same plan_id.

Way in 1 - JSON

Assemble a PlanInput from your own systems and POST it to /v1/plan. One body, one site, one plan. This is the integration you want in the end: it carries the nested structures a flat file cannot - rack levels, temperature bands, hazard metadata, segregation rules.

Way in 2 - CSV

Export one file per dataset and post each to /v1/parse/csv to get canonical rows back. Header matching ignores case, spaces and punctuation and consults an alias table per field, so Qty, QUANTITY and qty_units all land in the same place. Columns Synapse cannot place are kept verbatim under _extra, never dropped.

Units, once, for everything

Units are in the field names and are never inferred. A field that is absent and a field that is null mean the same thing: use the default.

Lengths

centimetres

_cm

Volumes

cubic metres

_m3

Weights

kilograms

_kg

Temperatures

Celsius

_c

Dates

ISO-8601 calendar days, no times and no zones

YYYY-MM-DD

Money

the account’s single currency, never per line

unit_cost

Section 2

The data contract

7 datasets, 118 fields, 17 of them required. Everything else has a documented default, and every default Synapse actually applies comes back in the plan’s assumptions[]. Unknown fields are a warning, never an error - extend your export freely.

Site

site required single record · key site_id · 20 fields

Site-wide policy. One record. Every field has a defensible default, so a first plan needs nothing but a site_id.

Field Type Unit Default Description
site_id required string - - Identifier for the site being planned. One plan covers one site. e.g. CEDAR-ST
name string - no default Human-readable site name. Presentational only. e.g. Cedar Street Market
currency string - USD ISO-4217 code. Presentational only - the engine uses one currency per account. e.g. USD
timezone string - UTC IANA timezone name. Presentational only; all dates are plain calendar dates. e.g. America/Denver
service_level number 0-1 fraction 0.95 Cycle service level targeted for non-perishables. Perishables use a newsvendor ratio instead. e.g. 0.95
review_period_days number ≥ 0 days 7 How often this site places orders. Half of it is exposure the safety stock must cover. e.g. 7
horizon_days number ≥ 1 days review_period_days + the longest lead time in the input Planning horizon used for reporting. e.g. 14
holding_cost_rate_annual number 0-1 fraction 0.25 Carrying cost as a fraction of unit cost per year. Sets the overage cost for non-perishables. e.g. 0.25
default_order_cost number ≥ 0 currency 25 Fixed cost per purchase order used when a supplier declares none. The EOQ setup cost. e.g. 25
default_lead_time_days number ≥ 0 days 3 Lead time used when neither the product nor its supplier declares one. e.g. 3
default_margin_pct number ≥ 0 fraction 0.3 Margin used to derive unit_price when a product omits it. e.g. 0.3
waste_tolerance_pct number 0-100 % 2 Expected spoilage the business accepts, as a share of units ordered. Caps every perishable line. e.g. 2
budget number ≥ 0 currency unlimited Cash ceiling for this order cycle. The allocator spends it on the highest-value units first. e.g. 12000
max_manual_lift_kg number ≥ 0 kg 16 Above this case weight, a case may not be assigned above shoulder height. e.g. 16
order_weekdays number[] weekday the review period ISO weekdays this site places orders, 1 = Monday to 7 = Sunday, for suppliers that do not state their own. e.g. [1,4]
trading_minutes_by_weekday number[] minutes not set - stockout scale-ups then assume a 24-hour trading day, and that assumption is a known fiction Minutes this site trades each weekday: seven entries, Monday first, each 0-1440, 0 meaning closed. Sharpens the stockout scale-up - six hours out of stock on a declared 12-hour day is half the trading day, not a quarter of it. Synapse never invents your opening hours. e.g. [720,720,720,720,720,720,0]
off_cycle_order_cost number ≥ 0 currency not set - off-cycle risk is then reported in orders, not money What one unplanned order between scheduled deliveries costs you: freight surcharge, administration, whatever the supplier charges. Synapse never invents this number. e.g. 85
unbooked_movement_pct number 0-100 % not set - Synapse then treats your books as exact, and says so Of the units that move through a line, the percentage that leaves without being booked: shrink, damage, unrecorded transfers, receiving miscounts. Your shrink-as-a-percent-of-sales figure. Synapse never invents it. e.g. 1.4
receiving_capacity_m3_per_day number ≥ 0 m3/day unlimited Dock and putaway throughput. Caps how much volume one delivery may bring. e.g. 40
segregation_rules JSON only object[] - the three DEFAULT_SEGREGATION_RULES (hazardous away from ordinary stock, raw-above-ready-to-eat, flammable/oxidiser) Pairs of tokens that may not share a zone (scope "zone") or be stacked above one another (scope "above"). e.g. [{"scope":"above","a":"raw_meat","b":"ready_to_eat","reason":"Drip contamination"}]

Zones

zones optional array · key zone_id · 19 fields

undefined

Field Type Unit Default Description
zone_id required string - - Identifier for a physically distinct storage area. Inventory rows reference it. e.g. COOLER-1
name string - no default Human-readable zone name. e.g. Chilled store
storage_class required string - - The condition this zone maintains. Gates which products may be stored here. one of: ambient · cool · chilled · frozen · deep_frozen · controlled · hazmat e.g. chilled
capacity_mode required string - - How this zone measures capacity. The allocator only ever compares like with like. one of: volume · pallets · shelf_linear · slots e.g. volume
temp_min_c JSON: temp_c.min number C the storage class's nominal band Coldest temperature the zone reaches or maintains. e.g. 1
temp_max_c JSON: temp_c.max number C the storage class's nominal band Warmest temperature the zone reaches. A product is only eligible if it tolerates this. e.g. 4
humidity_min_pct JSON: humidity_pct.min number 0-100 % unconstrained Lowest relative humidity held in the zone. e.g. 80
humidity_max_pct JSON: humidity_pct.max number 0-100 % unconstrained Highest relative humidity held in the zone. e.g. 90
capacity_volume_m3 number ≥ 0 m3 derived from levels when they are supplied Usable cubic metres. Required for capacity_mode "volume" when levels are omitted. e.g. 18.5
capacity_pallets integer ≥ 0 pallets derived from levels when they are supplied Whole pallet positions. Required for capacity_mode "pallets" when levels are omitted. e.g. 12
capacity_shelf_linear_m number ≥ 0 m derived from levels when they are supplied Linear metres of shelf face. Required for capacity_mode "shelf_linear" when levels are omitted. e.g. 24
capacity_slots integer ≥ 0 slots derived from levels when they are supplied Countable identical bins, totes or cages. Required for capacity_mode "slots" when levels are omitted. e.g. 40
max_load_kg number ≥ 0 kg unlimited Whole-zone load rating. Floors and mezzanines have one even when beams do not. e.g. 4000
fill_target_pct number 1-100 % 85 Practical ceiling that leaves room for aisles, airflow, pick space and receiving surge. e.g. 85
reserved_volume_m3 number ≥ 0 m3 0 Capacity held back for something Synapse does not model - a staging square, a returns cage. e.g. 1.2
hazard_classes_allowed string[] - none, except a hazmat zone which defaults to every hazard class present in the input Hazard classes this zone is licensed to hold. A hazardous product is ineligible everywhere else. e.g. ["flammable_liquid","corrosive"]
segregation_groups_present string[] - [] Fixed occupants already in the zone that incoming stock must be segregated against. e.g. ["hazard:corrosive"]
airflow_restricted boolean - false True when stacking to the clear height is not permitted, so the top of each level stays empty. e.g. false
levels JSON only object[] - a single level covering the whole zone Rack levels: level, clear_height_cm, area_m2, pallet_positions, shelf_linear_m, slots, max_load_kg, reach_height_cm, ladder_required. e.g. [{"level":1,"clear_height_cm":180,"area_m2":6,"max_load_kg":900}]

Products

products required array · key sku · 45 fields

One row per SKU. The required core is sku, storage_class, case_pack and unit_cost; everything else sharpens the answer.

Field Type Unit Default Description
sku required string - - Stock-keeping unit. Matching is case-sensitive and Synapse never changes its case. e.g. MLK-2L
name string - no default Human-readable product name, echoed back on every plan line. e.g. Whole milk, 2 L
category string - no default Free-text grouping. Also usable as a segregation token (for example "raw_meat" or "solvent"). e.g. chilled_goods
storage_class required string - - The condition this product requires. Decides which zones may hold it. one of: ambient · cool · chilled · frozen · deep_frozen · controlled · hazmat e.g. chilled
temp_min_c JSON: temp_c.min number C the storage class's nominal band Coldest temperature the product tolerates. Widening this opens up more zones. e.g. 0
temp_max_c JSON: temp_c.max number C the storage class's nominal band Warmest temperature the product tolerates. A zone that goes above this is ineligible. e.g. 5
humidity_min_pct JSON: humidity_pct.min number 0-100 % unconstrained Lowest relative humidity the product tolerates. e.g. 45
humidity_max_pct JSON: humidity_pct.max number 0-100 % unconstrained Highest relative humidity the product tolerates. e.g. 65
shelf_life_days number ≥ 0 days absent - the product is treated as non-perishable Total life from receipt at the correct temperature. Omit for non-perishables. e.g. 14
min_remaining_life_days number ≥ 0 days 0 Life that must still remain at the moment of sale or issue. The retailer rule that shortens every order. e.g. 5
life_penalty_days_per_excursion number ≥ 0 days 0 Life lost per temperature excursion. Reserved; accepted and echoed but not used in v1 scoring. e.g. 2
case_pack required integer ≥ 1 units - Selling units per shipping case. 1 for eaches. Also the default order multiple. e.g. 6
case_volume_m3 JSON: case.volume_m3 number ≥ 0 m3 derived from case length x width x height when those are given Shipping-case volume. The primary size input for volume-measured zones. e.g. 0.021
case_length_cm JSON: case.length_cm number ≥ 0 cm no default Case length. Supplying real dimensions replaces the assumed packing efficiency with real stacking. e.g. 30
case_width_cm JSON: case.width_cm number ≥ 0 cm no default Case width. With length and height it replaces the assumed packing efficiency with real stacking. e.g. 20
case_height_cm JSON: case.height_cm number ≥ 0 cm no default Case height. Decides how many cases fit under a beam. e.g. 35
case_gross_weight_kg JSON: case.gross_weight_kg number ≥ 0 kg no default Filled case weight. Drives beam load limits and the manual-lift height rule. e.g. 12.4
case_max_stack JSON: case.max_stack integer ≥ 1 cases 5 5, or 1 when crush_sensitive is true How many of these cases may be stacked on top of one another. e.g. 5
case_crush_sensitive JSON: case.crush_sensitive boolean - false True for cases nothing may be stacked on. Forces max_stack to 1. e.g. false
cases_per_pallet JSON: case.cases_per_pallet integer ≥ 1 cases derived from case dimensions on a 1.2 x 1.0 m pallet at 1.5 m usable height Cases on a full pallet. Required for pallet-measured zones unless dimensions are given. e.g. 48
facing_width_cm JSON: case.facing_width_cm number ≥ 0 cm derived from case width Shelf face a single case or tray occupies. Required for shelf-linear zones unless dimensions are given. e.g. 20
units_per_slot integer ≥ 1 units case_pack - one slot is assumed to hold a full case Selling units that fit one bin, tote or cage. Used by slot-counted zones. e.g. 4
unit_volume_m3 number ≥ 0 m3 case volume divided by case_pack Volume of one selling unit. e.g. 0.0035
unit_weight_kg number ≥ 0 kg case gross weight divided by case_pack Weight of one selling unit. e.g. 2.07
unit_cost required number ≥ 0 currency - Landed cost per selling unit. Drives order value, holding cost and the cost of over-ordering. e.g. 1.62
unit_price number ≥ 0 currency unit_cost x (1 + default_margin_pct) Sale price per selling unit. Drives the cost of running out. e.g. 2.49
salvage_value_per_unit number ≥ 0 currency 0 Value recovered when a unit expires - markdown, rendering, donation credit. e.g. 0.4
disposal_cost_per_unit number ≥ 0 currency 0 Paid to destroy an expired unit - hazardous waste, cold-chain return, rendering fee. e.g. 0.15
stockout_penalty_per_unit number ≥ 0 currency 0 Goodwill or contractual cost of an unmet unit, on top of the lost margin. e.g. 0.5
hazard_class JSON: hazard.class string - no hazard Hazard class token, e.g. flammable_liquid, oxidiser, corrosive, aerosol. e.g. corrosive
hazard_un_number JSON: hazard.un_number string - no default UN dangerous-goods number. Carried through to the plan; not used in scoring. e.g. UN1791
hazard_segregation_group JSON: hazard.segregation_group string - no default The customer's own grouping key, matched against site segregation_rules. e.g. hazard:corrosive
hazard_requires_bunding JSON: hazard.requires_bunding boolean - false True when the product must sit in a bunded (spill-contained) area. e.g. true
allergens string[] - [] Allergen tokens, where the stock has any. Matched against site segregation_rules for separation and above/below rules. e.g. ["peanut","milk"]
segregation_groups string[] - [] Extra segregation tokens for this product. Anything non-hazardous is treated as ordinary stock that hazardous goods must be kept away from; list "non_food" to opt out of that rule. e.g. ["non_food"]
supplier_id string - none - the product then uses site defaults for lead time and order cost Supplier this product is bought from. Groups plan lines into supplier orders. e.g. VAL-DAIRY
lead_time_days number ≥ 0 days the supplier's lead_time_days, else site default_lead_time_days Days from placing the order to receiving it, for this product specifically. e.g. 2
lead_time_sigma_days number ≥ 0 days the supplier's lead_time_sigma_days, else 0 Standard deviation of the lead time. Widens safety stock; a late delivery is a stockout. e.g. 0.5
moq_units number ≥ 0 units 0 Supplier minimum for this line. A line either meets it or is dropped to zero - never part-filled. e.g. 24
order_multiple_units number ≥ 1 units case_pack Order granularity. Every order quantity is rounded to a multiple of this. e.g. 6
max_order_units number ≥ 0 units unlimited Hard ceiling on this line - allocation, contract or dock constraint. e.g. 300
order_weekdays number[] weekday the supplier's, else the site's, else the review period ISO weekdays this line is ordered on when it runs on its own cadence, 1 = Monday to 7 = Sunday. e.g. [1,4]
discontinued boolean - false True to stop ordering the product while still reporting its expiry and dead-stock risk. e.g. false
substitutes string[] - [] SKUs that cover the same demand. Reported as an option; v1 never substitutes automatically. e.g. ["MLK-1L"]
received_life_days number ≥ 0 days shelf_life_days Life remaining on arrival when the supplier ships aged stock. Shortens the sellable window. e.g. 12

Inventory

inventory optional array · key sku + zone_id + lot_id · 10 fields

Stock on hand, one row per lot. Lot granularity is what makes shelf-life reasoning possible.

Field Type Unit Default Description
sku required string - - SKU of the stock on hand. Must exist in products. e.g. MLK-2L
zone_id string - no default Zone the lot currently occupies, when zones are described. It is space already spent. e.g. COOLER-1
qty_units required number ≥ 0 units - Selling units in this lot. e.g. 18
lot_id string - none - the lot is identified by sku + zone_id + row order Lot or batch identifier, quoted back in expiry risks so an operator can find the stock. e.g. L204
expires_on date date received_on + shelf_life_days, else the lot is treated as ageless Expiry date of the lot. Without it Synapse cannot reason about spoilage and says so. e.g. 2026-08-11
received_on date date no default Date the lot was received. Used to derive expiry when expires_on is absent. e.g. 2026-07-28
level integer ≥ 1 - unknown - treated as level 1 for load purposes Rack level the lot currently occupies. 1 is the floor or bottom beam. e.g. 1
quarantined boolean - false True for stock that is unavailable to sell but still occupies its space. e.g. false
counted_on date date never counted - the figure is aged from received_on instead, and reported as null when there is neither When this lot was last physically counted. The age of the figure, not its result - a quantity nobody has verified in six weeks is a different number from one counted this morning. e.g. 2026-07-27
count_variance_units number units not reported Book quantity minus counted quantity at that count. Positive means the book was overstating. Reported back as measured; never fitted into a rate, never applied to an order. e.g. -3

Demand history

demand_history optional array · key sku + date · 8 fields

One row per SKU per day. Synapse needs units_sold; everything else sharpens the estimate.

Field Type Unit Default Description
sku required string - - SKU the day of demand belongs to. e.g. MLK-2L
date required date date - The calendar day. One row per SKU per day; days you were closed still need a row. e.g. 2026-07-14
units_sold required number ≥ 0 units - Selling units sold or issued that day. e.g. 11
units_lost_to_stockout number ≥ 0 units unknown - de-censored from hours_out_of_stock instead Known unmet demand. The highest-value optional field in the contract: without it, days out of stock look like days of no demand and every forecast biases low. e.g. 3
hours_out_of_stock number 0-24 hours 0 Hours the item was unavailable. Used to scale the day up when lost sales are unknown. e.g. 6
units_spoiled number ≥ 0 units not reported Units written off that day. Reported back as measured waste, never fitted as demand. e.g. 2
promotion boolean - false Flags the day as non-representative of baseline demand. Kept for variance, dropped from the level fit. e.g. false
closed boolean - false Flags a day the site was shut. Excluded from the fit entirely so it does not read as zero demand. e.g. false

On order

on_order optional array · key po_id + sku · 6 fields

Stock already bought and not yet received, so Synapse does not buy it twice.

Field Type Unit Default Description
sku required string - - SKU already bought and not yet received. e.g. MLK-2L
qty_units required number ≥ 0 units - Selling units on the inbound line. Subtracted from what this plan needs to buy. e.g. 48
expected_on required date date - Date the stock is expected. Only inbound arriving inside the protection window counts. e.g. 2026-07-31
po_id string - no default Purchase-order identifier, echoed back for reconciliation. e.g. PO-88213
zone_id string - the allocator's choice Zone the inbound stock will be put away in, when it is already decided. e.g. COOLER-1
received_life_days number ≥ 0 days the product's received_life_days Life this particular shipment will arrive with, when it differs from the product default. e.g. 10

Suppliers

suppliers optional array · key supplier_id · 10 fields

Lead times, delivery days and order economics. Absent suppliers fall back to site defaults.

Field Type Unit Default Description
supplier_id required string - - Supplier identifier referenced by products. Plan lines are grouped into one order per supplier. e.g. VAL-DAIRY
name string - no default Human-readable supplier name. e.g. Northgate Supply Co.
lead_time_days number ≥ 0 days 3 Days from order to delivery for this supplier. e.g. 2
lead_time_sigma_days number ≥ 0 days 0 Standard deviation of this supplier's lead time. Unreliability is paid for in safety stock. e.g. 0.75
delivery_days number[] weekday every day ISO weekdays the supplier delivers, 1 = Monday to 7 = Sunday. When a delivery can land - not how often you order. e.g. [2,5]
order_weekdays number[] weekday the site review period ISO weekdays you place orders with this supplier, 1 = Monday to 7 = Sunday. Declaring it sizes each order to the real gap to your next one rather than to an average. e.g. [1,4]
order_cost number ≥ 0 currency 25 site default_order_cost Fixed cost of raising one purchase order with this supplier. The EOQ setup cost. e.g. 25
min_order_value number ≥ 0 currency 0 Order value below which the supplier will not ship. Reported when unmet; never silently padded. e.g. 250
max_order_value number ≥ 0 currency unlimited Credit or contract ceiling per order. e.g. 5000
free_freight_over number ≥ 0 currency none Free-freight threshold. Reported as an opportunity, never a reason to over-order. e.g. 400
Section 3

Classes, bands and capacity

A product may only be stored in a zone whose class is compatible with its own and whose temperature band contains the product’s. A product with no legal zone is a hard error - product.no_eligible_zone - and it is the single most common real problem in a first data feed.

Product storage_class Nominal band May be stored in a zone of class
ambient 5 to 30 °C ambient · controlled · cool
cool 8 to 18 °C cool · controlled · chilled
chilled 0 to 5 °C chilled
frozen -25 to -15 °C frozen · deep_frozen
deep_frozen -40 to -25 °C deep_frozen
controlled 15 to 25 °C controlled
hazmat 5 to 30 °C hazmat

A zone or product that declares its own temp_c overrides the nominal band. Widening a product’s band is how you tell Synapse it may live colder.

Capacity modes

A zone declares exactly one mode and the allocator only ever compares like with like. Supply the matching total, or supply levels and Synapse will add them up.

capacity_mode Total field Unit Meaning
volume capacity_volume_m3 m3 Usable cubic metres. Required for capacity_mode "volume" when levels are omitted.
pallets capacity_pallets pallets Whole pallet positions. Required for capacity_mode "pallets" when levels are omitted.
shelf_linear capacity_shelf_linear_m m Linear metres of shelf face. Required for capacity_mode "shelf_linear" when levels are omitted.
slots capacity_slots slots Countable identical bins, totes or cages. Required for capacity_mode "slots" when levels are omitted.

Segregation rules applied when you declare none

scope: zone

hazard:any  vs  goods:any

Hazardous goods may not share a location with ordinary stock

scope: above

raw_meat  vs  ready_to_eat

Raw meat may not be stored above ready-to-eat food (drip contamination)

scope: zone

hazard:flammable_liquid  vs  hazard:oxidiser

Flammable liquids and oxidisers may not share a zone

Declaring site.segregation_rules replaces this set entirely. zone rules block sharing a zone; above rules block stacking one above the other.

Section 4

CSV templates

One file per dataset. Every column is present, including the optional ones, so the shape of each field is self-evident. Delete the columns you do not have - Synapse defaults them and names each default it used.

These downloads are generated in your browser by the engine’s own csvTemplate() - the same function behind GET /v1/schema/csv/{dataset}. Nothing is fetched. Files are CRLF-terminated, RFC-4180 quoted; list-valued cells use | as the separator, because the comma is already taken.

Site

site.csv · 19 columns
site_id,name,currency,timezone,service_level,review_period_days,horizon_days,holding_cost_rate_annual,default_order_cost,default_lead_time_days,default_margin_pct,waste_tolerance_pct,budget,max_manual_lift_kg,order_weekdays,trading_minutes_by_weekday,off_cycle_order_cost,unbooked_movement_pct,receiving_capacity_m3_per_dayCEDAR-ST,Cedar Street Market,USD,America/Denver,0.95,7,14,0.25,25,3,0.3,2,12000,16,1|4,720|720|720|720|720|720|0,85,1.4,40

Zones

zones.csv · 18 columns
zone_id,name,storage_class,capacity_mode,temp_min_c,temp_max_c,humidity_min_pct,humidity_max_pct,capacity_volume_m3,capacity_pallets,capacity_shelf_linear_m,capacity_slots,max_load_kg,fill_target_pct,reserved_volume_m3,hazard_classes_allowed,segregation_groups_present,airflow_restrictedCOOLER-1,Chilled store,chilled,volume,1,4,80,90,18.5,12,24,40,4000,85,1.2,flammable_liquid|corrosive,hazard:corrosive,false

Products

products.csv · 45 columns
sku,name,category,storage_class,temp_min_c,temp_max_c,humidity_min_pct,humidity_max_pct,shelf_life_days,min_remaining_life_days,life_penalty_days_per_excursion,case_pack,case_volume_m3,case_length_cm,case_width_cm,case_height_cm,case_gross_weight_kg,case_max_stack,case_crush_sensitive,cases_per_pallet,facing_width_cm,units_per_slot,unit_volume_m3,unit_weight_kg,unit_cost,unit_price,salvage_value_per_unit,disposal_cost_per_unit,stockout_penalty_per_unit,hazard_class,hazard_un_number,hazard_segregation_group,hazard_requires_bunding,allergens,segregation_groups,supplier_id,lead_time_days,lead_time_sigma_days,moq_units,order_multiple_units,max_order_units,order_weekdays,discontinued,substitutes,received_life_daysMLK-2L,"Whole milk, 2 L",chilled_goods,chilled,0,5,45,65,14,5,2,6,0.021,30,20,35,12.4,5,false,48,20,4,0.0035,2.07,1.62,2.49,0.4,0.15,0.5,corrosive,UN1791,hazard:corrosive,true,peanut|milk,non_food,VAL-DAIRY,2,0.5,24,6,300,1|4,false,MLK-1L,12

Inventory

inventory.csv · 10 columns
sku,zone_id,qty_units,lot_id,expires_on,received_on,level,quarantined,counted_on,count_variance_unitsMLK-2L,COOLER-1,18,L204,2026-08-11,2026-07-28,1,false,2026-07-27,-3

Demand history

demand_history.csv · 8 columns
sku,date,units_sold,units_lost_to_stockout,hours_out_of_stock,units_spoiled,promotion,closedMLK-2L,2026-07-14,11,3,6,2,false,false

On order

on_order.csv · 6 columns
sku,qty_units,expected_on,po_id,zone_id,received_life_daysMLK-2L,48,2026-07-31,PO-88213,COOLER-1,10

Suppliers

suppliers.csv · 10 columns
supplier_id,name,lead_time_days,lead_time_sigma_days,delivery_days,order_weekdays,order_cost,min_order_value,max_order_value,free_freight_overVAL-DAIRY,Northgate Supply Co.,2,0.75,2|5,1|4,25,250,5000,400
# the same template this page downloads, served by the APIcurl -sS "$SYNAPSE_HOST/v1/schema/csv/products" -o products-template.csv# fill it in, then turn it into canonical rowscurl -sS -X POST "$SYNAPSE_HOST/v1/parse/csv?dataset=products" \  -H "Authorization: Bearer $SYNAPSE_KEY" \  -H "Content-Type: text/csv" \  --data-binary @products.csv
Section 5

Endpoints

7 routes, all under /v1. 3 of them need no credentials, because you should be able to read the contract before you sign anything.

Route Auth Request Response Status
GET /v1/health none - Service version, engine version, contract version, uptime. 200 · 405 · 500
GET /v1/schema none - The whole DATA_CONTRACT: every dataset, every field, the CSV headers and the aliases. This page is rendered from the same array. 200 · 405 · 500
GET /v1/schema/csv/{dataset} none dataset in the path text/csv - the header row and one example row for that dataset. 200 · 404 · 405 · 500
POST /v1/validate Bearer PlanInput ValidationResult: ok, the full issue list, the normalised input and record counts. A body that is a well-formed object answers 200 whether or not it is plannable - the report is the answer. Read ok, or the X-Synapse-Validation header. Add ?normalized=false to omit the echoed input. 200 · 400 · 401 · 405 · 413 · 422 · 500 · 503
POST /v1/plan Bearer PlanInput Plan: orders, lines, zones, risks, summary, warnings, assumptions. The main route, and the one that answers 422 when the input breaks the contract. 200 · 400 · 401 · 405 · 413 · 422 · 500 · 503
POST /v1/simulate Bearer PlanInput plus scenarios[] - a label and a shallow patch over site, zones or options The base summary and one summary and delta per scenario. 200 · 400 · 401 · 405 · 413 · 422 · 500 · 503
POST /v1/parse/csv?dataset=… Bearer text/csv, with ?dataset= naming the dataset Canonical JSON rows for that dataset plus every issue raised while reading the file. A header Synapse cannot place is a warning, not a refusal - the column survives under _extra. 200 · 401 · 405 · 413 · 422 · 500
curl -sS -X POST "$SYNAPSE_HOST/v1/plan" \  -H "Authorization: Bearer $SYNAPSE_KEY" \  -H "Content-Type: application/json" \  -H "Idempotency-Key: cedar-st-2026-07-29" \  --data-binary @plan-input.json

Authentication

Authorization: Bearer <key>. Keys are held only as SHA-256 digests and compared in constant time; the plaintext key never reaches a log line. With no keys configured the service runs in open development mode and every response carries "warning": "unauthenticated dev mode" so you can never mistake one for the other. Keys are managed in the customer portal.

Limits

6 MB per body, 20,000 products, 2,000,000 history rows, 500,000 inventory rows, 12 scenarios per /v1/simulate call. Past any of those you get a 413 telling you to page by SKU rather than a truncated answer. A wall-clock guard returns 503 time_budget_exhausted with a retry_with hint instead of timing out.

Idempotency

Send Idempotency-Key if it helps your plumbing, but planning is pure: identical inputs always yield the identical plan_id, which is echoed as X-Synapse-Plan-Id. Retrying is free and cannot produce a second order.

Headers and CORS

Every response carries X-Synapse-Engine-Version, X-Synapse-Contract-Version, X-Synapse-Request-Id and Cache-Control: no-store; plans add X-Synapse-Plan-Id and validations add X-Synapse-Validation. CORS allows POST, GET, OPTIONS with Authorization, Content-Type, Idempotency-Key, against the SYNAPSE_ALLOWED_ORIGINS allowlist - never a wildcard with credentials. Every route answers the OPTIONS preflight with a 204.

Response vocabularies

Two closed sets you will want to switch on. Both are unions in types.mjs at contract 1.2.0; unlike every other table on this page they are quoted rather than iterated, because they are documentation types with no runtime value to read.

line.constrained_by

none shelf_life zone_capacity zone_weight budget max_order supplier_max no_eligible_zone segregation receiving_capacity discontinued marginal_value

risks[].type

expiring_stock dead_stock no_eligible_zone temperature_mismatch segregation_conflict overstocked stockout_imminent capacity_exhausted data_gap off_cycle_risk shelf_life_shorter_than_cycle storage_not_modelled hazard_not_checked
Section 6

A worked example, computed here

5 SKUs, 2 zones, 30 days of history, 150 daily rows. The request below is complete. The response below it is what the engine returned when this page loaded - the real engine, in your browser, not a transcript.

The dataset is invented for this page: one small fictional shop, chosen because a concrete example is easier to read than an abstract one. Nothing about the contract is specific to it - the same fields describe a warehouse, a clinic or a workshop, and storage is optional. Nothing here is anyone’s real data. as_of is pinned to 2026-07-29, which is why every visitor sees the same plan_id.

Request

POST /v1/plan · 955 lines · 17,516 bytes
{  "site": {    "site_id": "CEDAR-ST",    "name": "Cedar Street Market",    "currency": "USD",    "service_level": 0.95,    "review_period_days": 7,    "budget": 2500  },  "zones": [    {      "zone_id": "COOLER-1",      "name": "Walk-in cooler",      "storage_class": "chilled",      "capacity_mode": "volume",      "temp_c": {        "min": 1,        "max": 4      },      "fill_target_pct": 85,      "levels": [        {          "level": 1,          "area_m2": 0.95,          "clear_height_cm": 55,          "max_load_kg": 400        },        {          "level": 2,          "area_m2": 0.95,          "clear_height_cm": 45,          "max_load_kg": 250        }      ]    },    {      "zone_id": "DRY-1",      "name": "Dry goods aisle",      "storage_class": "ambient",      "capacity_mode": "volume",      "capacity_volume_m3": 14,      "fill_target_pct": 85,      "max_load_kg": 3000    }  ],  "products": [    {      "sku": "MLK-2L",      "name": "Whole milk 2 L",      "category": "dairy",      "storage_class": "chilled",      "shelf_life_days": 14,      "min_remaining_life_days": 5,      "case_pack": 6,      "unit_cost": 1.94,      "unit_price": 3.49,      "supplier_id": "VAL-DAIRY",      "lead_time_days": 2,      "case": {        "volume_m3": 0.018,        "gross_weight_kg": 12.4,        "max_stack": 4      }    },    {      "sku": "SAL-CHK",      "name": "Chicken caesar salad",      "category": "prepared",      "storage_class": "chilled",      "shelf_life_days": 5,      "min_remaining_life_days": 2,      "case_pack": 4,      "unit_cost": 3.1,      "unit_price": 7.5,      "salvage_value_per_unit": 0.5,      "supplier_id": "VAL-DAIRY",      "lead_time_days": 1,      "case": {        "volume_m3": 0.012,        "gross_weight_kg": 3.2,        "max_stack": 3,        "crush_sensitive": true      }    },    {      "sku": "BTR-250",      "name": "Cultured butter 250 g",      "category": "dairy",      "storage_class": "chilled",      "shelf_life_days": 60,      "min_remaining_life_days": 14,      "case_pack": 12,      "unit_cost": 2.4,      "unit_price": 4.25,      "supplier_id": "VAL-DAIRY",      "lead_time_days": 2,      "case": {        "volume_m3": 0.011,        "gross_weight_kg": 3.4,        "max_stack": 6      }    },    {      "sku": "FLR-20K",      "name": "Bread flour 20 kg",      "category": "bakery",      "storage_class": "ambient",      "case_pack": 1,      "unit_cost": 18.5,      "unit_price": 27,      "supplier_id": "MILL-CO",      "lead_time_days": 4,      "moq_units": 10,      "case": {        "volume_m3": 0.032,        "gross_weight_kg": 20.4,        "max_stack": 5      }    },    {      "sku": "NAP-1200",      "name": "Paper napkins, 1200 ct",      "category": "consumables",      "storage_class": "ambient",      "case_pack": 4,      "unit_cost": 5.2,      "unit_price": 7.9,      "supplier_id": "MILL-CO",      "lead_time_days": 4,      "case": {        "volume_m3": 0.09,        "gross_weight_kg": 7.1,        "max_stack": 4      }    }  ],  "inventory": [    {      "sku": "MLK-2L",      "lot_id": "L204",      "zone_id": "COOLER-1",      "qty_units": 24,      "expires_on": "2026-08-02"    },    {      "sku": "SAL-CHK",      "lot_id": "L311",      "zone_id": "COOLER-1",      "qty_units": 8,      "expires_on": "2026-07-30"    },    {      "sku": "BTR-250",      "lot_id": "L118",      "zone_id": "COOLER-1",      "qty_units": 30,      "expires_on": "2026-09-07"    },    {      "sku": "FLR-20K",      "lot_id": "L900",      "zone_id": "DRY-1",      "qty_units": 6    },    {      "sku": "NAP-1200",      "lot_id": "L901",      "zone_id": "DRY-1",      "qty_units": 10    }  ],  "demand_history": [    {      "sku": "MLK-2L",      "date": "2026-06-29",      "units_sold": 21    },    {      "sku": "SAL-CHK",      "date": "2026-06-29",      "units_sold": 7    },    {      "sku": "BTR-250",      "date": "2026-06-29",      "units_sold": 5    },     147 more daily rows - one per SKU per day  ],  "on_order": [    {      "sku": "MLK-2L",      "po_id": "PO-88213",      "qty_units": 24,      "expected_on": "2026-07-30"    }  ],  "suppliers": [    {      "supplier_id": "VAL-DAIRY",      "name": "Valley Dairy Co-op",      "lead_time_days": 2,      "lead_time_sigma_days": 0.5,      "delivery_days": [        2,        5      ],      "min_order_value": 150    },    {      "supplier_id": "MILL-CO",      "name": "Millwright Supply",      "lead_time_days": 4,      "order_cost": 35,      "min_order_value": 300    }  ],  "options": {    "as_of": "2026-07-29"  }}

147 of the 150 demand_history rows are elided above for legibility. The download is the complete body, byte for byte.

Response

200 OK · 1267 lines · 48,653 bytes
{  "plan_id": "syn_10snatz",  "engine_version": "0.2.1",  "generated_for": "2026-07-29",  "site_id": "CEDAR-ST",  "horizon_days": 11,  "orders": [    {      "supplier_id": "MILL-CO",      "name": "Millwright Supply",      "deliver_on": "2026-08-02",      "total_cost": 638.2,      "line_count": 2,      "meets_minimum": true,      "lines": [        {          "sku": "FLR-20K",          "name": "Bread flour 20 kg",          "supplier_id": "MILL-CO",          "order_units": 30,          "order_cases": 30,          "line_cost": 555,          "zone_id": "DRY-1",          "level": 1,          "ideal_units": 31,          "shelf_life_cap_units": null,          "constrained_by": "marginal_value",          "gap_attribution": {            "marginal_value": 1          },          "on_hand_units": 6,          "on_hand_usable_units": 6,          "on_order_units": 0,          "days_of_cover": 15.3,          "expected_spoilage_units": 0,          "expected_stockout_units": 0.04,          "service_level_achieved": 0.978,          "space_m3": 1.3333,          "weight_kg": 612,          "value_score": 162.07,          "why": [            "Stopped at 30 units. Past that the next case costs more to hold and risk than it earns back. The last 1 unit were cut.",            "Demand 2.3 units/day, flat across the week.",            "Lead time 4 days + 7-day review window = 11 days of exposure.",            "6 units on hand.",            "Order 30 units at $555 - 15.3 days of cover.",            "It takes 1.33 m³ of Dry goods aisle and adds 612 kg.",            "97.8 % chance of getting through the window without a gap.",            "Put away on level 1 of Dry goods aisle: at 20.4 kg the case is over the 16 kg manual-lift limit, so it may not go above 140 cm.",            "Forecast confidence 0.20 on 30 days of history - treat the quantity as a starting point, not a settled number.",            "Fitted with exponential smoothing (alpha 0.3, damped trend phi 0.85) over 30 day(s).",            "Demand is falling by 0.007 unit(s)/day. The trend is reported, not compounded into the horizon."          ],          "protection_window_days": 11,          "protection_window_source": "review_period",          "exposure_days": 11,          "service_window_days": 11,          "days_to_delivery": 4,

Showing the first 58 of 1267 lines. orders[].lines and the flat lines[] are the same lines viewed two ways, which is most of the length.

The same response, read as a buyer would read it

Order cost

$990.60

210 units · 5 of 5 SKUs

Expected spoilage

$4.69

1.9 units over the window

Expected stockout

$155.63

82.9 units of demand unmet

Cost of the building

$128.04

unconstrained $1,118.64 minus what fits

unconstrained_cost is the same plan costed with infinite space. The gap is stock this business would have bought and could not put anywhere - 1 line(s) cut by capacity, 1 by shelf life.

SKU Order Ideal Life cap Cover Zone Bound by Why
FLR-20K Bread flour 20 kg 30 30 cases · $555 31 - 15.3 d DRY-1 level 1 marginal value
MLK-2L Whole milk 2 L 120 20 cases · $233 252 210 6.1 d COOLER-1 level 1 zone capacity
NAP-1200 Paper napkins, 1200 ct 16 4 cases · $83 20 - 15.4 d DRY-1 level 1 marginal value
SAL-CHK Chicken caesar salad 20 5 cases · $62 108 23 2.3 d COOLER-1 level 1 shelf life
BTR-250 Cultured butter 250 g 24 2 cases · $58 48 230 9.4 d COOLER-1 level 1 marginal value
Zone Mode Capacity Occupied This plan Used after Shadow price
COOLER-1 binding volume 0.81 0.18 0.61 98.2 % $289/wk
DRY-1 volume 11.90 0.64 1.83 20.8 % -

Capacity is in the zone’s own unit, already net of fill_target_pct and reservations. The shadow price is what one more unit of that zone’s capacity would have earned this week - the number that says whether renting more cold space pays. The building you already have is sunk: space enters a line’s price only as opportunity cost, and only when it actually binds.

risks[] - 12

  • high · expiring_stock

    Lot L311 holds 8 units of Chicken caesar salad (SAL-CHK) that expire in 1 day and will not sell in time - $21 at cost.

    Mark it down, move it, or donate it today.

  • high · stockout_imminent

    Whole milk 2 L (MLK-2L) has 0 days of stock left and the next delivery is 2 days out - about 46.9 units of demand will go unmet.

    Expedite, substitute, or accept the gap and tell the floor.

  • high · stockout_imminent

    Chicken caesar salad (SAL-CHK) has 0 days of stock left and the next delivery is 1 day out - about 8.6 units of demand will go unmet.

    Expedite, substitute, or accept the gap and tell the floor.

  • medium · data_gap

    Forecast confidence for Cultured butter 250 g (BTR-250) is 0.23 on 30 days of history. The quantity is a starting point, not a settled number.

    Send more history, or de-censor the stockout days with units_lost_to_stockout.

  • medium · data_gap

    Forecast confidence for Bread flour 20 kg (FLR-20K) is 0.20 on 30 days of history. The quantity is a starting point, not a settled number.

    Send more history, or de-censor the stockout days with units_lost_to_stockout.

7 more in the full response.

assumptions[] - 24

Everything the engine had to invent because the input did not say.

  • No carrying cost was given, so Synapse used DEFAULTS.holding_cost_rate_annual = 0.25 (25 % of unit cost per year) to price holding non-perishables.
  • Where neither the product nor its supplier gave a lead time, Synapse used DEFAULTS.lead_time_days = 3 days.
  • No waste tolerance was set, so Synapse capped perishable orders at DEFAULTS.waste_tolerance_pct = 2 % expected spoilage. Raise it if you would rather risk waste than run out.
  • No manual-lift limit was set, so Synapse used DEFAULTS.max_manual_lift_kg = 16 kg - heavier cases are not placed above 140 cm.
  • No segregation rules were supplied, so Synapse applied its default set: hazardous goods may not share a zone with food, raw meat may not sit above ready-to-eat food, and flammable liquids may not share a zone with oxidisers (3 rules).

19 more in the full response.

plan_id syn_10snatz · engine 0.2.1 · generated_for 2026-07-29 · horizon 11 days · 0 validation warning(s)

Section 7

Errors

One shape, always. The point of a 422 is that you can fix your feed from the response alone: every issue carries a stable code, a path into your input, a message and a hint naming the fix.

code HTTP When
invalid_json 400 The body is not JSON, or is not an object. Nothing is parsed and nothing is planned.
unauthorized 401 The bearer token is missing or does not match a configured key. Keys are compared as SHA-256 digests in constant time; key material is never logged.
not_found 404 No such route, or no such dataset on /v1/schema/csv/{dataset}.
method_not_allowed 405 A GET route called with POST, or the reverse.
payload_too_large 413 Body over 6 MB, more than 20,000 products, 2,000,000 history rows, or 500,000 inventory rows - or more than 12 scenarios in one /v1/simulate call. The message tells you to page by SKU.
validation_failed 422 The input broke the contract. The response carries the complete ValidationIssue[] - level, code, path, message, hint - so a feed can be fixed from the response alone. /v1/plan answers 422; /v1/validate answers 200 and puts the same issues in the body.
internal 500 A defect on our side. The request_id in the body is the one to quote.
time_budget_exhausted 503 The wall-clock guard refused the work rather than letting the request time out. The details carry a retry_with hint describing how to split the job.
{  "error": {    "code": "validation_failed",    "message": "3 fields need fixing before this input can be planned.",    "details": [      {        "level": "error",        "code": "product.no_eligible_zone",        "path": "products[7].storage_class",        "message": "No zone can hold SAL-CHK: it needs chilled and no chilled zone was declared.",        "hint": "Add a chilled zone, or widen the product's temp_c band."      }    ],    "request_id": "01JZ8Q4T2N7R"  }}

Warnings never block a plan. Unknown columns, ragged CSV rows and missing optional fields all come back as level: "warning" in plan.warnings, and the plan is computed anyway.

Section 8

What we guarantee

Deterministic

The same input produces byte-identical output, on any machine, at any hour. plan_id is a hash over the canonical serialisation of your input and the engine version, so an unchanged plan_id is proof that nothing changed. There is exactly one clock reading in the whole engine: when you omit options.as_of it falls back to today in UTC. Pass as_of and the answer is reproducible forever - that is how this page shows the same plan_id to everyone.

Stateless by default

Every input a plan needs is in the request body. The service reads no database to answer you and holds nothing afterwards. Plan storage is off unless an account explicitly configures a table, and where it is on, records carry a 90-day expiry.

Units are explicit

Every field name carries its unit and no unit is ever inferred. There is no "quantity" in this contract: there is qty_units, case_pack, capacity_volume_m3, case_gross_weight_kg. Ambiguity about whether a number means cases or eaches is how an order comes out six times too large.

Every default is named

Anything the engine had to invent because your data did not say comes back in assumptions[], in plain language, citing the default by name. A silent default is a lie you find out about at the loading dock.

Every cut is attributed

A line smaller than the unconstrained ideal names the constraint that bound it, and the ideal it was cut from. summary.unconstrained_cost is the same plan costed with infinite space, so the difference is what the building costs you this cycle, in money.

No dependencies

The engine is pure ESM JavaScript with zero runtime dependencies and no Node built-ins. It runs identically in the Lambda, in a browser tab and in a test process - the worked example on this page is the real engine running in yours.

The 16 named defaults

Read straight out of DEFAULTS. When one of these is used because your input was silent, the plan says so by name in assumptions[].

DEFAULTS key Value
airflow_efficiency 0.9
fill_target_pct 85
holding_cost_rate_annual 0.25
ladder_height_cm 180
lead_time_days 3
margin_pct 0.3
max_manual_lift_kg 16
max_stack 5
min_remaining_life_days 0
order_cost 25
perishable_life_threshold_days 120
review_period_days 7
service_level 0.95
shoulder_height_cm 140
volumetric_efficiency 0.72
waste_tolerance_pct 2

Modelling a real building

A design partner gets their data modelled, a plan run against their actual zones and their actual history, and the shadow prices that say what more space would be worth. No charge during the pilot.