{
  "openapi": "3.1.0",
  "info": {
    "title": "Angels Print — ordering API for AI agents",
    "version": "2026-08-24",
    "summary": "Search a print catalogue, price a job exactly, and prepare an order a human operator approves and pays.",
    "description": "Angels Print is a custom printing and signage shop in Sun Valley, CA. This API exists so an AI agent, told by its operator to \"print 500 business cards by Thursday\", can find the product, price it exactly and hand the operator a link to pay.\n\n**Orders are never charged automatically. An agent can only prepare a draft; a human operator must open the approval link, review the cart and pay via Stripe Checkout.**\n\nEvery price is recomputed on the server from the product configuration — at quote, at draft, and again when the operator pays. A price sent by a client is ignored, so an agent cannot invent, discount or inflate a total.\n\nA draft expires 7 days after it is created. The same five capabilities are exposed as MCP tools at https://angelsprint.com/api/mcp. Human-facing documentation: https://angelsprint.com/for-agents",
    "contact": {
      "name": "Angels Print",
      "email": "hello@angelsprint.com",
      "url": "https://angelsprint.com/for-agents"
    },
    "termsOfService": "https://angelsprint.com/terms"
  },
  "servers": [
    {
      "url": "https://angelsprint.com",
      "description": "Production"
    }
  ],
  "externalDocs": {
    "description": "Agent guide, MCP config and curl examples",
    "url": "https://angelsprint.com/for-agents"
  },
  "x-agent-policy": {
    "humanApprovalRequired": true,
    "agentCanPay": false,
    "statement": "Orders are never charged automatically. An agent can only prepare a draft; a human operator must open the approval link, review the cart and pay via Stripe Checkout.",
    "approvalUrlPattern": "https://angelsprint.com/a/{draft_id}",
    "agentCard": "https://angelsprint.com/.well-known/agents.json",
    "mcp": "https://angelsprint.com/api/mcp"
  },
  "tags": [
    {
      "name": "catalogue",
      "description": "Read-only product discovery."
    },
    {
      "name": "pricing",
      "description": "Authoritative server-side pricing."
    },
    {
      "name": "orders",
      "description": "Draft orders and the human approval handoff."
    }
  ],
  "paths": {
    "/api/agent/products": {
      "get": {
        "tags": [
          "catalogue"
        ],
        "operationId": "searchProducts",
        "summary": "Search or list the catalogue",
        "description": "Word search over name, group and description — every term must match, so \"kraft boxes\" does not return every box in the shop. Also returns the 6 categories with their product counts. MCP tool: `search_products`.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "description": "Free text, e.g. \"business cards\", \"vinyl banner\", \"mailer box\". Empty lists the catalogue.",
            "schema": {
              "type": "string",
              "maxLength": 120
            },
            "example": "business cards"
          },
          {
            "name": "category",
            "in": "query",
            "required": false,
            "description": "Restrict to one category.",
            "schema": {
              "type": "string",
              "enum": [
                "business-cards",
                "marketing-material",
                "banners-and-signs",
                "stands-and-displays",
                "labels-and-stickers",
                "packaging-and-boxes"
              ]
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Clamped to 1–50.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 10
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Matching products, best match first.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProductList"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited — `code: rate_limited`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/agent/products/{slug}": {
      "get": {
        "tags": [
          "catalogue"
        ],
        "operationId": "getProduct",
        "summary": "Read one product and its option surface",
        "description": "Option groups with the price each choice adds, the quantity ladder with price per piece, turnaround speeds with production days, and the size limits. Feed the choices back to `getQuote` as `selections`. MCP tool: `get_product`.",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "classic-business-cards-160"
          }
        ],
        "responses": {
          "200": {
            "description": "Product detail.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProductDetail"
                }
              }
            }
          },
          "404": {
            "description": "No product with that slug — `code: unknown_product`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/agent/quote": {
      "post": {
        "tags": [
          "pricing"
        ],
        "operationId": "getQuote",
        "summary": "Price one configured product exactly",
        "description": "Priced by the same engine that charges at checkout. Nothing is stored and nothing is charged — call it as often as you need while narrowing options. Keep `itemId` from the reply and pass it to `createOrder` so the price cannot drift. MCP tool: `get_quote`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/QuoteRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Priced configuration.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Quote"
                }
              }
            }
          },
          "400": {
            "description": "Unknown option, unknown option group, or a size the product does not support.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Unknown product slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The product cannot be priced with those options — `code: not_priceable`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/agent/orders": {
      "post": {
        "tags": [
          "orders"
        ],
        "operationId": "createOrder",
        "summary": "Prepare an order for a human to approve",
        "description": "Prices every line again, signs the result and returns `approval_url`. **Nothing is charged and no order row is written.** Hand the link to your operator; they review the cart there and pay with Stripe.\n\nThe draft travels inside `draft_id` (an HMAC-signed token — there is no database row) and expires after 7 days. Identify yourself in `agent` or the `X-Agent-Name` header; it is shown to the operator and recorded on the lead, never trusted.\n\nMCP tool: `create_order`.",
        "parameters": [
          {
            "name": "X-Agent-Name",
            "in": "header",
            "required": false,
            "description": "Who is ordering, e.g. \"Claude for Acme Design\". Used when `agent` is not in the body.",
            "schema": {
              "type": "string",
              "maxLength": 120
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateOrderRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Draft prepared. Send `approval_url` to the human operator.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrderDraft"
                }
              }
            }
          },
          "400": {
            "description": "Validation failed — missing or bad `operator.email`, empty `items`, more than 20 lines, unknown slug, bad `itemId`, non-https artwork URL, or a draft too large to fit in a link.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "A line cannot be priced with those options.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Draft signing is not configured on this deployment (`code: not_configured`); catalogue and quotes still work.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/agent/orders/{draft_id}": {
      "get": {
        "tags": [
          "orders"
        ],
        "operationId": "checkOrder",
        "summary": "Read a draft back",
        "description": "Poll to see whether the operator approved and paid. No table is consulted: the draft is re-read from its own signature and `paid` is answered by Stripe. Never cached. MCP tool: `check_order`.",
        "parameters": [
          {
            "name": "draft_id",
            "in": "path",
            "required": true,
            "description": "The `draft_id` from createOrder, URL-encoded. It is long — it carries the whole cart.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Draft state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/OrderStatus"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Draft signing is not configured on this deployment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "description": "Every failure has this shape and nothing else, so a model can branch on `code` without parsing English.",
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable explanation, safe to show the operator."
          },
          "code": {
            "type": "string",
            "description": "Stable machine code.",
            "enum": [
              "bad_request",
              "bad_json",
              "unknown_product",
              "unknown_option",
              "unknown_option_group",
              "size_not_supported",
              "not_priceable",
              "empty_order",
              "too_many_items",
              "missing_slug",
              "bad_item_id",
              "slug_mismatch",
              "operator_email_required",
              "bad_operator_email",
              "bad_artwork_url",
              "draft_too_large",
              "not_configured",
              "expired",
              "stale_item",
              "price_changed",
              "oversize_shipping_quote_required",
              "rate_limited",
              "server_error"
            ]
          },
          "slug": {
            "type": "string",
            "description": "On `stale_item`: which product went bad."
          }
        }
      },
      "ProductHit": {
        "type": "object",
        "required": [
          "slug",
          "name",
          "category",
          "priceFrom",
          "nextDay",
          "oversize",
          "url"
        ],
        "properties": {
          "slug": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "category": {
            "type": "string",
            "enum": [
              "business-cards",
              "marketing-material",
              "banners-and-signs",
              "stands-and-displays",
              "labels-and-stickers",
              "packaging-and-boxes"
            ]
          },
          "priceFrom": {
            "type": "number",
            "description": "Entry price in USD for the default configuration."
          },
          "unitQty": {
            "type": "integer",
            "description": "Pieces in the default run, on products sold in runs."
          },
          "unitPrice": {
            "type": "number",
            "description": "Price per piece at that default run."
          },
          "nextDay": {
            "type": "boolean",
            "description": "Next-day production is offered as a turnaround choice."
          },
          "oversize": {
            "type": "boolean",
            "description": "Excluded from free and flat-rate shipping; pickup or a separate shipping quote is required."
          },
          "url": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "CategoryCount": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "count": {
            "type": "integer"
          }
        }
      },
      "ProductList": {
        "type": "object",
        "required": [
          "count",
          "products",
          "categories"
        ],
        "properties": {
          "query": {
            "type": [
              "string",
              "null"
            ]
          },
          "category": {
            "type": [
              "string",
              "null"
            ]
          },
          "count": {
            "type": "integer",
            "description": "How many products came back (already limited)."
          },
          "categories": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CategoryCount"
            }
          },
          "products": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ProductHit"
            }
          }
        }
      },
      "OptionChoice": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string",
            "description": "Send this string (or the id) back in `selections`."
          },
          "priceDelta": {
            "type": "number",
            "description": "What this choice adds to the default configuration, in USD."
          },
          "default": {
            "type": "boolean"
          }
        }
      },
      "OptionGroup": {
        "type": "object",
        "properties": {
          "group": {
            "type": "string",
            "description": "Engine key, e.g. `Paper_Stock`."
          },
          "label": {
            "type": "string",
            "description": "Human label, e.g. `Paper Stock`. Either form works as a `selections` key."
          },
          "type": {
            "type": "string",
            "description": "Engine variable type, e.g. `material_list`."
          },
          "selected": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Id chosen by default."
          },
          "choices": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OptionChoice"
            }
          }
        }
      },
      "SizeSurface": {
        "type": [
          "object",
          "null"
        ],
        "description": "Present when the product has a size. `unit` is inches — this shop measures in inches only.",
        "properties": {
          "unit": {
            "type": "string",
            "const": "inch"
          },
          "min": {
            "type": "object",
            "properties": {
              "w": {
                "type": "number"
              },
              "h": {
                "type": "number"
              }
            }
          },
          "max": {
            "type": "object",
            "properties": {
              "w": {
                "type": "number"
              },
              "h": {
                "type": "number"
              }
            }
          },
          "default": {
            "$ref": "#/components/schemas/Size"
          },
          "is3D": {
            "type": "boolean",
            "description": "True for boxes and other products with depth."
          },
          "presets": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer"
                },
                "name": {
                  "type": "string"
                },
                "w": {
                  "type": "number"
                },
                "h": {
                  "type": "number"
                },
                "d": {
                  "type": "number"
                }
              }
            }
          },
          "custom": {
            "type": "boolean",
            "description": "Whether an arbitrary size within min/max is allowed."
          }
        }
      },
      "Size": {
        "type": "object",
        "description": "Inches. `d` only on three-dimensional products.",
        "required": [
          "w",
          "h"
        ],
        "properties": {
          "w": {
            "type": "number"
          },
          "h": {
            "type": "number"
          },
          "d": {
            "type": "number"
          }
        }
      },
      "ProductDetail": {
        "type": "object",
        "required": [
          "slug",
          "name",
          "basePrice",
          "currency",
          "options"
        ],
        "properties": {
          "slug": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "category": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "image": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "priceFrom": {
            "type": "number"
          },
          "basePrice": {
            "type": "number",
            "description": "Price of the default configuration — what the storefront opens on."
          },
          "currency": {
            "type": "string",
            "const": "usd"
          },
          "oversize": {
            "type": "boolean",
            "description": "Excluded from automatic shipping rates when true."
          },
          "quantities": {
            "type": "array",
            "description": "The quantity ladder. `quantity` is pieces, `price` is the whole run.",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer"
                },
                "quantity": {
                  "type": "integer"
                },
                "price": {
                  "type": "number"
                },
                "pricePerPiece": {
                  "type": "number"
                },
                "default": {
                  "type": "boolean"
                }
              }
            }
          },
          "turnaround": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "integer"
                },
                "name": {
                  "type": "string"
                },
                "productionDays": {
                  "type": [
                    "integer",
                    "null"
                  ]
                },
                "priceDelta": {
                  "type": "number"
                },
                "default": {
                  "type": "boolean"
                }
              }
            }
          },
          "size": {
            "$ref": "#/components/schemas/SizeSurface"
          },
          "options": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OptionGroup"
            }
          },
          "notes": {
            "type": "array",
            "description": "Free-text fields the configurator shows (special instructions).",
            "items": {
              "type": "object",
              "properties": {
                "group": {
                  "type": "string"
                },
                "label": {
                  "type": "string"
                }
              }
            }
          },
          "defaults": {
            "type": "object",
            "description": "Raw engine selections behind `basePrice`.",
            "additionalProperties": true
          }
        }
      },
      "Selections": {
        "type": "object",
        "description": "Chosen option per group, keyed by the group's `group` or `label` from getProduct (case and punctuation are ignored). A value is the choice name or its id. An option the product does not have is a 400, never a silent default.",
        "additionalProperties": {
          "oneOf": [
            {
              "type": "string"
            },
            {
              "type": "number"
            }
          ]
        },
        "examples": [
          {
            "Paper Stock": "16PT Matte",
            "Finishing": "Matte, 2 Sides"
          }
        ]
      },
      "ConfigureInput": {
        "type": "object",
        "required": [
          "slug"
        ],
        "properties": {
          "slug": {
            "type": "string",
            "description": "From searchProducts."
          },
          "quantity": {
            "type": "integer",
            "description": "Pieces in the run. Snapped to the nearest rung of the product's quantity ladder; the snap is reported in `adjustments`."
          },
          "selections": {
            "$ref": "#/components/schemas/Selections"
          },
          "size": {
            "$ref": "#/components/schemas/Size"
          },
          "turnaround": {
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "integer"
              }
            ],
            "description": "Turnaround name or id, e.g. \"Next Day\"."
          }
        }
      },
      "QuoteRequest": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ConfigureInput"
          },
          {
            "type": "object",
            "properties": {
              "packages": {
                "type": "integer",
                "minimum": 1,
                "maximum": 1000,
                "default": 1,
                "description": "How many identical runs of this configuration."
              }
            }
          }
        ]
      },
      "Chosen": {
        "type": "object",
        "description": "What the engine actually settled on, in human words.",
        "properties": {
          "quantity": {
            "type": "integer"
          },
          "turnaround": {
            "type": [
              "string",
              "null"
            ]
          },
          "size": {
            "$ref": "#/components/schemas/Size"
          },
          "options": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          }
        }
      },
      "Quote": {
        "type": "object",
        "required": [
          "slug",
          "unitPrice",
          "shipping",
          "total",
          "currency",
          "itemId",
          "shippingQuoteRequired"
        ],
        "properties": {
          "slug": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "quantity": {
            "type": "integer",
            "description": "Pieces in one run."
          },
          "packages": {
            "type": "integer"
          },
          "unitPrice": {
            "type": "number",
            "description": "Authoritative price of one run."
          },
          "pricePerPiece": {
            "type": "number"
          },
          "subtotal": {
            "type": "number"
          },
          "shipping": {
            "type": [
              "number",
              "null"
            ],
            "description": "null when shipping must be quoted separately."
          },
          "total": {
            "type": [
              "number",
              "null"
            ],
            "description": "null when shipping is not yet known; subtotal remains authoritative."
          },
          "currency": {
            "type": "string",
            "const": "usd"
          },
          "freeShippingOver": {
            "type": "number",
            "description": "Standard-size U.S. ground is free at or above this subtotal; oversized items are excluded."
          },
          "oversize": {
            "type": "boolean"
          },
          "shippingQuoteRequired": {
            "type": "boolean"
          },
          "pickupAvailable": {
            "type": "boolean",
            "const": true
          },
          "shippingPolicy": {
            "type": "string",
            "description": "Show this shipping rule to the operator."
          },
          "chosen": {
            "$ref": "#/components/schemas/Chosen"
          },
          "summary": {
            "type": "string",
            "description": "The configuration in one line, for showing the operator."
          },
          "shipEstimate": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "productionDays": {
                "type": "integer"
              },
              "shipsBy": {
                "type": "string"
              }
            }
          },
          "itemId": {
            "type": "string",
            "description": "Opaque cart id pinning these selections. Pass it back verbatim in createOrder."
          },
          "adjustments": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Anything the engine snapped or corrected. Show these to the operator."
          }
        }
      },
      "OrderLine": {
        "type": "object",
        "description": "Either an `itemId` from getQuote, or a `slug` with the same configuration fields getQuote takes. One of the two is required.",
        "anyOf": [
          {
            "required": [
              "slug"
            ]
          },
          {
            "required": [
              "itemId"
            ]
          }
        ],
        "properties": {
          "slug": {
            "type": "string"
          },
          "itemId": {
            "type": "string",
            "description": "From a quote. Use it and the configuration cannot drift; `slug` then becomes optional and must match it."
          },
          "quantity": {
            "type": "integer",
            "description": "Pieces in the run. Ignored when `itemId` is given."
          },
          "selections": {
            "$ref": "#/components/schemas/Selections"
          },
          "size": {
            "$ref": "#/components/schemas/Size"
          },
          "turnaround": {
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "integer"
              }
            ]
          },
          "qty": {
            "type": "integer",
            "minimum": 1,
            "maximum": 1000,
            "default": 1,
            "description": "How many identical runs of this line. The print run itself is `quantity`, not this."
          }
        }
      },
      "Operator": {
        "type": "object",
        "required": [
          "email"
        ],
        "description": "The human who will approve and pay. The approval link and the receipt go to this address, and it becomes the customer record.",
        "properties": {
          "email": {
            "type": "string",
            "format": "email"
          },
          "name": {
            "type": "string",
            "maxLength": 120
          },
          "phone": {
            "type": "string",
            "maxLength": 40
          }
        }
      },
      "Artwork": {
        "type": "object",
        "required": [
          "url"
        ],
        "description": "Referenced, never uploaded: a public https link the shop can fetch. PDF, PNG, JPG, WEBP, AI, EPS, DXF, DWG, up to 25 MB. No data: URLs.",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "maxLength": 480,
            "pattern": "^https://"
          },
          "name": {
            "type": "string",
            "maxLength": 120
          }
        }
      },
      "CreateOrderRequest": {
        "type": "object",
        "required": [
          "items",
          "operator"
        ],
        "properties": {
          "items": {
            "type": "array",
            "minItems": 1,
            "maxItems": 7,
            "items": {
              "$ref": "#/components/schemas/OrderLine"
            }
          },
          "operator": {
            "$ref": "#/components/schemas/Operator"
          },
          "note": {
            "type": "string",
            "maxLength": 1200,
            "description": "Context for the shop and the operator — deadline, event, who asked."
          },
          "artwork": {
            "type": "array",
            "maxItems": 10,
            "items": {
              "$ref": "#/components/schemas/Artwork"
            }
          },
          "agent": {
            "type": "string",
            "maxLength": 120,
            "description": "Who is ordering, e.g. \"Claude for Acme Design\". Falls back to the X-Agent-Name header."
          }
        }
      },
      "OrderLineOut": {
        "type": "object",
        "properties": {
          "slug": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "options": {
            "type": "string",
            "description": "The configuration in one line."
          },
          "quantity": {
            "type": "integer",
            "description": "Pieces in the run."
          },
          "qty": {
            "type": "integer",
            "description": "How many runs."
          },
          "unitPrice": {
            "type": "number"
          },
          "lineTotal": {
            "type": "number"
          },
          "url": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "OrderDraft": {
        "type": "object",
        "required": [
          "draft_id",
          "approval_url",
          "status",
          "expires_at",
          "items",
          "total",
          "next_step"
        ],
        "properties": {
          "draft_id": {
            "type": "string",
            "description": "The signed draft. It IS the order — long, because it carries the whole cart, and there is no row to lose."
          },
          "approval_url": {
            "type": "string",
            "format": "uri",
            "description": "Give this to the human operator. It is the only way the order gets placed."
          },
          "status": {
            "type": "string",
            "const": "awaiting_approval"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "operator": {
            "$ref": "#/components/schemas/Operator"
          },
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OrderLineOut"
            }
          },
          "subtotal": {
            "type": "number"
          },
          "shipping": {
            "type": "number"
          },
          "total": {
            "type": "number"
          },
          "currency": {
            "type": "string",
            "const": "usd"
          },
          "note": {
            "type": "string"
          },
          "adjustments": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "next_step": {
            "type": "string",
            "description": "One sentence to relay to the operator verbatim."
          }
        }
      },
      "OrderStatus": {
        "type": "object",
        "required": [
          "draft_id",
          "status"
        ],
        "properties": {
          "draft_id": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "awaiting_approval",
              "paid",
              "expired"
            ]
          },
          "approval_url": {
            "type": "string",
            "format": "uri",
            "description": "Present while the draft is still awaiting approval."
          },
          "order_url": {
            "type": "string",
            "format": "uri",
            "description": "Present once paid."
          },
          "paid_at": {
            "type": "string",
            "format": "date-time"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time"
          },
          "operator": {
            "type": "object",
            "properties": {
              "email": {
                "type": "string",
                "format": "email"
              }
            }
          },
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "slug": {
                  "type": "string"
                },
                "name": {
                  "type": "string"
                },
                "options": {
                  "type": "string"
                },
                "qty": {
                  "type": "integer"
                },
                "unitPrice": {
                  "type": "number"
                }
              }
            }
          },
          "subtotal": {
            "type": "number"
          },
          "shipping": {
            "type": "number"
          },
          "total": {
            "type": "number"
          },
          "currency": {
            "type": "string",
            "const": "usd"
          },
          "payment_lookup": {
            "type": "string",
            "const": "unavailable",
            "description": "Payments could not be queried right now — the draft is valid, the paid/unpaid answer is not yet known."
          }
        }
      }
    }
  },
  "x-rate-limits": {
    "scope": "IP",
    "windowMinutes": 1,
    "requestsPerWindow": {
      "searchProducts": 120,
      "getProduct": 120,
      "getQuote": 60,
      "createOrder": 6,
      "checkOrder": 60
    },
    "overLimit": {
      "status": 429,
      "code": "rate_limited"
    }
  },
  "x-mcp": {
    "endpoint": "https://angelsprint.com/api/mcp",
    "transport": "streamable-http (JSON-RPC 2.0 over POST)",
    "maxJsonRpcBatch": 10,
    "note": "Each tool call in a batch spends the same per-IP bucket as its REST twin."
  }
}