{
  "components": {
    "schemas": {
      "ApiAccessError": {
        "description": "The token authenticated and its shop’s plan does not include the API. It describes the token’s OWN shop and nothing about any other, which is why it carries a body where the 401, 404 and 429 do not.",
        "properties": {
          "error": {
            "description": "Why the request was refused.",
            "enum": [
              "api-not-on-plan"
            ],
            "type": "string"
          }
        },
        "required": [
          "error"
        ],
        "type": "object"
      },
      "BoxRow": {
        "additionalProperties": false,
        "description": "One box, as a PATCH. Only `name` is required, because the row is merged over whatever is already stored before the rules run: a row carrying a name and `costAmount` changes the cost and nothing else. An absent key leaves the stored value alone; an explicit `null` clears one of the fields that allows it. An unknown key is a `row.unknownField` refusal rather than a silent drop, because a payload saying `weight` when it means `maxWeight` is a column the merchant believes they are pushing. A value of the wrong JSON type is refused too and never coerced: `\"length\": \"10\"` is `row.wrongType`, so a locale that writes `10,5` cannot become 105. BLANK, EXACTLY. Absent is \"leave it\". `null` clears, and only on the four fields that can be empty - `minimumHeight`, `maxWeight`, `emptyWeight` and `costAmount`; `null` anywhere else is `row.wrongType`. `costAmount: \"\"` (or only spaces) is stored as `null`, the same as sending `null`. `\"\"` in a number field is `row.wrongType`, in `unit` or `wallThicknessUnit` it is `box.unit.unknown`, in `weightUnit` it is `box.weightUnit.unknown`, and as `name` it is `box.name.missing`. This is the OPPOSITE of `/api/dimensions`, where a row replaces what is stored and a field left out is reset.",
        "properties": {
          "active": {
            "description": "A deactivated box keeps its history and is not offered to the packer. Defaults to true on create. `false` is how a feed RETIRES a box, because this API has no delete; `true` brings it back.",
            "type": "boolean"
          },
          "costAmount": {
            "description": "Added to every rate quoted for this box. A DECIMAL STRING and not a number, so money never drifts through a binary float. Null, `\"\"` or only spaces clears it.",
            "type": [
              "string",
              "null"
            ]
          },
          "emptyWeight": {
            "description": "What the empty box weighs, in `weightUnit`. Null if not tracked.",
            "type": [
              "number",
              "null"
            ]
          },
          "height": {
            "description": "Interior height, in `unit`.",
            "type": "number"
          },
          "length": {
            "description": "Interior length, in `unit`.",
            "type": "number"
          },
          "maxWeight": {
            "description": "The most this box may weigh once packed, its own empty weight included, in `weightUnit`. Null means no cap of the merchant’s own; the carrier’s still apply.",
            "type": [
              "number",
              "null"
            ]
          },
          "minimumHeight": {
            "description": "The lowest height a multi-depth box can be scored down to, in `unit`, the same unit as `height`. Scoring takes 2 in off at a time, so the value is accepted when it is not above `height` and `height` minus it, converted to inches, is a whole number of 2 in steps, give or take 0.01 in. A 12 in box may say 10, 8 or any lower whole step, and not 11 (`box.minimumHeight.notAStep`); in centimetres one step is 5.08 cm. Above `height` is `box.minimumHeight.aboveHeight`; zero or below is `box.dimension.notPositive`; equal to `height` is zero steps and is accepted. Null for a single-depth box.",
            "type": [
              "number",
              "null"
            ]
          },
          "name": {
            "description": "The merchant’s own box name, and the key the upsert matches on. Trimmed, then compared exactly, not case-insensitively, because the database unique constraint is exact: `\"Small\"` and `\"small\"` are two boxes, and `\" Small \"` is `\"Small\"`. Required, and not blank once trimmed (`box.name.missing`). It has to be readable text: a control character or a Unicode noncharacter is `box.name.unreadable`, the same rule a carton’s `name` follows on the dimensions endpoint. There is no length cap. The name is what a packer reads on the packing summary, so keep it to what a person can read.",
            "type": "string"
          },
          "padding": {
            "description": "Space kept free inside on EVERY SIDE, in `unit`, so each interior axis loses twice this. Twice the padding at or past the shortest interior axis is `box.padding.exceedsInterior`. Defaults to 0 on create.",
            "type": "number"
          },
          "unit": {
            "description": "The unit the three lengths, the minimum height and the padding are in. Never implied: a row that sets a length and no unit is refused.",
            "enum": [
              "in",
              "cm",
              "mm"
            ],
            "type": "string"
          },
          "wallThickness": {
            "description": "ONE wall, in `wallThicknessUnit`. Exterior is interior plus twice this on each axis. Defaults to 7 mm.",
            "type": "number"
          },
          "wallThicknessUnit": {
            "description": "The unit `wallThickness` is in. Usually `mm`.",
            "enum": [
              "in",
              "cm",
              "mm"
            ],
            "type": "string"
          },
          "weightUnit": {
            "description": "The unit both weights are in.",
            "enum": [
              "lb",
              "oz",
              "kg",
              "g"
            ],
            "type": "string"
          },
          "width": {
            "description": "Interior width, in `unit`.",
            "type": "number"
          }
        },
        "required": [
          "name"
        ],
        "type": "object"
      },
      "BulkBoxRequest": {
        "description": "Up to 500 box rows. An empty array is a legal no-op and answers 200 with zero counts, so a nightly job whose system of record changed nothing need not special-case skipping the call.",
        "properties": {
          "boxes": {
            "description": "The rows. Over 500 is a 413 and nothing is written - the cap bounds the time one request spends, so a larger catalogue is sent as several.",
            "items": {
              "$ref": "#/components/schemas/BoxRow"
            },
            "maxItems": 500,
            "type": "array"
          },
          "shop": {
            "description": "Optional, and only ever COMPARED. The shop is taken from the token; naming a different one is a 404. Send it when you want the request to fail loudly if a token is pointed at the wrong store.",
            "type": "string"
          }
        },
        "required": [
          "boxes"
        ],
        "type": "object"
      },
      "BulkBoxResponse": {
        "description": "The answer to a bulk box upsert. 200 when every row was written, 207 when any row failed - `res.ok` is true either way, so the cheap check a cron job wants is `status !== 200`.",
        "properties": {
          "count": {
            "description": "Rows received.",
            "type": "integer"
          },
          "created": {
            "description": "Part of `succeeded`. NOT stable across a retry, deliberately.",
            "type": "integer"
          },
          "failed": {
            "description": "Rows refused. Stable across a retry of the same payload.",
            "type": "integer"
          },
          "results": {
            "description": "One entry per row, in request order.",
            "items": {
              "$ref": "#/components/schemas/BulkRowResult"
            },
            "type": "array"
          },
          "shop": {
            "description": "The shop the TOKEN named. Never the one the payload did.",
            "type": "string"
          },
          "succeeded": {
            "description": "Rows now stored as sent. Stable across a retry of the same payload.",
            "type": "integer"
          },
          "updated": {
            "description": "Part of `succeeded`. NOT stable across a retry, deliberately.",
            "type": "integer"
          }
        },
        "required": [
          "shop",
          "count",
          "succeeded",
          "failed",
          "created",
          "updated",
          "results"
        ],
        "type": "object"
      },
      "BulkRequestError": {
        "description": "A whole request refused, as opposed to a row. Nothing was written. These describe the caller’s own request and say nothing about any shop or any stored row, which is why they carry a body where the 401, 404 and 429 do not.",
        "properties": {
          "error": {
            "description": "Why the request was refused.",
            "enum": [
              "body.notJson",
              "body.notAnObject",
              "boxes.missing",
              "boxes.notAnArray",
              "boxes.tooMany"
            ],
            "type": "string"
          },
          "limit": {
            "description": "Present on `boxes.tooMany` only: the cap, which is 500.",
            "type": "integer"
          },
          "received": {
            "description": "Present on `boxes.tooMany` only: how many rows were sent.",
            "type": "integer"
          }
        },
        "required": [
          "error"
        ],
        "type": "object"
      },
      "BulkRowIssue": {
        "description": "One reason a row was not written. The vocabulary spans two sets on purpose: the structural codes are about the JSON, and the rest come from `app/lib/boxes/validate.ts`, which is the same validator the admin form and the paste importer call. A caller fixing a generated payload does not care which of the two refused it.",
        "properties": {
          "code": {
            "description": "Why this row was refused. `box.girth.overCarrierCap` is length + 2 x width + 2 x height over the 165 in carriers refuse past, measured in inches whatever unit the row used.",
            "enum": [
              "row.notAnObject",
              "row.unknownField",
              "row.wrongType",
              "box.name.missing",
              "box.name.unreadable",
              "box.name.duplicate",
              "box.dimension.missing",
              "box.dimension.notPositive",
              "box.unit.unknown",
              "box.girth.overCarrierCap",
              "box.minimumHeight.aboveHeight",
              "box.minimumHeight.notAStep",
              "box.padding.negative",
              "box.padding.exceedsInterior",
              "box.wallThickness.negative",
              "box.weight.notPositive",
              "box.emptyWeight.atOrAboveMaxWeight",
              "box.weightUnit.unknown",
              "box.cost.notANumber",
              "box.cost.negative"
            ],
            "type": "string"
          },
          "field": {
            "description": "The payload key at fault. Absent when the whole row is.",
            "type": "string"
          }
        },
        "required": [
          "code"
        ],
        "type": "object"
      },
      "BulkRowResult": {
        "description": "What happened to one row. `results` is in request order, so entry `i` is the row you sent at index `i` and nothing has to be matched on name.",
        "properties": {
          "collidesWith": {
            "description": "The OTHER rows in this same payload claiming this name. Present only on a `box.name.duplicate` refusal. Two rows keyed to one box are two instructions in an order nobody chose, so both are refused and neither is applied.",
            "items": {
              "type": "integer"
            },
            "type": "array"
          },
          "index": {
            "description": "The row’s position in the request’s `boxes` array, from 0.",
            "type": "integer"
          },
          "issues": {
            "description": "Never empty when `status` is `failed`, and absent otherwise.",
            "items": {
              "$ref": "#/components/schemas/BulkRowIssue"
            },
            "type": "array"
          },
          "name": {
            "description": "The key this row upserts on, or null when it carried no usable name.",
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "description": "`created` and `updated` both mean the row is stored. They deliberately do NOT match across a retry - 500 created then 500 updated is the true story - so compare `count`, `succeeded` and `failed`, which are stable. `updated` means the named box existed and was written, which may have set every column to the value it already held.",
            "enum": [
              "created",
              "updated",
              "failed"
            ],
            "type": "string"
          }
        },
        "required": [
          "index",
          "name",
          "status"
        ],
        "type": "object"
      },
      "Calculation": {
        "description": "One calculation, as the admin screen shows it. The body is built by the SAME code that renders the app’s own calculation page, so a packing slip and the merchant’s screen cannot say different things. Every display string is in `locale`; `id`, `requestedAt`, `locale`, `outcome` and each `carrier`, `code` and `requiresReconnect` are the machine fields to branch on. The destination is city, province, postal code and country only: the app never stores a street, a name or a contact.",
        "properties": {
          "carrierErrors": {
            "description": "Every carrier that did not answer. Empty when all of them did.",
            "items": {
              "$ref": "#/components/schemas/CalculationCarrierError"
            },
            "type": "array"
          },
          "destination": {
            "description": "City, province, postal code and country, as one line.",
            "type": "string"
          },
          "facts": {
            "description": "The summary lines, in the order the screen prints them.",
            "items": {
              "$ref": "#/components/schemas/CalculationFact"
            },
            "type": "array"
          },
          "id": {
            "description": "The calculation id, as asked for.",
            "type": "string"
          },
          "lineItems": {
            "description": "The cart, as the packer saw it.",
            "items": {
              "$ref": "#/components/schemas/CalculationLineItem"
            },
            "type": "array"
          },
          "locale": {
            "description": "The locale every display string in this body is in: `?locale=` when sent and supported, else the shop’s own.",
            "enum": [
              "en",
              "es",
              "fr",
              "de",
              "pt-BR",
              "ja"
            ],
            "type": "string"
          },
          "outcome": {
            "description": "READ THIS, NOT THE STATUS. `rates` - carriers answered. `backstop` - every carrier failed and the merchant’s own backup rate answered. `planBackstop` - the shop was past its plan’s allowance and its own backup rate answered by design. `noRates` - the buyer was offered nothing. All four are a 200: a calculation that recorded a failure was still read.",
            "enum": [
              "rates",
              "backstop",
              "planBackstop",
              "noRates"
            ],
            "type": "string"
          },
          "outcomeLabel": {
            "description": "`outcome` as the admin badge words it, in `locale`.",
            "type": "string"
          },
          "packages": {
            "description": "The boxes, in the order a packer works through them.",
            "items": {
              "$ref": "#/components/schemas/CalculationPackage"
            },
            "type": "array"
          },
          "packingSummary": {
            "description": "The packer’s one-line sentence, e.g. `2 × Medium mailer`.",
            "type": [
              "string",
              "null"
            ]
          },
          "rates": {
            "description": "Every rate the buyer was offered. Empty on `noRates`.",
            "items": {
              "$ref": "#/components/schemas/CalculationRate"
            },
            "type": "array"
          },
          "requestedAt": {
            "description": "When the rate was requested, ISO 8601 in UTC.",
            "type": "string"
          },
          "when": {
            "description": "`requestedAt` for a person, in the shop’s time zone and `locale`.",
            "type": "string"
          }
        },
        "required": [
          "id",
          "requestedAt",
          "locale",
          "when",
          "destination",
          "outcome",
          "outcomeLabel",
          "facts",
          "packingSummary",
          "packages",
          "rates",
          "carrierErrors",
          "lineItems"
        ],
        "type": "object"
      },
      "CalculationCarrierError": {
        "description": "One carrier that did not answer. `body` is a stored one-line diagnostic with account numbers removed before it was written; a raw carrier reply is never kept.",
        "properties": {
          "body": {
            "description": "The one-line diagnostic.",
            "type": "string"
          },
          "carrier": {
            "description": "Which carrier, as a machine id.",
            "type": "string"
          },
          "carrierName": {
            "description": "`carrier` as the admin screen names it, in `locale`, e.g. “UPS”.",
            "type": "string"
          },
          "code": {
            "description": "Why, as a machine code (`timeout`, `auth`, `throttled` and the like).",
            "type": "string"
          },
          "elapsed": {
            "description": "How long the attempt took, e.g. “8,000 ms”.",
            "type": "string"
          },
          "explanation": {
            "description": "What the merchant can do about it, in `locale`, when the carrier named a cause the app knows, e.g. a UPS account registered in a different country from the location the order ships from. Null otherwise.",
            "type": [
              "string",
              "null"
            ]
          },
          "id": {
            "description": "Unique within this body.",
            "type": "string"
          },
          "requiresReconnect": {
            "description": "True when the merchant has to reconnect this carrier.",
            "type": "boolean"
          },
          "status": {
            "description": "“HTTP 401”, or a localised “no response” line when there was no status.",
            "type": "string"
          }
        },
        "required": [
          "id",
          "carrier",
          "carrierName",
          "code",
          "status",
          "elapsed",
          "body",
          "requiresReconnect",
          "explanation"
        ],
        "type": "object"
      },
      "CalculationError": {
        "description": "The only bodied refusal this endpoint has. It is about this app’s database, not your request.",
        "properties": {
          "error": {
            "description": "Why the calculation could not be read.",
            "enum": [
              "calculation-read-failed"
            ],
            "type": "string"
          }
        },
        "required": [
          "error"
        ],
        "type": "object"
      },
      "CalculationFact": {
        "description": "One line of the summary, as the admin screen prints it. Localised; do not branch on `label`.",
        "properties": {
          "label": {
            "description": "What the line is, in `locale`.",
            "type": "string"
          },
          "value": {
            "description": "Its value, formatted for a person in `locale`.",
            "type": "string"
          }
        },
        "required": [
          "label",
          "value"
        ],
        "type": "object"
      },
      "CalculationLineItem": {
        "description": "One cart line as the packer saw it. Catalogue data, never a buyer.",
        "properties": {
          "id": {
            "description": "Unique within this body.",
            "type": "string"
          },
          "label": {
            "description": "The SKU, else the variant id, else a localised “unnamed item”.",
            "type": "string"
          },
          "quantity": {
            "description": "How many, formatted in `locale`.",
            "type": "string"
          },
          "weight": {
            "description": "Unit weight in grams, formatted in `locale`.",
            "type": "string"
          }
        },
        "required": [
          "id",
          "label",
          "quantity",
          "weight"
        ],
        "type": "object"
      },
      "CalculationPackage": {
        "description": "One box the packer chose, formatted for a packing slip.",
        "properties": {
          "boxName": {
            "description": "The box as it is named in the merchant’s own catalogue.",
            "type": "string"
          },
          "contents": {
            "description": "The SKUs or variant ids in this box, joined with commas.",
            "type": "string"
          },
          "dimensions": {
            "description": "Outer size with its unit, e.g. “12 × 10 × 8 in”.",
            "type": "string"
          },
          "id": {
            "description": "Stable id of this parcel row.",
            "type": "string"
          },
          "note": {
            "description": "Null for one of the merchant’s own boxes; otherwise says the item shipped in its own carton or in carrier packaging.",
            "type": [
              "string",
              "null"
            ]
          },
          "position": {
            "description": "“Box 1 of 3”. Counted from one, in `locale`.",
            "type": "string"
          },
          "weight": {
            "description": "Packed weight with its unit, e.g. “4.2 lb”.",
            "type": "string"
          }
        },
        "required": [
          "id",
          "position",
          "boxName",
          "dimensions",
          "weight",
          "note",
          "contents"
        ],
        "type": "object"
      },
      "CalculationRate": {
        "description": "One rate the buyer was offered.",
        "properties": {
          "account": {
            "description": "The UPS account that priced this rate, as the admin names it: the merchant’s nickname for it, else “UPS account ending 53A3” in `locale`. Buy the label on this account for its cost to match what the buyer paid. Null when the calculation names no account: the shop holds one UPS account, the rate is another carrier’s, or it was logged before this was recorded.",
            "type": [
              "string",
              "null"
            ]
          },
          "accountLast4": {
            "description": "The last four characters of that account number, exactly as stored, e.g. `53A3`. Never more of the number. Null exactly when `account` is null.",
            "type": [
              "string",
              "null"
            ]
          },
          "amount": {
            "description": "The price, formatted with its currency in `locale`.",
            "type": "string"
          },
          "carrier": {
            "description": "Which carrier quoted it, as a machine id.",
            "type": "string"
          },
          "carrierName": {
            "description": "`carrier` as the admin screen names it, in `locale`, e.g. “Canada Post”.",
            "type": "string"
          },
          "id": {
            "description": "Unique within this body.",
            "type": "string"
          },
          "name": {
            "description": "The service name the buyer saw.",
            "type": "string"
          },
          "negotiated": {
            "description": "Whether this price came off the merchant’s own negotiated rates. Three states: `true` - the carrier priced it on the merchant’s negotiated tariff. `false` - the carrier’s published price. `null` - the carrier did not say, which is every carrier but UPS, the backup rate, and any calculation logged before this was recorded. Null is not false: do not read it as a published price.",
            "type": [
              "boolean",
              "null"
            ]
          },
          "pricing": {
            "description": "`negotiated` as the admin screen words it, in `locale`. Null exactly when `negotiated` is null.",
            "type": [
              "string",
              "null"
            ]
          },
          "savings": {
            "description": "What the merchant’s negotiated rate saved against the carrier’s published price, e.g. “Your rate $12.10, retail $18.40, you save $6.30”, in `locale`. Null when there is nothing to show: the rate was not negotiated, the carrier gave no published price, or it was not higher.",
            "type": [
              "string",
              "null"
            ]
          },
          "transit": {
            "description": "Transit time, e.g. “2 days”. Null when the carrier gave none.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "carrier",
          "carrierName",
          "name",
          "amount",
          "transit",
          "negotiated",
          "pricing",
          "savings",
          "account",
          "accountLast4"
        ],
        "type": "object"
      },
      "DimensionPart": {
        "additionalProperties": false,
        "description": "One carton of a multi-part variant. A kit that ships as three boxes is three parts. Order is the array’s and is the stored order. An entry that is not a JSON object is `dimensions.part.notAnObject` with the `partIndex` it was at. A key that is not listed below is a `dimensions.part.unknownField` refusal, also carrying its `partIndex`, rather than a silent drop - a carton whose `weight` you spelled `wieght` ships unweighed and nothing anywhere says so.",
        "properties": {
          "height": {
            "description": "Height, in `unit`. Refused over 165 in, converted from `unit` (`dimensions.part.length.tooLarge`).",
            "type": "number"
          },
          "keepVertical": {
            "description": "This carton may not be laid on its side. Accepts a JSON boolean, or `\"true\"`, `\"1\"`, `\"yes\"` and their negatives; anything else is `dimensions.boolean.unreadable` rather than a guess. Absent, null or `\"\"` is false.",
            "type": "boolean"
          },
          "length": {
            "description": "Length, in `unit`. Refused over 165 in, converted from `unit` - no box could ever hold a carton that long (`dimensions.part.length.tooLarge`).",
            "type": "number"
          },
          "name": {
            "description": "What the packing summary calls this carton. The one field of a carton stored as you send it, so it has to be readable text: a control character or a Unicode noncharacter is `dimensions.part.name.unreadable`, and a value that is not a string, a number or a boolean is the same refusal rather than a carton named after the shape you sent.",
            "type": "string"
          },
          "packMode": {
            "description": "How the packer treats this carton, with the same four names as the variant’s `packMode`. Absent or null is `pack-into-box`; `\"\"` is `dimensions.part.packMode.unknown`, and so is a Boxify numeric code.",
            "enum": [
              "pack-into-box",
              "ship-as-is",
              "ship-as-is-if-alone",
              "ship-for-free"
            ],
            "type": "string"
          },
          "padding": {
            "description": "Space kept free around this carton on EVERY SIDE, in `unit`, so each axis grows by twice this. Absent, null or `\"\"` is 0.",
            "type": "number"
          },
          "unit": {
            "description": "The unit this carton’s lengths and padding are in.",
            "enum": [
              "in",
              "cm",
              "mm"
            ],
            "type": "string"
          },
          "weight": {
            "description": "What this carton weighs, in `weightUnit`. Absent, null or `\"\"` is a weightless carton, not an error. A weight sent without a readable `weightUnit` is refused (`dimensions.part.weightUnit.unknown`), and so is a negative one (`dimensions.part.weight.negative`), the same as the variant’s own `weight`.",
            "type": "number"
          },
          "weightUnit": {
            "description": "The unit `weight` is in. Required whenever `weight` is sent.",
            "enum": [
              "lb",
              "oz",
              "kg",
              "g"
            ],
            "type": "string"
          },
          "width": {
            "description": "Width, in `unit`. Refused over 165 in, converted from `unit` (`dimensions.part.length.tooLarge`).",
            "type": "number"
          }
        },
        "type": "object"
      },
      "DimensionRow": {
        "additionalProperties": false,
        "description": "One variant’s measurements. Send EXACTLY ONE of `sku` and `variantId`: a row carrying both addresses two variants and is refused rather than resolved. The three lengths go together - all three or none - and `unit` is never implied, so a row that sets a length and no unit is `dimensions.unit.unknown`. All three blank is a deliberate clear. A key that is not listed below is a `dimensions.row.unknownField` refusal rather than a silent drop. Every rule here is the admin form’s own validator, called on a `FormData` built from this row, so a rule added to the form applies to this endpoint the same night. A ROW REPLACES WHAT IS STORED FOR THE VARIANT; IT IS NOT A PATCH, unlike a box row on `/api/boxes`. Every field but `parts` is written from this row alone, so a field you leave out is RESET rather than kept: absent `length`, `width` and `height` clear the measurement, absent `padding` stores 0, absent `weight` stores no weight (the rate then uses the weight Shopify holds for the variant), absent `keepVertical` stores false and absent `packMode` stores `pack-into-box`. Send every value you want kept, on every post. `parts` is the one field where absent means \"leave it alone\". BLANK, EXACTLY. For the three lengths, `padding`, `weight` and `weightUnit`, a `null`, a `\"\"` and a string of only spaces all mean the same as leaving the field out. For `keepVertical` they mean false. For `packMode`, `null` means the default but `\"\"` is `dimensions.packMode.unknown`. For `unit` every one of them is `dimensions.unit.unknown`.",
        "properties": {
          "height": {
            "description": "Height, in `unit`. Refused over 165 in, converted from `unit` (`dimensions.length.tooLarge`).",
            "type": "number"
          },
          "keepVertical": {
            "description": "This variant may not be laid on its side. Accepts a JSON boolean, or `\"true\"`, `\"1\"`, `\"yes\"` and their negatives; anything else is `dimensions.boolean.unreadable`. Absent, null or `\"\"` stores false, even on a variant that held true.",
            "type": "boolean"
          },
          "length": {
            "description": "Length, in `unit`. Refused over 165 in, converted from `unit` - no box could ever hold a variant that long (`dimensions.length.tooLarge`).",
            "type": "number"
          },
          "packMode": {
            "description": "How the packer treats this variant: `pack-into-box` - placed inside one of the shop’s boxes; `ship-as-is` - ships in its own carton and is never placed in a box; `ship-as-is-if-alone` - ships in its own carton when it is the only item in the cart, and is packed with the rest otherwise; `ship-for-free` - goes in no package and adds nothing to the rate. Absent or null stores `pack-into-box`, which is what the form’s select posts when a merchant does not touch it - EVEN ON A VARIANT THAT HOLDS ANOTHER MODE, because a row replaces what is stored. `\"\"` is refused as `dimensions.packMode.unknown`. A Boxify export holds Pack Mode as a number, and a dimensions paste in the app reads it as `0` = `ship-for-free`, `1` = `pack-into-box`, `2` = `ship-as-is`, `3` = `ship-as-is-if-alone`. This endpoint does not: send the name, and a number, Boxify’s codes included, is `dimensions.packMode.unknown`.",
            "enum": [
              "pack-into-box",
              "ship-as-is",
              "ship-as-is-if-alone",
              "ship-for-free"
            ],
            "type": "string"
          },
          "padding": {
            "description": "Space kept free around this variant on EVERY SIDE inside a box, in `unit`, so each axis of the item grows by twice this. A box’s own `padding` is added on top, not instead. Absent, null or `\"\"` stores 0; negative is `dimensions.padding.negative`.",
            "type": "number"
          },
          "parts": {
            "description": "The cartons this variant ships as, REPLACED AS A SET. Absent leaves the variant’s cartons alone; `[]` removes them; a list replaces them. That difference is load-bearing - a job that stops sending `parts` must not silently delete a merchant’s kit - so absent and empty are genuinely different requests. At most 20 cartons.",
            "items": {
              "$ref": "#/components/schemas/DimensionPart"
            },
            "maxItems": 20,
            "type": "array"
          },
          "sku": {
            "description": "The variant’s SKU. Matched trimmed and case-insensitively, and refused as `ambiguous` when more than one variant carries it.",
            "type": "string"
          },
          "unit": {
            "description": "The unit the three lengths and the padding are in. Never implied.",
            "enum": [
              "in",
              "cm",
              "mm"
            ],
            "type": "string"
          },
          "variantId": {
            "description": "A variant id, as `gid://shopify/ProductVariant/43210987654321` or the bare number. Verified against Shopify rather than trusted, so a typo is `notFound` and never a row written against nothing.",
            "type": "string"
          },
          "weight": {
            "description": "What this variant weighs, in `weightUnit`, overriding the weight Shopify holds for it. A weight that cannot be read is REFUSED, and the row is `invalid` and not written: a value that is not a number is `dimensions.weight.notANumber`, a negative one is `dimensions.weight.negative`, and one sent without a readable `weightUnit` is `dimensions.weightUnit.unknown`. `0` is a weight of zero, not \"no weight\". Absent, null or `\"\"` stores no weight, so the rate uses the weight Shopify holds: see `DimensionRow` on why absent is not \"keep\".",
            "type": "number"
          },
          "weightUnit": {
            "description": "The unit `weight` is in. Required whenever `weight` is sent; without one the row is `dimensions.weightUnit.unknown`. Sent with no `weight`, it is ignored.",
            "enum": [
              "lb",
              "oz",
              "kg",
              "g"
            ],
            "type": "string"
          },
          "width": {
            "description": "Width, in `unit`. Refused over 165 in, converted from `unit` (`dimensions.length.tooLarge`).",
            "type": "number"
          }
        },
        "type": "object"
      },
      "DimensionRowError": {
        "description": "One reason a row was refused. The codes are the admin form’s own, plus a short list this endpoint adds for shapes a form cannot be in - a key that names nothing, a key that names two things, and a boolean that reads as neither.",
        "properties": {
          "code": {
            "description": "Why this row was refused. A code, never a sentence, and the same code the admin screen shows for the same problem.",
            "enum": [
              "dimensions.row.notAnObject",
              "dimensions.row.unknownField",
              "dimensions.part.unknownField",
              "dimensions.key.missing",
              "dimensions.key.both",
              "dimensions.key.notFound",
              "dimensions.key.ambiguous",
              "dimensions.key.duplicate",
              "dimensions.parts.notAnArray",
              "dimensions.part.notAnObject",
              "dimensions.boolean.unreadable",
              "dimensions.write.failed",
              "dimensions.variant.missing",
              "dimensions.triple.incomplete",
              "dimensions.length.notANumber",
              "dimensions.length.notPositive",
              "dimensions.length.tooLarge",
              "dimensions.unit.unknown",
              "dimensions.padding.notANumber",
              "dimensions.padding.negative",
              "dimensions.packMode.unknown",
              "dimensions.weight.notANumber",
              "dimensions.weight.negative",
              "dimensions.weightUnit.unknown",
              "dimensions.parts.tooMany",
              "dimensions.part.name.missing",
              "dimensions.part.name.unreadable",
              "dimensions.part.triple.incomplete",
              "dimensions.part.length.notANumber",
              "dimensions.part.length.notPositive",
              "dimensions.part.length.tooLarge",
              "dimensions.part.unit.unknown",
              "dimensions.part.weight.notANumber",
              "dimensions.part.weight.negative",
              "dimensions.part.weightUnit.unknown",
              "dimensions.part.padding.notANumber",
              "dimensions.part.padding.negative",
              "dimensions.part.packMode.unknown"
            ],
            "type": "string"
          },
          "field": {
            "description": "The payload key at fault, or `row` when the whole row is.",
            "type": "string"
          },
          "partIndex": {
            "description": "Present only for a problem inside one carton. Zero-based, as sent.",
            "type": "integer"
          }
        },
        "required": [
          "code",
          "field"
        ],
        "type": "object"
      },
      "DimensionRowResult": {
        "description": "What happened to one variant row. In request order, so entry `i` is the row you sent at index `i`.",
        "properties": {
          "errors": {
            "description": "Empty on `written` and `unchanged`. Never empty otherwise.",
            "items": {
              "$ref": "#/components/schemas/DimensionRowError"
            },
            "type": "array"
          },
          "index": {
            "description": "The row’s position in the request’s `variants` array, from 0.",
            "type": "integer"
          },
          "key": {
            "description": "The key as you sent it, so a report can be joined back to a spreadsheet.",
            "type": [
              "string",
              "null"
            ]
          },
          "keyKind": {
            "description": "Which of the two keys the row carried, or null when it carried neither or both.",
            "enum": [
              "sku",
              "variantId",
              null
            ],
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "description": "Past tense, and the same words the paste review screen uses wherever they mean the same thing. `unchanged` means the row resolved and validated and every value already matched, so nothing was written and `updatedAt` did not move - which is what makes a retried nightly job free. `notFound` is never a created variant: this API holds dimensions for Shopify objects that already exist.",
            "enum": [
              "written",
              "unchanged",
              "noKey",
              "notFound",
              "ambiguous",
              "duplicate",
              "invalid",
              "failed"
            ],
            "type": "string"
          },
          "variantId": {
            "description": "The variant the key resolved to, once it did. Null when it did not.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "index",
          "status",
          "key",
          "keyKind",
          "variantId",
          "errors"
        ],
        "type": "object"
      },
      "DimensionsRequest": {
        "description": "Up to 500 variant rows - the same cap as `/api/boxes`, so one client can page both feeds in one loop.",
        "properties": {
          "shop": {
            "description": "Optional, and only ever COMPARED. The shop is taken from the token; naming a different one is a 404.",
            "type": "string"
          },
          "variants": {
            "description": "The rows. Over 500 is a 413 and NOTHING is written - the request is refused whole rather than truncated, so a caller can never believe 900 rows landed when 500 did.",
            "items": {
              "$ref": "#/components/schemas/DimensionRow"
            },
            "maxItems": 500,
            "type": "array"
          }
        },
        "required": [
          "variants"
        ],
        "type": "object"
      },
      "DimensionsRequestError": {
        "description": "A whole request refused. Nothing was written.",
        "properties": {
          "error": {
            "description": "Why the request was refused.",
            "enum": [
              "not-json",
              "variants-not-an-array",
              "too-many-variants",
              "shopify-unavailable"
            ],
            "type": "string"
          },
          "max": {
            "description": "Present on `too-many-variants` only: the cap, which is 500.",
            "type": "integer"
          },
          "sent": {
            "description": "Present on `too-many-variants` only: how many rows were sent.",
            "type": "integer"
          }
        },
        "required": [
          "error"
        ],
        "type": "object"
      },
      "DimensionsResponse": {
        "description": "The answer to a bulk dimension upsert. 200 when every row landed (`written` or `unchanged`), 207 when any row did not - `res.ok` is true either way, so the cheap check a cron job wants is `status !== 200`, the same check `/api/boxes` takes. `counts` is the line a cron logs and `results` is what an operator reads when the count is not what they expected.",
        "properties": {
          "counts": {
            "description": "Three numbers over the whole payload. `failed` is every row that did not land, whatever the reason; walk `results` for the breakdown, which stays accurate as statuses are added.",
            "properties": {
              "failed": {
                "description": "Rows that landed neither way.",
                "type": "integer"
              },
              "unchanged": {
                "description": "Rows that already matched. Nothing was written for these.",
                "type": "integer"
              },
              "written": {
                "description": "Rows that differed from what was stored and were written.",
                "type": "integer"
              }
            },
            "required": [
              "written",
              "unchanged",
              "failed"
            ],
            "type": "object"
          },
          "results": {
            "description": "One entry per row, in request order.",
            "items": {
              "$ref": "#/components/schemas/DimensionRowResult"
            },
            "type": "array"
          }
        },
        "required": [
          "counts",
          "results"
        ],
        "type": "object"
      },
      "OrderPacking": {
        "description": "How one order was packed. This is the SAME body the app’s own order-page block renders, read off the same order metafield, so the warehouse and the admin cannot show different answers.",
        "properties": {
          "boxes": {
            "description": "The boxes, in `position` order. Empty unless `state` is `packed`.",
            "items": {
              "$ref": "#/components/schemas/OrderPackingBox"
            },
            "type": "array"
          },
          "calculatedAt": {
            "description": "When the rate was QUOTED, ISO 8601 - not when the summary was written. Null unless `state` is `packed`.",
            "type": [
              "string",
              "null"
            ]
          },
          "orderGid": {
            "description": "The order, as `gid://shopify/Order/<n>`.",
            "type": "string"
          },
          "packageCount": {
            "description": "How many boxes. Counted from the `boxes` array that survived the read, never from a stored count that could disagree with it.",
            "type": "integer"
          },
          "service": {
            "description": "The shipping the buyer chose. Null on an order with no shipping line.",
            "properties": {
              "carrier": {
                "description": "Which carrier quoted it, as a machine id: `usps`, `ups`, `fedex` or `canada-post`. Null for a backup, free or merchant rate.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "code": {
                "description": "The carrier’s own service code.",
                "type": [
                  "string",
                  "null"
                ]
              },
              "title": {
                "description": "What the buyer saw at checkout.",
                "type": [
                  "string",
                  "null"
                ]
              }
            },
            "required": [
              "title",
              "code",
              "carrier"
            ],
            "type": [
              "object",
              "null"
            ]
          },
          "state": {
            "description": "READ THIS BEFORE THE BOXES. `packed` - the order carries a packing summary this release understands and it has at least one box. `none` - the order carries no packing summary. Common and not an error: the app prices at checkout, so an order placed before install, a draft order, a checkout that took the merchant’s own backup rate, or an order older than the calculation retention window all read this way. `boxes` is empty. `unreadable` - a summary is stored and cannot be trusted: a `version` this release does not know, or a shape missing the fields a packer reads. `boxes` is empty. Do NOT treat this as `none`; it means something is there and we would be guessing.",
            "enum": [
              "packed",
              "none",
              "unreadable",
              "pending"
            ],
            "type": "string"
          },
          "summary": {
            "description": "The one-line sentence the calculation composed, e.g. `2 × Medium mailer`. Null unless `state` is `packed`.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "orderGid",
          "state",
          "summary",
          "packageCount",
          "service",
          "calculatedAt",
          "boxes"
        ],
        "type": "object"
      },
      "OrderPackingBox": {
        "description": "One box, with the dimensions and weight the carrier was quoted on. Boxes come back in `position` order.",
        "properties": {
          "boxName": {
            "description": "The box as it is named in the merchant’s own catalogue.",
            "type": "string"
          },
          "carrierPackagingKey": {
            "description": "The carrier’s own packaging code when the box is a carrier-supplied one, otherwise null.",
            "type": [
              "string",
              "null"
            ]
          },
          "height": {
            "description": "Outer height, in `unit`.",
            "type": "number"
          },
          "items": {
            "description": "What is in this box.",
            "items": {
              "$ref": "#/components/schemas/OrderPackingItem"
            },
            "type": "array"
          },
          "length": {
            "description": "Outer length, in `unit`.",
            "type": "number"
          },
          "position": {
            "description": "Zero-based, and the order a packer works through the boxes in.",
            "type": "integer"
          },
          "shipAsIs": {
            "description": "True when the item ships in its own carton and was not placed in a box.",
            "type": "boolean"
          },
          "unit": {
            "description": "The length unit the three measurements are in.",
            "type": "string"
          },
          "weight": {
            "description": "Packed weight, in `weightUnit`.",
            "type": "number"
          },
          "weightUnit": {
            "description": "The weight unit.",
            "type": "string"
          },
          "width": {
            "description": "Outer width, in `unit`.",
            "type": "number"
          }
        },
        "required": [
          "position",
          "boxName",
          "carrierPackagingKey",
          "shipAsIs",
          "length",
          "width",
          "height",
          "unit",
          "weight",
          "weightUnit",
          "items"
        ],
        "type": "object"
      },
      "OrderPackingError": {
        "description": "The only two bodied refusals this endpoint has. Both are about this app’s ability to reach Shopify; neither is about your request, so neither is fixed by changing it.",
        "properties": {
          "error": {
            "description": "Why the packing could not be read.",
            "enum": [
              "shopify-unavailable",
              "order-read-failed"
            ],
            "type": "string"
          }
        },
        "required": [
          "error"
        ],
        "type": "object"
      },
      "OrderPackingItem": {
        "description": "One order line inside one box, as the packer placed it.",
        "properties": {
          "partPosition": {
            "description": "Which part of a multi-part product this is, counted from ZERO. Null on an ordinary line. `0` is a real part and is not the same as null.",
            "type": [
              "integer",
              "null"
            ]
          },
          "quantity": {
            "description": "How many of this line are in this box.",
            "type": "number"
          },
          "sku": {
            "description": "The variant’s SKU at the time the rate was quoted.",
            "type": [
              "string",
              "null"
            ]
          },
          "title": {
            "description": "The product title the order carried. Null on a summary written before the field existed, which named no item.",
            "type": [
              "string",
              "null"
            ]
          },
          "variantGid": {
            "description": "The variant this line is for. Null when the packer had no variant for it.",
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "variantGid",
          "sku",
          "title",
          "partPosition",
          "quantity"
        ],
        "type": "object"
      }
    },
    "securitySchemes": {
      "bearerAuth": {
        "description": "A token minted on the app’s API tokens screen. Sent as `Authorization: Bearer <token>`. The scheme word is compared case-insensitively by the parser, but spell it `Bearer`.",
        "scheme": "bearer",
        "type": "http"
      }
    }
  },
  "info": {
    "description": "Box Warden holds a merchant’s box catalogue and their products’ dimensions, packs each cart\ninto those boxes, and answers Shopify’s carrier-service callback with rates for the boxes it\nchose. This API is how a merchant’s own system of record - a WMS, an ERP, a nightly cron job -\nkeeps both of those tables current without anybody opening the admin, and how it reads back the\nboxes the app chose for an order it is about to pack, or for one calculation in its log.\n\n## Authentication\n\nEvery endpoint but this document takes a bearer token minted on the app’s API tokens screen:\n`Authorization: Bearer <token>`. A token belongs to exactly one shop and the shop is always\ntaken FROM the token, never from a payload, a query parameter or a path segment. A payload may\ncarry a `shop` field, and the two reads take the same thing as a `?shop=` parameter; both are\ncompared and then dropped. Naming a different shop is a 404 and never a 403, because a 403 would\nconfirm that the named shop exists.\n\nA token is shown once and stored as a peppered hash, so it cannot be read back. Rotate or\nrevoke on the same screen; a revoked token is a 401 immediately.\n\n## Calling from a browser\n\nEvery path answers CORS for `https://shelfwarden.app`, the origin of the rendered\nreference, so its \"try it\" client can call the API with a pasted token. An `OPTIONS` preflight is\nanswered 204 with no body and needs no token. Every answer to that origin, a refusal included,\nnames it in `Access-Control-Allow-Origin`, and every answer varies on `Origin`, so a 401 or a 429 reaches the\npage as itself rather than as a network error. The request headers allowed are `Authorization` and `Content-Type`,\nand `Retry-After` and `WWW-Authenticate` are exposed to the page. No other origin is named and\ncredentials are not allowed: the token travels in a header the caller sets, never in a cookie.\nCORS is a browser rule and not an access control, so a server-side caller is unaffected by it\nand every request still needs its bearer token.\n\n## Plans\n\nThe API is on every paid plan and on no free one. A valid token whose shop is on the free plan,\nor on no plan at all, is answered 403 with `{ \"error\": \"api-not-on-plan\" }` on every\nendpoint, and the token works again the moment the shop is on a paid plan. How many requests a\ntoken may make also depends on the plan; the 429 on each endpoint gives the numbers, and they\nare per-token, per-instance, not shared across Cloud Run instances.\n\n## How a bulk write answers\n\nBoth write endpoints take an array and report PER ROW. One bad row never costs its neighbours,\nand the answer carries an entry per row, in request order, naming the row’s index and why it\nwas refused. Refusals are CODES and never sentences, and they are the same codes the admin\nscreens show for the same problem. Five things refuse a whole request instead - a bad token, a\nshop whose plan has no API, a shop that is not the token’s, bytes that are not the shape the\nendpoint takes, and more rows than the cap - and a caller can fix each of those without knowing\nanything about its rows.\n\nBoth answer an accepted request the same way: 200 when every row landed and 207 when any row\ndid not, with the same body either way. `res.ok` is true for both, so nothing throws, and\n`status !== 200` is the whole check a job needs to know a row wants a look.\n\n## How the two reads answer\n\nEach read answers a single thing and therefore has no partial state: it is a 200 with the whole\nanswer, or it is a refusal, and never a 207.\n\nThe order read: BRANCH ON `state` AND NOT ON THE STATUS. `none` is a 200 and means the app never\npacked that order, which is normal for anything bought before the app was installed.\n`unreadable` is a 200 and means a summary is stored that this release will not guess at.\nNeither is an error and neither is worth retrying.\n\nThe calculation read: BRANCH ON `outcome`. A checkout whose carriers all failed is a 200 with\n`outcome` saying so, because the row recording that failure was read.\n\n## Writing a nightly job against this\n\nPage your catalogue at or under the cap, post boxes first and dimensions second, and log\n`counts` from each response. A 207 is not a reason to resend the page: read `results`, because\na row refused for its content fails identically until it is fixed.\n\nSend a box row with only what you want changed, because a box row is a patch. Send a\ndimensions row COMPLETE, because it replaces what is stored and a field you leave out is reset.\n\nTime out no sooner than 300 seconds. That is how long the host lets one request run\nbefore it cuts it off, so a shorter client timeout can abandon a page the server is still\nwriting. A full page is sequential work - a read and a write per box, a Shopify lookup per\nbatch of keys and a transaction per changed variant - so a page costs far more than one row.\nIf a request does time out, resend the same page: both endpoints are idempotent, so a page that\nhalf landed is finished rather than doubled.\n\nRetry on a 429 after `Retry-After` seconds, as many times as your own cap allows. Resend the\npage on a 500 or a timeout. A 503 means the app has no session for the shop and it does not\nclear on a timer, so try again at the next scheduled run rather than in a loop. Never retry a\n400, 404 or 413 - those are your payload and will fail identically - nor a 403, which is the\nshop’s plan and answers the same until the plan changes.\nRe-posting an unchanged page is free on dimensions and harmless on boxes, so a job that cannot\nwork out what changed may simply send everything.",
    "summary": "Push box sizes and product dimensions into Box Warden from your own system of record, and read back how it packed an order or a calculation.",
    "title": "Box Warden API",
    "version": "1.2.0"
  },
  "openapi": "3.1.0",
  "paths": {
    "/api/boxes": {
      "description": "POST only. Any other method but OPTIONS is a 405 with `Allow: POST` and no body. OPTIONS is a CORS preflight: 204, no body, no token needed. See \"Calling from a browser\" in the API description.",
      "post": {
        "description": "Upserts up to 500 boxes for the shop the token authenticates as, matching each row on its `name`. Idempotent: re-posting the same payload leaves the same rows and answers the same `count`, `succeeded` and `failed`.  A row that cannot be written NEVER fails its neighbours. Every other row is applied and the answer carries one entry per row naming its index, its key and its issues, which is what a merchant pushing 400 boxes out of an ERP needs and what a single 400 denies them.  The rules are the admin form’s: each row is merged over the stored box first and the merged shape is handed to the one validator the form and the paste importer also call. That is why a patch carrying only a name and a cost is not refused for the dimensions it did not send.  THERE IS NO LIST AND NO DELETE. This endpoint creates and updates and nothing else, so a sync cannot read back which boxes the shop holds, and a box your feed stops sending is left exactly as it was. To retire one from a feed, send it with `active: false`: it stops being offered to the packer, keeps its history, and comes back with `active: true`. Deleting a box outright is done by the merchant, on the app’s Boxes screen, by opening the box and deleting it there.  This endpoint never calls Shopify, so it has no 503: a shop with a live token is written whether or not the app can reach Shopify for it.",
        "operationId": "upsertBoxes",
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "boxes": [
                  {
                    "active": true,
                    "costAmount": "1.35",
                    "emptyWeight": 1.2,
                    "height": 8,
                    "length": 12,
                    "maxWeight": 50,
                    "name": "SGPFL-12x10x8",
                    "padding": 0.5,
                    "unit": "in",
                    "wallThickness": 7,
                    "wallThicknessUnit": "mm",
                    "weightUnit": "lb",
                    "width": 10
                  },
                  {
                    "costAmount": "2.80",
                    "name": "SGPFL-18x18x18"
                  }
                ]
              },
              "schema": {
                "$ref": "#/components/schemas/BulkBoxRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "count": 2,
                  "created": 1,
                  "failed": 0,
                  "results": [
                    {
                      "index": 0,
                      "name": "SGPFL-12x10x8",
                      "status": "created"
                    },
                    {
                      "index": 1,
                      "name": "SGPFL-18x18x18",
                      "status": "updated"
                    }
                  ],
                  "shop": "sgpfl.myshopify.com",
                  "succeeded": 2,
                  "updated": 1
                },
                "schema": {
                  "$ref": "#/components/schemas/BulkBoxResponse"
                }
              }
            },
            "description": "Every row was written. The example is the answer to the request example above, posted to a shop that already held `SGPFL-18x18x18`."
          },
          "207": {
            "content": {
              "application/json": {
                "example": {
                  "count": 4,
                  "created": 0,
                  "failed": 3,
                  "results": [
                    {
                      "collidesWith": [
                        3
                      ],
                      "index": 0,
                      "issues": [
                        {
                          "code": "box.name.duplicate",
                          "field": "name"
                        }
                      ],
                      "name": "SGPFL-12x10x8",
                      "status": "failed"
                    },
                    {
                      "index": 1,
                      "name": "SGPFL-18x18x18",
                      "status": "updated"
                    },
                    {
                      "index": 2,
                      "issues": [
                        {
                          "code": "row.wrongType",
                          "field": "length"
                        },
                        {
                          "code": "row.unknownField",
                          "field": "weight"
                        }
                      ],
                      "name": "SGPFL-24x24x24",
                      "status": "failed"
                    },
                    {
                      "collidesWith": [
                        0
                      ],
                      "index": 3,
                      "issues": [
                        {
                          "code": "box.name.duplicate",
                          "field": "name"
                        }
                      ],
                      "name": "SGPFL-12x10x8",
                      "status": "failed"
                    }
                  ],
                  "shop": "sgpfl.myshopify.com",
                  "succeeded": 1,
                  "updated": 1
                },
                "schema": {
                  "$ref": "#/components/schemas/BulkBoxResponse"
                }
              }
            },
            "description": "At least one row failed and the rest were written. The same body as the 200, so a caller reads one shape; the status is what makes a dropped row impossible to miss."
          },
          "400": {
            "content": {
              "application/json": {
                "example": {
                  "error": "boxes.notAnArray"
                },
                "schema": {
                  "$ref": "#/components/schemas/BulkRequestError"
                }
              }
            },
            "description": "The envelope is wrong: the body is not JSON, is not a JSON object, carries no `boxes` key, or carries one that is not an array. Nothing was written."
          },
          "401": {
            "description": "The request carried no bearer token, a malformed `Authorization` header, a token this shop never minted, a revoked token, or reached a revision holding no token pepper. All five are this exact response, byte for byte and with no body: a refusal a caller could tell apart would let one merchant enumerate another merchant’s tokens. Mint a new token on the app’s API tokens screen and retry; there is nothing in the response to branch on.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always the bare challenge `Bearer`, with no `error=` parameter.",
                "example": "Bearer",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "error": "api-not-on-plan"
                },
                "schema": {
                  "$ref": "#/components/schemas/ApiAccessError"
                }
              }
            },
            "description": "The token is valid, and the shop it belongs to is on a plan without API access: the free plan, or no plan at all. The API is on every paid plan. Nothing was read or written. The token stays live and works again the moment the shop is on a paid plan, so there is nothing to re-mint; do not retry until then, because every retry answers this."
          },
          "404": {
            "description": "The payload named a `shop` other than the one the token authenticates as. 404 and never 403, and it is the same bodiless answer an id that never existed gets - a 403 would confirm the named shop exists, which tells one merchant something about another. Drop the `shop` field or spell it as the token’s own shop; the shop is always taken from the token."
          },
          "413": {
            "content": {
              "application/json": {
                "example": {
                  "error": "boxes.tooMany",
                  "limit": 500,
                  "received": 900
                },
                "schema": {
                  "$ref": "#/components/schemas/BulkRequestError"
                }
              }
            },
            "description": "More than 500 rows. Nothing was written - send the catalogue as several requests."
          },
          "429": {
            "description": "Too many requests on this token. Wait the number of seconds in `Retry-After` and retry; the response has no body, because naming the remaining budget would be a side channel on it. The ceiling depends on the shop’s plan. Each token has a bucket that holds the burst, starts full and refills at the sustained rate, so a rested token may fire the burst at once and then the sustained rate: `starter` 60 a minute with bursts to 120; `growth` 120 a minute with bursts to 240; `pro` 300 a minute with bursts to 600; `business` 600 a minute with bursts to 1200; `starter-annual` 60 a minute with bursts to 120; `growth-annual` 120 a minute with bursts to 240; `pro-annual` 300 a minute with bursts to 600; `business-annual` 600 a minute with bursts to 1200. Those numbers are per-token, per-instance, not shared across Cloud Run instances - so they are not a guarantee in either direction and must not be treated as one. Obey the header rather than pacing against the numbers. `Retry-After` is 60 on every plan, which is long enough for every plan’s bucket to refill at least its sustained number. A refused request spends nothing from the bucket and nothing escalates - the wait never grows and the token is never locked - so the API puts no cap on how many times you retry. Put one in your own client.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying. The bucket refills continuously, so a slot often frees sooner.",
                "example": 60,
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "A fault inside the app, such as the database refusing a write. Rows are written one at a time in request order, so the rows before the fault may already be stored and the rest are not. The page is idempotent, so resend it whole. Any body is not part of this contract."
          }
        },
        "summary": "Create or update boxes in bulk, keyed by name",
        "tags": [
          "Boxes"
        ]
      }
    },
    "/api/calculations/{id}": {
      "description": "GET only. Any other method but OPTIONS is a 405 with `Allow: GET, HEAD` and no body. HEAD is served and returns the same headers with no body. OPTIONS is a CORS preflight: 204, no body, no token needed. See \"Calling from a browser\" in the API description.",
      "get": {
        "description": "Answers one entry of the calculation log for the shop the token authenticates as: where the cart was going, which boxes the app packed it into, what went in each, the rates the buyer was offered and any carrier that did not answer. This is the endpoint a packing-slip generator calls with the id the log hands out.  IT IS THE SAME ANSWER THE MERCHANT SEES. The body is built by the code that renders the app’s own calculation page, and its display strings are that page’s strings in the shop’s language. Send `?locale=` for another supported language. For raw numbers per box, read `/api/orders/{id}/packing`, which carries them for a machine.  A 200 IS NOT A GOOD CALCULATION. A checkout whose carriers all failed is a row recording a failure, read successfully, so it is a 200 and `outcome` says what happened. This read has no partial state and never answers 207: that status belongs to the bulk writes, where some rows of one request can land and others not.  The log keeps 30 days. An id older than that, an id that never existed, and an id belonging to another shop are ONE bodiless 404 - a caller that could tell them apart could ask this endpoint which ids are real, and whose.",
        "operationId": "getCalculation",
        "parameters": [
          {
            "description": "The calculation id, as the calculation log shows it.",
            "example": "cmfk0p2vq0001l508a1b2c3d4",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Optional, and only ever COMPARED. The shop is taken from the token; naming a different shop is a 404.",
            "example": "sgpfl.myshopify.com",
            "in": "query",
            "name": "shop",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "The language for the display strings. One of the supported locales; anything else falls back to the shop’s own language.",
            "example": "fr",
            "in": "query",
            "name": "locale",
            "required": false,
            "schema": {
              "enum": [
                "en",
                "es",
                "fr",
                "de",
                "pt-BR",
                "ja"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "carrierErrors": [
                    {
                      "body": "No detail recorded",
                      "carrier": "ups",
                      "carrierName": "UPS",
                      "code": "timeout",
                      "elapsed": "8,000 ms",
                      "explanation": null,
                      "id": "ups:0",
                      "requiresReconnect": false,
                      "status": "No response"
                    }
                  ],
                  "destination": "Toronto, ON, M4B 1B3, CA",
                  "facts": [
                    {
                      "label": "Source",
                      "value": "Checkout"
                    },
                    {
                      "label": "Counted toward your plan",
                      "value": "Yes"
                    },
                    {
                      "label": "Items",
                      "value": "2 items"
                    },
                    {
                      "label": "Boxes",
                      "value": "1 box"
                    },
                    {
                      "label": "Address treated as",
                      "value": "Residential"
                    },
                    {
                      "label": "Cart value",
                      "value": "CA$184.00"
                    },
                    {
                      "label": "Packing time",
                      "value": "4 ms"
                    },
                    {
                      "label": "Total time",
                      "value": "1,212 ms"
                    }
                  ],
                  "id": "cmfk0p2vq0001l508a1b2c3d4",
                  "lineItems": [
                    {
                      "id": "SG-2001:0",
                      "label": "SG-2001",
                      "quantity": "1",
                      "weight": "900 g"
                    },
                    {
                      "id": "SG-2002:1",
                      "label": "SG-2002",
                      "quantity": "1",
                      "weight": "240 g"
                    }
                  ],
                  "locale": "en",
                  "outcome": "rates",
                  "outcomeLabel": "2 rates",
                  "packages": [
                    {
                      "boxName": "Medium mailer",
                      "contents": "SG-2001, SG-2002",
                      "dimensions": "12 × 9 × 4 in",
                      "id": "cmfk0p2vq0002l508e5f6g7h8",
                      "note": null,
                      "position": "Box 1 of 1",
                      "weight": "2.5 lb"
                    }
                  ],
                  "packingSummary": "1 × Medium mailer",
                  "rates": [
                    {
                      "account": null,
                      "accountLast4": null,
                      "amount": "CA$14.20",
                      "carrier": "canada-post",
                      "carrierName": "Canada Post",
                      "id": "canada-post:DOM.EP:0",
                      "name": "Expedited Parcel",
                      "negotiated": null,
                      "pricing": null,
                      "savings": null,
                      "transit": "2 days"
                    },
                    {
                      "account": null,
                      "accountLast4": null,
                      "amount": "CA$11.85",
                      "carrier": "canada-post",
                      "carrierName": "Canada Post",
                      "id": "canada-post:DOM.RP:1",
                      "name": "Regular Parcel",
                      "negotiated": null,
                      "pricing": null,
                      "savings": null,
                      "transit": "4 days"
                    }
                  ],
                  "requestedAt": "2026-09-14T19:04:11.000Z",
                  "when": "Sep 14, 2026, 3:04 PM"
                },
                "schema": {
                  "$ref": "#/components/schemas/Calculation"
                }
              }
            },
            "description": "The calculation exists on this shop. Read `outcome` before anything else. The example is a checkout packed into one box, with one carrier that timed out."
          },
          "401": {
            "description": "The request carried no bearer token, a malformed `Authorization` header, a token this shop never minted, a revoked token, or reached a revision holding no token pepper. All five are this exact response, byte for byte and with no body: a refusal a caller could tell apart would let one merchant enumerate another merchant’s tokens. Mint a new token on the app’s API tokens screen and retry; there is nothing in the response to branch on.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always the bare challenge `Bearer`, with no `error=` parameter.",
                "example": "Bearer",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "error": "api-not-on-plan"
                },
                "schema": {
                  "$ref": "#/components/schemas/ApiAccessError"
                }
              }
            },
            "description": "The token is valid, and the shop it belongs to is on a plan without API access: the free plan, or no plan at all. The API is on every paid plan. Nothing was read or written. The token stays live and works again the moment the shop is on a paid plan, so there is nothing to re-mint; do not retry until then, because every retry answers this."
          },
          "404": {
            "description": "No such calculation for this shop. The SAME bodiless answer for an id that never existed, one past the 30-day retention, and one belonging to another shop, so none of the three can be told from the others. It is also the answer when `?shop=` named a shop other than the token’s. Nothing here is retryable; check the id."
          },
          "429": {
            "description": "Too many requests on this token. Wait the number of seconds in `Retry-After` and retry; the response has no body, because naming the remaining budget would be a side channel on it. The ceiling depends on the shop’s plan. Each token has a bucket that holds the burst, starts full and refills at the sustained rate, so a rested token may fire the burst at once and then the sustained rate: `starter` 60 a minute with bursts to 120; `growth` 120 a minute with bursts to 240; `pro` 300 a minute with bursts to 600; `business` 600 a minute with bursts to 1200; `starter-annual` 60 a minute with bursts to 120; `growth-annual` 120 a minute with bursts to 240; `pro-annual` 300 a minute with bursts to 600; `business-annual` 600 a minute with bursts to 1200. Those numbers are per-token, per-instance, not shared across Cloud Run instances - so they are not a guarantee in either direction and must not be treated as one. Obey the header rather than pacing against the numbers. `Retry-After` is 60 on every plan, which is long enough for every plan’s bucket to refill at least its sustained number. A refused request spends nothing from the bucket and nothing escalates - the wait never grows and the token is never locked - so the API puts no cap on how many times you retry. Put one in your own client.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying. The bucket refills continuously, so a slot often frees sooner.",
                "example": 60,
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "content": {
              "application/json": {
                "example": {
                  "error": "calculation-read-failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/CalculationError"
                }
              }
            },
            "description": "The calculation log could not be read. A fault in this app, not in your request; retry, and tell support if it persists."
          }
        },
        "summary": "Read one calculation, as the admin screen shows it",
        "tags": [
          "Calculations"
        ]
      }
    },
    "/api/dimensions": {
      "description": "POST only. Any other method but OPTIONS is a 405 with `Allow: POST` and no body. OPTIONS is a CORS preflight: 204, no body, no token needed. See \"Calling from a browser\" in the API description.",
      "post": {
        "description": "Writes measurements, weights, pack mode and carton lists for up to 500 variants of the shop the token authenticates as. This is the endpoint a nightly job pushes a catalogue through.  A ROW REPLACES, IT DOES NOT PATCH - the opposite of `/api/boxes`. A field left out of a row is reset to its default rather than kept, so a job that sends only the lengths and a unit clears the stored weight and padding, sets `keepVertical` false and puts `packMode` back to `pack-into-box`. Only `parts` is kept when absent. `DimensionRow` lists what each field resets to and what counts as blank.  RE-POSTING THE SAME PAYLOAD IS A NO-OP, not an upsert that happens to write the same values: a row whose stored state already matches is not written at all, is reported `unchanged`, and does not move `updatedAt`. A retried job therefore costs no writes, and the dimensions screen keeps showing when a measurement last CHANGED rather than when a cron last ran.  Keys are verified against Shopify - an id as well as a SKU - so an unknown key is a row-level `notFound` and never a created variant. Every other rule is the admin form’s own validator, called on a `FormData` built from the row, so the screen and the feed cannot drift.  An unknown key in a row is a `dimensions.row.unknownField` refusal rather than a silent drop, which is the same call `/api/boxes` makes with `row.unknownField`: a misspelt `weightUnit` is a column you believe you are pushing, so it is named and the row is not written. Every unknown key in the row is reported, not just the first, and the same is true inside each carton of `parts` - a nested one is `dimensions.part.unknownField` and carries the `partIndex` it was at, so a forty-carton kit names the carton you have to fix.",
        "operationId": "upsertVariantDimensions",
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "variants": [
                  {
                    "height": 6,
                    "keepVertical": false,
                    "length": 24,
                    "packMode": "pack-into-box",
                    "padding": 0.25,
                    "sku": "SG-1001",
                    "unit": "in",
                    "weight": 14.5,
                    "weightUnit": "lb",
                    "width": 18
                  },
                  {
                    "height": 8,
                    "keepVertical": true,
                    "length": 48,
                    "packMode": "ship-as-is",
                    "parts": [
                      {
                        "height": 5,
                        "length": 32,
                        "name": "Cooktop",
                        "packMode": "ship-as-is",
                        "unit": "in",
                        "weight": 40,
                        "weightUnit": "lb",
                        "width": 22
                      },
                      {
                        "height": 3,
                        "length": 12,
                        "name": "Hardware carton",
                        "unit": "in",
                        "weight": 3,
                        "weightUnit": "lb",
                        "width": 8
                      }
                    ],
                    "unit": "in",
                    "variantId": "gid://shopify/ProductVariant/43210987654322",
                    "weight": 43,
                    "weightUnit": "lb",
                    "width": 30
                  },
                  {
                    "height": 10,
                    "length": 10,
                    "sku": "SG-2002",
                    "unit": "in",
                    "width": 10
                  },
                  {
                    "height": 10,
                    "length": 10,
                    "sku": "SG-9999",
                    "unit": "in",
                    "width": 10
                  }
                ]
              },
              "schema": {
                "$ref": "#/components/schemas/DimensionsRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "counts": {
                    "failed": 0,
                    "unchanged": 1,
                    "written": 2
                  },
                  "results": [
                    {
                      "errors": [],
                      "index": 0,
                      "key": "SG-1001",
                      "keyKind": "sku",
                      "status": "written",
                      "variantId": "gid://shopify/ProductVariant/43210987654321"
                    },
                    {
                      "errors": [],
                      "index": 1,
                      "key": "gid://shopify/ProductVariant/43210987654322",
                      "keyKind": "variantId",
                      "status": "written",
                      "variantId": "gid://shopify/ProductVariant/43210987654322"
                    },
                    {
                      "errors": [],
                      "index": 2,
                      "key": "SG-2002",
                      "keyKind": "sku",
                      "status": "unchanged",
                      "variantId": "gid://shopify/ProductVariant/43210987654323"
                    }
                  ]
                },
                "schema": {
                  "$ref": "#/components/schemas/DimensionsResponse"
                }
              }
            },
            "description": "Every row landed, as `written` or `unchanged`. The example answers the request example above with its last row left out, where `SG-2002` already held those measurements."
          },
          "207": {
            "content": {
              "application/json": {
                "example": {
                  "counts": {
                    "failed": 1,
                    "unchanged": 1,
                    "written": 2
                  },
                  "results": [
                    {
                      "errors": [],
                      "index": 0,
                      "key": "SG-1001",
                      "keyKind": "sku",
                      "status": "written",
                      "variantId": "gid://shopify/ProductVariant/43210987654321"
                    },
                    {
                      "errors": [],
                      "index": 1,
                      "key": "gid://shopify/ProductVariant/43210987654322",
                      "keyKind": "variantId",
                      "status": "written",
                      "variantId": "gid://shopify/ProductVariant/43210987654322"
                    },
                    {
                      "errors": [],
                      "index": 2,
                      "key": "SG-2002",
                      "keyKind": "sku",
                      "status": "unchanged",
                      "variantId": "gid://shopify/ProductVariant/43210987654323"
                    },
                    {
                      "errors": [
                        {
                          "code": "dimensions.key.notFound",
                          "field": "sku"
                        }
                      ],
                      "index": 3,
                      "key": "SG-9999",
                      "keyKind": "sku",
                      "status": "notFound",
                      "variantId": null
                    }
                  ]
                },
                "schema": {
                  "$ref": "#/components/schemas/DimensionsResponse"
                }
              }
            },
            "description": "At least one row failed and every other row landed. The same body as the 200, so a caller reads one shape; the status is what makes a dropped row impossible to miss, and it is the status `/api/boxes` answers in the same case. The example answers the request example above as sent, where `SG-9999` is not in the catalogue."
          },
          "400": {
            "content": {
              "application/json": {
                "example": {
                  "error": "variants-not-an-array"
                },
                "schema": {
                  "$ref": "#/components/schemas/DimensionsRequestError"
                }
              }
            },
            "description": "The body is not JSON, or carries no `variants` array. Nothing was written."
          },
          "401": {
            "description": "The request carried no bearer token, a malformed `Authorization` header, a token this shop never minted, a revoked token, or reached a revision holding no token pepper. All five are this exact response, byte for byte and with no body: a refusal a caller could tell apart would let one merchant enumerate another merchant’s tokens. Mint a new token on the app’s API tokens screen and retry; there is nothing in the response to branch on.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always the bare challenge `Bearer`, with no `error=` parameter.",
                "example": "Bearer",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "error": "api-not-on-plan"
                },
                "schema": {
                  "$ref": "#/components/schemas/ApiAccessError"
                }
              }
            },
            "description": "The token is valid, and the shop it belongs to is on a plan without API access: the free plan, or no plan at all. The API is on every paid plan. Nothing was read or written. The token stays live and works again the moment the shop is on a paid plan, so there is nothing to re-mint; do not retry until then, because every retry answers this."
          },
          "404": {
            "description": "The payload named a `shop` other than the one the token authenticates as. 404 and never 403, and it is the same bodiless answer an id that never existed gets - a 403 would confirm the named shop exists, which tells one merchant something about another. Drop the `shop` field or spell it as the token’s own shop; the shop is always taken from the token."
          },
          "413": {
            "content": {
              "application/json": {
                "example": {
                  "error": "too-many-variants",
                  "max": 500,
                  "sent": 900
                },
                "schema": {
                  "$ref": "#/components/schemas/DimensionsRequestError"
                }
              }
            },
            "description": "More than 500 rows. The request is refused whole and nothing is written."
          },
          "429": {
            "description": "Too many requests on this token. Wait the number of seconds in `Retry-After` and retry; the response has no body, because naming the remaining budget would be a side channel on it. The ceiling depends on the shop’s plan. Each token has a bucket that holds the burst, starts full and refills at the sustained rate, so a rested token may fire the burst at once and then the sustained rate: `starter` 60 a minute with bursts to 120; `growth` 120 a minute with bursts to 240; `pro` 300 a minute with bursts to 600; `business` 600 a minute with bursts to 1200; `starter-annual` 60 a minute with bursts to 120; `growth-annual` 120 a minute with bursts to 240; `pro-annual` 300 a minute with bursts to 600; `business-annual` 600 a minute with bursts to 1200. Those numbers are per-token, per-instance, not shared across Cloud Run instances - so they are not a guarantee in either direction and must not be treated as one. Obey the header rather than pacing against the numbers. `Retry-After` is 60 on every plan, which is long enough for every plan’s bucket to refill at least its sustained number. A refused request spends nothing from the bucket and nothing escalates - the wait never grows and the token is never locked - so the API puts no cap on how many times you retry. Put one in your own client.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying. The bucket refills continuously, so a slot often frees sooner.",
                "example": 60,
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "500": {
            "description": "A fault inside the app, or an error or throttle from Shopify while the keys were being looked up. Both happen before the first write, and a failed write is reported on its own row as `dimensions.write.failed` rather than here, so nothing was written. Resend the page. Any body is not part of this contract."
          },
          "503": {
            "content": {
              "application/json": {
                "example": {
                  "error": "shopify-unavailable"
                },
                "schema": {
                  "$ref": "#/components/schemas/DimensionsRequestError"
                }
              }
            },
            "description": "The app has no live session for this shop, usually an uninstall that has not been processed yet, so no variant key can be resolved and nothing was written. This is not a payload problem, and it does not clear on a timer: the app gets a session for a shop when the merchant installs, so it comes back only with a reinstall. Retry at your next scheduled run rather than in a loop."
          }
        },
        "summary": "Create or update variant dimensions in bulk, keyed by SKU or variant id",
        "tags": [
          "Dimensions"
        ]
      }
    },
    "/api/mcp": {
      "description": "POST only. GET and DELETE are a 405, as the 2026-07-28 Streamable HTTP transport specifies. OPTIONS is a CORS preflight: 204, no body, no token needed. See \"Calling from a browser\" in the API description.",
      "post": {
        "description": "A Model Context Protocol server over Streamable HTTP, stateless. Point an MCP client at this URL with the same `Authorization: Bearer <token>` header every other path takes; the same plan gate and the same per-token rate limit apply, one slot per HTTP request.  TOOLS: `get_calculation` (`getCalculation`) and `get_order_packing` (`getOrderPacking`). Each tool IS the GET operation it is named after: its arguments are that operation’s parameters, and a successful call answers the operation’s 200 body byte for byte, as a text block and as `structuredContent`. Any other status is a tool error (`isError: true`) whose text is the REST body, or `{\"status\":404}` where REST answers with no body. The bulk writes are not tools.  Both protocol eras are served: a 2025-era client (`initialize`, no envelope) is answered one request at a time and is never handed an `Mcp-Session-Id`. `subscriptions/listen` is refused. A request carrying a browser `Origin` the server does not allow is a 403.",
        "operationId": "callMcp",
        "requestBody": {
          "content": {
            "application/json": {
              "example": {
                "id": 1,
                "jsonrpc": "2.0",
                "method": "tools/call",
                "params": {
                  "arguments": {
                    "id": "cm1abc234def"
                  },
                  "name": "get_calculation"
                }
              },
              "schema": {
                "description": "A JSON-RPC 2.0 request.",
                "type": "object"
              }
            }
          },
          "description": "One JSON-RPC 2.0 request, as the MCP transport specifies.",
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "id": 1,
                  "jsonrpc": "2.0",
                  "result": {
                    "content": [
                      {
                        "text": "{\"id\":\"cm1abc234def\",\"locale\":\"en\", ...}",
                        "type": "text"
                      }
                    ],
                    "structuredContent": {
                      "id": "cm1abc234def",
                      "locale": "en"
                    }
                  }
                },
                "schema": {
                  "description": "A JSON-RPC 2.0 response.",
                  "type": "object"
                }
              }
            },
            "description": "The JSON-RPC answer. The example is abridged: a real `get_calculation` result carries the whole `Calculation` body. A tool that REST refused is still a 200 here, with `isError: true` on the result."
          },
          "401": {
            "description": "The request carried no bearer token, a malformed `Authorization` header, a token this shop never minted, a revoked token, or reached a revision holding no token pepper. All five are this exact response, byte for byte and with no body: a refusal a caller could tell apart would let one merchant enumerate another merchant’s tokens. Mint a new token on the app’s API tokens screen and retry; there is nothing in the response to branch on.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always the bare challenge `Bearer`, with no `error=` parameter.",
                "example": "Bearer",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "error": "api-not-on-plan"
                },
                "schema": {
                  "$ref": "#/components/schemas/ApiAccessError"
                }
              }
            },
            "description": "The token is valid, and the shop it belongs to is on a plan without API access: the free plan, or no plan at all. The API is on every paid plan. Nothing was read or written. The token stays live and works again the moment the shop is on a paid plan, so there is nothing to re-mint; do not retry until then, because every retry answers this."
          },
          "429": {
            "description": "Too many requests on this token. Wait the number of seconds in `Retry-After` and retry; the response has no body, because naming the remaining budget would be a side channel on it. The ceiling depends on the shop’s plan. Each token has a bucket that holds the burst, starts full and refills at the sustained rate, so a rested token may fire the burst at once and then the sustained rate: `starter` 60 a minute with bursts to 120; `growth` 120 a minute with bursts to 240; `pro` 300 a minute with bursts to 600; `business` 600 a minute with bursts to 1200; `starter-annual` 60 a minute with bursts to 120; `growth-annual` 120 a minute with bursts to 240; `pro-annual` 300 a minute with bursts to 600; `business-annual` 600 a minute with bursts to 1200. Those numbers are per-token, per-instance, not shared across Cloud Run instances - so they are not a guarantee in either direction and must not be treated as one. Obey the header rather than pacing against the numbers. `Retry-After` is 60 on every plan, which is long enough for every plan’s bucket to refill at least its sustained number. A refused request spends nothing from the bucket and nothing escalates - the wait never grows and the token is never locked - so the API puts no cap on how many times you retry. Put one in your own client.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying. The bucket refills continuously, so a slot often frees sooner.",
                "example": 60,
                "schema": {
                  "type": "integer"
                }
              }
            }
          }
        },
        "summary": "The API as a remote MCP server",
        "tags": [
          "MCP"
        ]
      }
    },
    "/api/openapi.json": {
      "get": {
        "description": "Serves this OpenAPI document. Unauthenticated, because a contract is public and carries nothing about any shop.  The bytes are the committed `openapi.json` in the app’s own repository, and a gate in `npm run verify` fails when the two differ, so a client generated from this URL is generated from the same file a reviewer read in the pull request.",
        "operationId": "getOpenApiDocument",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "example": {
                  "info": {
                    "title": "Box Warden API",
                    "version": "1.2.0"
                  },
                  "openapi": "3.1.0",
                  "paths": {}
                },
                "schema": {
                  "description": "An OpenAPI 3.1 document.",
                  "type": "object"
                }
              }
            },
            "description": "The document. The example is abridged; the real body is this whole file."
          }
        },
        "security": [],
        "summary": "This document",
        "tags": [
          "Reference"
        ]
      }
    },
    "/api/orders/{id}/packing": {
      "description": "GET only. Any other method but OPTIONS is a 405 with `Allow: GET, HEAD` and no body. HEAD is served and returns the same headers with no body. OPTIONS is a CORS preflight: 204, no body, no token needed. See \"Calling from a browser\" in the API description.",
      "get": {
        "description": "Answers the packing summary for one order of the shop the token authenticates as: which boxes the app chose, what went in each, their packed dimensions and weight, and the service the buyer picked. This is the endpoint a warehouse system calls when a pick ticket prints.  IT IS THE SAME ANSWER THE MERCHANT SEES. The body is read off the one order metafield the app writes and is built by the same code the order page’s own block renders, so a packer reading your screen and a packer reading the app’s cannot be told different things.  READ `state` FIRST AND BRANCH ON IT. A 200 does NOT mean the order was packed by this app: `none` is a legitimate 200 and is common, because the app prices at checkout and an order placed before install never went through it. Only `packed` carries boxes.  NOTHING IS GUESSED. The stored summary carries a `version` (currently 1), it is read before anything else, and a version this release does not know answers `unreadable` rather than being picked over for the fields that happen to still be there.  The `:id` may be the numeric order id or the whole `gid://shopify/Order/<n>`. An id that is not an order id, an order this shop does not have, and an order belonging to another shop are ONE bodiless 404 - a caller that could tell them apart could ask this endpoint which of a list of order ids are real and whose they are.",
        "operationId": "getOrderPacking",
        "parameters": [
          {
            "description": "The order, as the numeric id or as the full `gid://shopify/Order/<n>`. A GID carries slashes, so url-encode it if you send that spelling.",
            "example": "5550001",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Optional, and only ever COMPARED. The shop is taken from the token; sending this is how a client proves to itself which tenant it meant. Naming a different shop is a 404.",
            "example": "sgpfl.myshopify.com",
            "in": "query",
            "name": "shop",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "examples": {
                  "none": {
                    "summary": "An order that exists and that nothing packed",
                    "value": {
                      "boxes": [],
                      "calculatedAt": null,
                      "orderGid": "gid://shopify/Order/5550001",
                      "packageCount": 0,
                      "service": null,
                      "state": "none",
                      "summary": null
                    }
                  },
                  "packed": {
                    "summary": "An order the app priced, packed into two boxes",
                    "value": {
                      "boxes": [
                        {
                          "boxName": "Medium mailer",
                          "carrierPackagingKey": null,
                          "height": 4,
                          "items": [
                            {
                              "partPosition": null,
                              "quantity": 1,
                              "sku": "SG-2001",
                              "title": "Cast iron grate",
                              "variantGid": "gid://shopify/ProductVariant/43210987654321"
                            }
                          ],
                          "length": 12,
                          "position": 0,
                          "shipAsIs": false,
                          "unit": "in",
                          "weight": 2.5,
                          "weightUnit": "lb",
                          "width": 9
                        },
                        {
                          "boxName": "Medium mailer",
                          "carrierPackagingKey": null,
                          "height": 4,
                          "items": [
                            {
                              "partPosition": 0,
                              "quantity": 1,
                              "sku": "SG-2002",
                              "title": "Burner kit",
                              "variantGid": "gid://shopify/ProductVariant/43210987654323"
                            }
                          ],
                          "length": 12,
                          "position": 1,
                          "shipAsIs": false,
                          "unit": "in",
                          "weight": 3.1,
                          "weightUnit": "lb",
                          "width": 9
                        }
                      ],
                      "calculatedAt": "2026-09-22T18:04:11.000Z",
                      "orderGid": "gid://shopify/Order/5550001",
                      "packageCount": 2,
                      "service": {
                        "carrier": "usps",
                        "code": "USPS_GA",
                        "title": "Ground Advantage"
                      },
                      "state": "packed",
                      "summary": "2 × Medium mailer"
                    }
                  }
                },
                "schema": {
                  "$ref": "#/components/schemas/OrderPacking"
                }
              }
            },
            "description": "The order exists on this shop. Read `state` before anything else: this is also the answer for an order the app never packed. The first example is a packed order; the second is an order carrying no packing summary, which is a 200 and not a 404."
          },
          "401": {
            "description": "The request carried no bearer token, a malformed `Authorization` header, a token this shop never minted, a revoked token, or reached a revision holding no token pepper. All five are this exact response, byte for byte and with no body: a refusal a caller could tell apart would let one merchant enumerate another merchant’s tokens. Mint a new token on the app’s API tokens screen and retry; there is nothing in the response to branch on.",
            "headers": {
              "WWW-Authenticate": {
                "description": "Always the bare challenge `Bearer`, with no `error=` parameter.",
                "example": "Bearer",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "403": {
            "content": {
              "application/json": {
                "example": {
                  "error": "api-not-on-plan"
                },
                "schema": {
                  "$ref": "#/components/schemas/ApiAccessError"
                }
              }
            },
            "description": "The token is valid, and the shop it belongs to is on a plan without API access: the free plan, or no plan at all. The API is on every paid plan. Nothing was read or written. The token stays live and works again the moment the shop is on a paid plan, so there is nothing to re-mint; do not retry until then, because every retry answers this."
          },
          "404": {
            "description": "No such order for this shop. The SAME bodiless answer for three different facts - the id is not an order id, the order does not exist, and the order belongs to another shop - so that none of the three can be told from the others. It is also the answer when `?shop=` named a shop other than the token’s. Nothing here is retryable; check the id."
          },
          "429": {
            "description": "Too many requests on this token. Wait the number of seconds in `Retry-After` and retry; the response has no body, because naming the remaining budget would be a side channel on it. The ceiling depends on the shop’s plan. Each token has a bucket that holds the burst, starts full and refills at the sustained rate, so a rested token may fire the burst at once and then the sustained rate: `starter` 60 a minute with bursts to 120; `growth` 120 a minute with bursts to 240; `pro` 300 a minute with bursts to 600; `business` 600 a minute with bursts to 1200; `starter-annual` 60 a minute with bursts to 120; `growth-annual` 120 a minute with bursts to 240; `pro-annual` 300 a minute with bursts to 600; `business-annual` 600 a minute with bursts to 1200. Those numbers are per-token, per-instance, not shared across Cloud Run instances - so they are not a guarantee in either direction and must not be treated as one. Obey the header rather than pacing against the numbers. `Retry-After` is 60 on every plan, which is long enough for every plan’s bucket to refill at least its sustained number. A refused request spends nothing from the bucket and nothing escalates - the wait never grows and the token is never locked - so the API puts no cap on how many times you retry. Put one in your own client.",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before retrying. The bucket refills continuously, so a slot often frees sooner.",
                "example": 60,
                "schema": {
                  "type": "integer"
                }
              }
            }
          },
          "502": {
            "content": {
              "application/json": {
                "example": {
                  "error": "order-read-failed"
                },
                "schema": {
                  "$ref": "#/components/schemas/OrderPackingError"
                }
              }
            },
            "description": "Shopify’s Admin API did not answer the query. This is a fault between this app and Shopify - usually an installation that predates the `read_orders` scope and was never re-prompted, or a revoked grant. Your request was fine. Retry, and tell support if it persists."
          },
          "503": {
            "content": {
              "application/json": {
                "example": {
                  "error": "shopify-unavailable"
                },
                "schema": {
                  "$ref": "#/components/schemas/OrderPackingError"
                }
              }
            },
            "description": "The app has no live session for this shop, usually an uninstall that has not been processed yet, so the order could not be read. The order is not gone. Retry later; this is not a payload problem."
          }
        },
        "summary": "Read how one order was packed",
        "tags": [
          "Orders"
        ]
      }
    }
  },
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "servers": [
    {
      "description": "Box Warden’s one production host. Every shop that installs the app is served from it, so there is no per-shop or per-region URL: prefix each path below with this origin.",
      "url": "https://boxwarden.shelfwarden.app"
    }
  ],
  "tags": [
    {
      "description": "The merchant’s own box catalogue.",
      "name": "Boxes"
    },
    {
      "description": "Per-variant measurements, weights and carton lists.",
      "name": "Dimensions"
    },
    {
      "description": "How an order was packed, for the warehouse that packs it.",
      "name": "Orders"
    },
    {
      "description": "One entry of the calculation log, for a packing slip or a support question.",
      "name": "Calculations"
    },
    {
      "description": "The contract itself.",
      "name": "Reference"
    },
    {
      "description": "The reads above, as tools for an MCP client.",
      "name": "MCP"
    }
  ]
}
