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.
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.
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
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 |
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.
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
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
risks[].type
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)
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.
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.