{
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "$id": "https://apiv2.blizzfull.com/schema/blizzfullMenuSchema.json",
    "title": "Blizzfull Menu (import)",
    "description": "A whole menu, as a point of sale or partner sends it to Blizzfull to create or update a restaurant's menu.\n\nHow an import works:\n- Every ID is yours (the sender's): categories, items, sizes, option groups and options. Blizzfull remembers which of its records each of your IDs became, so importing the same IDs again updates those records in place. Keep IDs stable. An option's ID only needs to be unique within its group.\n- Arrays are ordered: the menu shows categories, items, groups and options in the order you send them.\n- Prices are dollars as numbers (`9.99`), not cents.\n- Option groups are defined once in `option_groups` and attached to items (or whole categories) by ID through `groups`. Only groups that something attaches are imported; the same goes for `sizes`.\n- Unknown fields are ignored, so you can send extra data.",
    "$comment": "This file also validates imports at runtime with a draft-04 validator (justinrainbow/json-schema 5.2), so it uses only keywords that validator honours or ignores: refs are #/$defs/... pointers, single values are one-value enums, and required sits on objects. See docs/feature/api-docs.md.",
    "type": "object",
    "required": ["categories", "option_groups", "sizes"],
    "properties": {
        "categories": {
            "description": "The menu's categories, in display order. Each holds items and, one level deep, subcategories (`children`).",
            "type": "array",
            "items": { "$ref": "#/$defs/category" }
        },
        "option_groups": {
            "description": "Every option group the menu uses (toppings, sauces, sides, cooking temperatures), defined once and attached to items and categories by ID. Send an empty array when there are none.",
            "type": "array",
            "items": { "$ref": "#/$defs/option_group" }
        },
        "sizes": {
            "description": "Every size the menu uses (Small, 12\", 6 Pcs), defined once and attached to items by ID with a price for each item. Send an empty array when there are none. An object keyed by size ID is also accepted.",
            "type": ["array", "object"],
            "items": { "$ref": "#/$defs/size" },
            "additionalProperties": { "$ref": "#/$defs/size" }
        },
        "merchant": {
            "title": "Restaurant settings",
            "description": "Optional settings for the restaurant itself rather than its menu. Sending this changes the live restaurant immediately.",
            "type": "object",
            "properties": {
                "hours": {
                    "title": "Ordering hours",
                    "description": "When the restaurant takes orders, separately for delivery and pickup.",
                    "type": "object",
                    "properties": {
                        "delivery": { "$ref": "#/$defs/store_hours" },
                        "pickup": { "$ref": "#/$defs/store_hours" }
                    },
                    "additionalProperties": false
                },
                "paused": {
                    "title": "Ordering paused",
                    "description": "True stops the restaurant taking orders; false resumes.",
                    "type": "boolean"
                }
            }
        },
        "custom_maps": {
            "description": "Extra ID mappings a point-of-sale integration keeps with the menu (for example, how its modifier combinations map to its own items). Stored as given; Blizzfull does not interpret it.",
            "type": ["object", "array"]
        },
        "enforceSyncCompatibility": {
            "description": "Only for `POST /menu/importExternalPOSMenu/{posName}`: the menu-sync version you built against. The current version is `1`; an import is refused if it doesn't match. Ignored by other imports.",
            "type": "integer"
        }
    },
    "$defs": {
        "id": {
            "description": "Your ID for the record. Must be stable between imports and must not be 0 or empty.",
            "type": ["integer", "string"]
        },
        "images": {
            "description": "Image URLs: one string, or an array of strings. Each must be absolute, public and point at an image; Blizzfull downloads them during the import, and a URL that is not an image stops the import.",
            "type": ["string", "array"],
            "items": { "type": "string" },
            "examples": ["https://example.com/images/wings.jpg"]
        },
        "store_hours": {
            "description": "Ordering hours by weekday, or for one date (`MM/DD`). Each value is a list of `[open, close]` ranges in 24-hour `HH:MM`. A weekday you leave out is closed.",
            "type": "object",
            "patternProperties": {
                "^(sunday|monday|tuesday|wednesday|thursday|friday|saturday|\\d{2}/\\d{2})$": {
                    "type": "array",
                    "items": { "$ref": "#/$defs/time_range" }
                }
            },
            "additionalProperties": false,
            "examples": [{ "monday": [["11:00", "14:30"], ["17:00", "21:00"]], "12/25": [] }]
        },
        "time_range": {
            "description": "An `[open, close]` pair in 24-hour `HH:MM`.",
            "type": "array",
            "items": { "type": "string" },
            "minItems": 2,
            "maxItems": 2,
            "examples": [["11:00", "21:30"]]
        },
        "category_hours": {
            "description": "When a category can be ordered, by weekday: `{\"monday\": [[\"11:00\", \"14:45\"]]}`. Only the first range of each day is used. Times run from 04:00 to 28:00 (4 AM the next morning), and `[\"00:00\", \"00:00\"]` closes the day. A weekday you leave out is closed, so `{}` hides the category all week. Leave `hours` out entirely for a category available whenever the restaurant is open.",
            "type": "object",
            "patternProperties": {
                "^(sunday|monday|tuesday|wednesday|thursday|friday|saturday)$": {
                    "type": "array",
                    "items": { "$ref": "#/$defs/time_range" }
                }
            }
        },
        "category": {
            "title": "Category",
            "description": "A section of the menu holding items, and optionally one level of subcategories.",
            "type": "object",
            "required": ["id", "name"],
            "properties": {
                "id": { "$ref": "#/$defs/id" },
                "name": { "type": "string" },
                "description": { "type": "string" },
                "active": {
                    "description": "False hides the category, whatever its hours. Defaults to true. Applied when an existing category is updated; a newly created category is always shown.",
                    "type": "boolean"
                },
                "hours": { "$ref": "#/$defs/category_hours" },
                "imageURL": { "$ref": "#/$defs/images" },
                "imageUrl": { "$ref": "#/$defs/images", "description": "Older spelling of `imageURL`, used when `imageURL` is absent." },
                "groups": {
                    "description": "Option groups attached to every item in the category, placed before each item's own groups.",
                    "type": "array",
                    "items": { "$ref": "#/$defs/option_group_link" }
                },
                "items": {
                    "description": "The category's items, in display order. An item ID that appears in more than one category is imported once, in the last category it appears in.",
                    "type": "array",
                    "items": { "$ref": "#/$defs/item" }
                },
                "children": {
                    "description": "Subcategories. One level only: a subcategory's own `children` are ignored.",
                    "type": "array",
                    "items": { "$ref": "#/$defs/category" }
                }
            }
        },
        "item": {
            "title": "Menu item",
            "description": "Something a customer can order, with a flat price or a price per size, and the option groups that customize it.",
            "type": "object",
            "required": ["id", "name", "price", "groups"],
            "properties": {
                "id": { "$ref": "#/$defs/id" },
                "name": { "type": "string" },
                "description": { "type": ["string", "null"] },
                "price": {
                    "description": "The base price in dollars. For a sized item, each size's price is added to this.",
                    "type": "number",
                    "examples": [12.5]
                },
                "active": {
                    "description": "`true` (or left out): on the menu. `false`: hidden. A Unix timestamp: sold out (86'd) until then. `0`: sold out until turned back on.",
                    "type": ["boolean", "integer"],
                    "examples": [true, 1793142409]
                },
                "availableDates": {
                    "description": "Limits the item to date windows, in the restaurant's time zone. Both dates are included. Leave it out for an item available every day.",
                    "type": "array",
                    "items": {
                        "type": "object",
                        "required": ["start", "end"],
                        "properties": {
                            "start": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "description": "First day, `YYYY-MM-DD`." },
                            "end": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "description": "Last day, `YYYY-MM-DD`." }
                        }
                    },
                    "examples": [[{ "start": "2026-11-26", "end": "2026-12-31" }]]
                },
                "tags": {
                    "description": "Dietary and other badges. Recognized: `spicy`, `specialty`, `vegan`, `gf` (gluten free), `keto`, `alcoholic`, and the upper-case forms `HOT`, `SPICY`, `SPECIALTY`, `VEGAN`, `GLUTEN_FREE`. Anything else is ignored, including `vegetarian`: send `vegan`.",
                    "type": "array",
                    "items": { "type": "string" },
                    "examples": [["spicy", "gf"]]
                },
                "imageURL": { "$ref": "#/$defs/images" },
                "imageUrl": { "$ref": "#/$defs/images", "description": "Older spelling of `imageURL`, used when `imageURL` is absent." },
                "sizes": {
                    "description": "The sizes this item comes in, each with its price for this item. The item's displayed price becomes its cheapest size.",
                    "type": "array",
                    "items": { "$ref": "#/$defs/item_size" }
                },
                "groups": {
                    "description": "The option groups that customize this item, in display order. Send an empty array when there are none.",
                    "type": "array",
                    "items": { "$ref": "#/$defs/option_group_link" }
                },
                "sku": { "description": "Your SKU, kept for loyalty-program matching.", "type": "string" },
                "loyaltyMap": { "description": "Loyalty-program product codes for the item, comma separated.", "type": "string", "examples": ["478730,478734"] },
                "loyaltyTags": {
                    "description": "Loyalty-program tags for the item: an array, or one comma-separated string.",
                    "type": ["array", "string"],
                    "items": { "type": "string" }
                }
            }
        },
        "item_size": {
            "title": "Item size",
            "description": "One size of this item and what it adds to the item's price.",
            "type": "object",
            "required": ["id", "price"],
            "properties": {
                "id": { "$ref": "#/$defs/id", "description": "A size from the top-level `sizes`, or a new size defined here with `name`." },
                "price": {
                    "description": "Dollars added to the item's `price` for this size. With a base price of 5 and a size price of 1, the size costs 6.",
                    "type": "number"
                },
                "name": { "description": "The size's name, when it is not in the top-level `sizes`.", "type": "string" }
            }
        },
        "size": {
            "title": "Size",
            "description": "A size, priced per item where items attach it.",
            "type": "object",
            "required": ["id"],
            "properties": {
                "id": { "$ref": "#/$defs/id" },
                "name": { "type": "string", "examples": ["Large 16\""] }
            }
        },
        "option_group": {
            "title": "Option group",
            "description": "A set of choices, such as wing flavors or burger toppings. How customers choose depends on `type`.",
            "type": "object",
            "required": ["id", "name", "options"],
            "properties": {
                "id": { "$ref": "#/$defs/id" },
                "name": { "type": "string" },
                "type": {
                    "description": "`TYPE_SINGLE_SELECT`: pick one. `TYPE_MULTI_SELECT`: pick any. `TYPE_UNIT_STEPPER`: pick a quantity of each. `TYPE_VARIABLE_AMOUNT`: pick a level of each (light, regular, extra). When left out, it is inferred: `allow_quantity` above 0 makes a stepper, `force_min` and `force_max` both 1 make a single select, anything else a multi select.",
                    "type": "string",
                    "enum": ["TYPE_SINGLE_SELECT", "TYPE_MULTI_SELECT", "TYPE_UNIT_STEPPER", "TYPE_VARIABLE_AMOUNT"]
                },
                "allow_quantity": { "description": "Used only to infer `type` when it is absent: above 0 means a unit stepper.", "type": "number" },
                "force_min": { "description": "Used only to infer `type` when it is absent. Set limits per item on the group link instead.", "type": "number" },
                "force_max": { "description": "Used only to infer `type` when it is absent. Set limits per item on the group link instead.", "type": "number" },
                "options": {
                    "description": "The choices, in display order. Send an empty array for a group with none.",
                    "type": "array",
                    "items": { "$ref": "#/$defs/option" }
                }
            }
        },
        "option": {
            "title": "Option",
            "description": "One choice in an option group, such as Buffalo or Extra Cheese.",
            "type": "object",
            "required": ["id", "name", "price"],
            "properties": {
                "id": { "$ref": "#/$defs/id", "description": "Your ID for the option, unique within its group." },
                "name": { "type": "string" },
                "price": { "description": "What the option adds, in dollars. May be 0 or negative.", "type": "number" },
                "inactiveEnds": {
                    "description": "Sold out until this Unix time. Leave it out for an available option. Applied when an existing option is updated, not when it is first created.",
                    "type": "number"
                },
                "min_units": { "description": "For a `TYPE_UNIT_STEPPER` group: the fewest of this option a customer may choose.", "type": "integer" },
                "max_units": { "description": "For a `TYPE_UNIT_STEPPER` group: the most of this option a customer may choose.", "type": "integer" },
                "sub_options": {
                    "description": "Option groups that open when this option is chosen (choosing Burger opens Cooking temperature). Each group must be in `option_groups` and also attached through some item's group link `options[].sub_options`, or it is skipped.",
                    "type": "array",
                    "items": {
                        "type": "object",
                        "required": ["id"],
                        "properties": { "id": { "$ref": "#/$defs/id", "description": "An option group's ID." } }
                    }
                },
                "sort": { "description": "Ignored: options keep the order they are sent in.", "type": "number", "deprecated": true }
            }
        },
        "option_group_link": {
            "title": "Option group link",
            "description": "Attaches an option group to an item or category, with any rules and defaults for that item.",
            "type": "object",
            "required": ["id"],
            "properties": {
                "id": { "$ref": "#/$defs/id", "description": "The option group's ID." },
                "required": {
                    "description": "True means the customer must choose from this group. Also accepts 1, \"1\" and \"true\".",
                    "type": ["boolean", "integer", "string"]
                },
                "force_min": {
                    "description": "For multi-select groups, the fewest options a customer must choose; for unit steppers, the fewest units. Ignored for single-select and variable-amount groups.",
                    "type": "number"
                },
                "force_max": {
                    "description": "For multi-select groups, the most options a customer may choose; for unit steppers, the most units in total. Ignored for single-select and variable-amount groups.",
                    "type": "number"
                },
                "free_options": {
                    "description": "How many choices are included free before options start costing extra. Options selected by default don't count. Ignored for single-select groups.",
                    "type": "number"
                },
                "options": {
                    "description": "Per-item settings for particular options of the group: defaults, size prices and nested groups. It does not limit which options appear; every option in the group is offered.",
                    "type": "array",
                    "items": { "$ref": "#/$defs/option_link" }
                }
            }
        },
        "option_link": {
            "title": "Option settings for an item",
            "description": "Settings for one option of the group, on this item only.",
            "type": "object",
            "required": ["id"],
            "properties": {
                "id": { "$ref": "#/$defs/id", "description": "The option's ID within the group." },
                "default": {
                    "description": "Pre-selects the option: the quantity or level, or true. 0 or false leaves it unselected. A single-select group with no defaults pre-selects its first option.",
                    "type": ["integer", "boolean"]
                },
                "sizePriceOverrides": {
                    "description": "This option's price when the item is a given size.",
                    "type": "array",
                    "items": {
                        "type": "object",
                        "required": ["id", "price"],
                        "properties": {
                            "id": { "$ref": "#/$defs/id", "description": "The size's ID." },
                            "price": {
                                "description": "The option's price in dollars for this size, a percentage of its normal price as a string (`\"50%\"`), or `-9999` to make the option unavailable in this size.",
                                "type": ["number", "string"],
                                "examples": [1.5, "50%", -9999]
                            }
                        }
                    }
                },
                "sub_options": {
                    "description": "Nested groups this option opens on this item, with their defaults. This is what attaches a nested group.",
                    "type": "array",
                    "items": {
                        "type": "object",
                        "required": ["id"],
                        "properties": {
                            "id": { "$ref": "#/$defs/id", "description": "The nested option group's ID." },
                            "options": {
                                "description": "Defaults inside the nested group.",
                                "type": "array",
                                "items": {
                                    "type": "object",
                                    "required": ["id"],
                                    "properties": {
                                        "id": { "$ref": "#/$defs/id" },
                                        "default": { "type": ["integer", "boolean"] }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
