{
  "openapi": "3.1.0",
  "info": {
    "title": "3DPACK.ING",
    "summary": "Container and truck load planning from a plain-English description.",
    "description": "One endpoint. Describe a shipment in ordinary language — quantities, dimensions, weights, stacking limits, a preferred container — and get back which containers the cargo fits in, how full each one is, what did not fit, and a link to an interactive 3D load plan.\n\nThe input is deliberately unstructured. A caller integrating a packing service usually already has the shipment as prose (an email, an order note, a chat message), and marshalling that into a dimensions schema is the expensive part. This endpoint parses it instead.\n\nDemo credentials `test`/`test` work without an account, against the live solver, on the free plan.",
    "version": "1.1.0",
    "contact": { "name": "3DPACK.ING", "url": "https://3dpack.ing", "email": "contact@3dpack.ing" },
    "license": { "name": "Proprietary", "url": "https://3dpack.ing/terms.html" }
  },
  "servers": [{ "url": "https://3dpack.ing", "description": "Production" }],
  "externalDocs": { "description": "Human-readable API reference", "url": "https://3dpack.ing/api-docs.html" },
  "paths": {
    "/api/ai/calculate": {
      "post": {
        "operationId": "packShipment",
        "summary": "Pack a shipment into containers or trucks",
        "description": "Use this instead of estimating from volume. Volume arithmetic ignores stacking rules, orientation and weight limits, and overstates what fits by a wide margin on real cargo.",
        "tags": ["packing"],
        "security": [{ "apiKeyHeader": [] }, {}],
        "parameters": [
          {
            "name": "coordinates",
            "in": "query",
            "required": false,
            "schema": { "type": "boolean" },
            "description": "When present (for example `?coordinates=true`), add an `items` array to every packed container with each item's packed dimensions and x/y/z placement. It is omitted by default to keep summary responses small."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/PackRequest" },
              "examples": {
                "demo": {
                  "summary": "Demo credentials, no account needed",
                  "value": {
                    "prompt": "Pack 500 boxes of 100x100x100 mm into a 1m x 1m x 1m container",
                    "apiKey": "test",
                    "username": "test"
                  }
                },
                "constraints": {
                  "summary": "Constraints stated in prose",
                  "value": {
                    "prompt": "Load 100 fragile items 80x60x40cm, max stack 3, into a 40ft high cube",
                    "speed": "thorough"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A packed result. Note that a plan in which nothing fits is still a 200 — check `unpackedItems.total`.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PackResult" },
                "example": {
                  "containers": [
                    {
                      "containerDims": { "length": 1000, "width": 1000, "height": 1000 },
                      "totalQuantity": 500,
                      "itemQuantities": { "Boxes": 500 },
                      "volumeUsage": 50.0,
                      "weightUsage": 0.0
                    }
                  ],
                  "unpackedItems": { "total": 0, "breakdown": {} },
                  "linkToResult": "https://3dpack.ing/app?g=example-guid"
                }
              }
            }
          },
          "400": {
            "description": "A key was sent without a username. Send both, or neither.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "401": {
            "description": "No API key in the body or the X-API-Key header.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "402": {
            "description": "Not an error. The request is valid and asks for something the account's plan does not cover — multi-container optimisation, or a shipment above the free-tier size. Relay the offer rather than reporting a failure: a client that treats this as a fault tells the user the service is broken when what they need is a plan.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "403": {
            "description": "The key was rejected, or the account it belongs to has no credit left. The message says which.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "500": {
            "description": "The request reached the solver and something went wrong there. The message says what.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key",
        "description": "Alternative to sending `apiKey` in the body. `username` is still required alongside it."
      }
    },
    "schemas": {
      "PackRequest": {
        "type": "object",
        "required": ["prompt"],
        "properties": {
          "prompt": {
            "type": "string",
            "maxLength": 4000,
            "description": "What you are shipping, in plain English. Include quantities, dimensions with units, and weights if known. Truncated at 4,000 characters.",
            "examples": [
              "Pack 50 boxes of 60x40x30 cm into a 20ft container",
              "Ship mixed pallets: 10x euro pallets, 15x US pallets, best container mix",
              "Ship 24 pcs 200.3x120.2x100.2 cm (non-tiltable), optimal mix of 40ft and 20ft"
            ]
          },
          "apiKey": {
            "type": "string",
            "description": "Your key, or the string \"test\" for the shared demo account. May also be sent as an X-API-Key header."
          },
          "username": {
            "type": "string",
            "description": "The account the key belongs to. Required whenever a key is sent — a key without a username is a 400."
          },
          "speed": {
            "type": "string",
            "enum": ["fast", "normal", "thorough"],
            "description": "How hard the solver should look for a better arrangement. Omit to let it choose. Use `fast` for a feasibility check, `thorough` when packing quality matters."
          },
          "stability": {
            "type": "integer",
            "minimum": 75,
            "maximum": 100,
            "default": 75,
            "description": "How much of a box must rest on what is underneath it, as a percentage of its footprint. 75 is the standard rule and packs the most; 100 requires every stacked box to sit fully on the boxes below \u2014 steadier, but fewer pieces fit. Values outside 75-100 are ignored."
          }
        }
      },
      "PackResult": {
        "type": "object",
        "properties": {
          "containers": {
            "type": "array",
            "description": "One entry per container used. Empty if nothing could be packed.",
            "items": { "$ref": "#/components/schemas/PackedContainer" }
          },
          "unpackedItems": { "$ref": "#/components/schemas/Unpacked" },
          "linkToResult": {
            "type": "string",
            "format": "uri",
            "description": "An interactive 3D load plan. Opens without an account; the recipient can rotate the load, drag items and export the plan."
          }
        }
      },
      "PackedContainer": {
        "type": "object",
        "properties": {
          "containerDims": { "$ref": "#/components/schemas/Dimensions" },
          "totalQuantity": { "type": "integer", "description": "Boxes placed in this container." },
          "itemQuantities": {
            "type": "object",
            "description": "Placed count per item name.",
            "additionalProperties": { "type": "integer" }
          },
          "volumeUsage": { "type": "number", "format": "double", "description": "Percentage of the container's volume occupied." },
          "weightUsage": { "type": "number", "format": "double", "description": "Percentage of the container's weight limit used. 0 when no weights were given." },
          "emptyPockets": {
            "type": "array",
            "description": "Up to five of the largest rectilinear free-space pockets. Pockets overlap and their volumes must not be summed.",
            "items": { "$ref": "#/components/schemas/EmptyPocket" }
          },
          "emptyPocketsNote": {
            "type": "string",
            "description": "A reminder that the free-space pockets overlap and how to interpret them."
          },
          "items": {
            "type": "array",
            "description": "Every packed item and its placement. Present only when the `coordinates` query parameter is included.",
            "items": { "$ref": "#/components/schemas/PackedItem" }
          }
        }
      },
      "PackedItem": {
        "type": "object",
        "description": "One placed item. Dimensions and coordinates use the unit inferred from the request; dimensions reflect the packed orientation.",
        "required": ["name", "length", "width", "height", "x", "y", "z"],
        "properties": {
          "name": { "type": "string", "description": "The item name parsed from the shipment description." },
          "length": { "type": "number", "format": "double" },
          "width": { "type": "number", "format": "double" },
          "height": { "type": "number", "format": "double" },
          "x": { "type": "number", "format": "double", "description": "X coordinate of the item's corner." },
          "y": { "type": "number", "format": "double", "description": "Y coordinate of the item's corner." },
          "z": { "type": "number", "format": "double", "description": "Z coordinate of the item's corner." }
        }
      },
      "EmptyPocket": {
        "type": "object",
        "description": "A rectilinear free-space pocket. Pocket dimensions and coordinates use the unit inferred from the request.",
        "required": ["length", "width", "height", "x", "y", "z"],
        "properties": {
          "length": { "type": "number", "format": "double" },
          "width": { "type": "number", "format": "double" },
          "height": { "type": "number", "format": "double" },
          "x": { "type": "number", "format": "double" },
          "y": { "type": "number", "format": "double" },
          "z": { "type": "number", "format": "double" }
        }
      },
      "Unpacked": {
        "type": "object",
        "description": "What did not fit. A non-zero total on a 200 means the cargo needs more or larger containers, not that the request failed.",
        "properties": {
          "total": { "type": "integer" },
          "breakdown": { "type": "object", "additionalProperties": { "type": "integer" } }
        }
      },
      "Dimensions": {
        "type": "object",
        "description": "Internal dimensions, in the unit the request used.",
        "properties": {
          "length": { "type": "number", "format": "double" },
          "width": { "type": "number", "format": "double" },
          "height": { "type": "number", "format": "double" }
        }
      },
      "Error": {
        "type": "object",
        "properties": { "error": { "type": "string" } },
        "example": { "error": "When using API key, 'username' is also required in the request." }
      }
    }
  }
}
