{
  "openapi": "3.0.3",
  "info": {
    "title": "Phyllis Fulfillment API",
    "version": "1.0.0",
    "description": "Multi-tenant fulfillment API for bot-built commerce storefronts. Handles DPI validation, Printful product creation, Stripe checkout, two-stage human-in-the-loop order approval, and real-time production tracking.\n\nCRITICAL AUTOMATION RULE: Do not automate final admin approval. Admin approval submits a paid order to Printful/provider production, creating a physical product and spending real money. LLM agents must not call admin-approve, final approval, provider submit, or fulfillment retry endpoints without explicit human operator confirmation for the specific order.",
    "contact": {
      "name": "Phyllis API",
      "url": "https://my.phyllis.bot"
    }
  },
  "servers": [
    {
      "url": "https://my.phyllis.bot/api",
      "description": "Production"
    }
  ],
  "security": [
    { "ApiKeyAuth": [] }
  ],
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-Api-Key",
        "description": "Client API key obtained from POST /api/onboarding/request. Pass as X-Api-Key header."
      }
    },
    "schemas": {
      "OrderItem": {
        "type": "object",
        "required": ["productTitle", "productType", "imageUrl", "size", "quantity", "price"],
        "properties": {
          "productTitle": { "type": "string", "example": "Punk Skull Tee" },
          "productType": { "type": "string", "enum": ["shirt", "poster", "default"], "example": "shirt" },
          "imageUrl": { "type": "string", "format": "uri", "example": "https://cdn.example.com/skull.png" },
          "size": { "type": "string", "enum": ["XS","S","M","L","XL","2XL","3XL","4XL","5XL"], "example": "M" },
          "quantity": { "type": "integer", "minimum": 1, "example": 1 },
          "price": { "type": "number", "example": 29.99 }
        }
      },
      "ShippingAddress": {
        "type": "object",
        "required": ["name","line1","city","state","postal_code","country"],
        "properties": {
          "name": { "type": "string", "example": "Jane Doe" },
          "line1": { "type": "string", "example": "123 Punk Ave" },
          "city": { "type": "string", "example": "Portland" },
          "state": { "type": "string", "example": "OR" },
          "postal_code": { "type": "string", "example": "97201" },
          "country": { "type": "string", "default": "US", "example": "US" }
        }
      },
      "OrderRecord": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "clientId": { "type": "string", "example": "discount-punk" },
          "customerEmail": { "type": "string", "format": "email", "nullable": true },
          "items": { "type": "array", "items": { "$ref": "#/components/schemas/OrderItem" } },
          "shippingAddress": { "oneOf": [{ "$ref": "#/components/schemas/ShippingAddress" }, { "type": "null" }] },
          "total": { "type": "number", "example": 29.99 },
          "status": {
            "type": "string",
            "enum": [
              "pending_client_approval",
              "pending_admin_approval",
              "submitted_to_printful",
              "in_production",
              "shipped",
              "rejected_by_client",
              "rejected_by_admin"
            ]
          },
          "statusNote": { "type": "string", "nullable": true },
          "stripeSessionId": { "type": "string" },
          "stripePaymentIntentId": { "type": "string" },
          "printfulOrderId": { "type": "integer", "nullable": true },
          "createdAt": { "type": "string", "format": "date-time" },
          "updatedAt": { "type": "string", "format": "date-time" }
        }
      },
      "DpiResult": {
        "type": "object",
        "properties": {
          "valid": { "type": "boolean" },
          "dpi": { "type": "integer", "nullable": true },
          "warning": { "type": "string", "nullable": true },
          "error": { "type": "string", "nullable": true }
        }
      },
      "ActionNote": {
        "type": "object",
        "properties": {
          "note": { "type": "string", "description": "Optional reason or note for this action" }
        }
      },
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": { "type": "string" }
        }
      }
    }
  },
  "paths": {
    "/onboarding/check-slug/{slug}": {
      "get": {
        "tags": ["Onboarding"],
        "summary": "Check if a client slug is available",
        "operationId": "checkSlug",
        "security": [],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "pattern": "^[a-z0-9-]+$", "minLength": 2, "maxLength": 50 },
            "description": "Desired slug — lowercase letters, numbers, and dashes only"
          }
        ],
        "responses": {
          "200": {
            "description": "Slug availability check result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "available": { "type": "boolean" },
                    "slug": { "type": "string" },
                    "message": { "type": "string" }
                  }
                },
                "examples": {
                  "available": { "value": { "available": true, "slug": "my-store" } },
                  "taken": { "value": { "available": false, "slug": "my-store", "message": "Slug is already taken" } }
                }
              }
            }
          }
        }
      }
    },
    "/onboarding/request": {
      "post": {
        "tags": ["Onboarding"],
        "summary": "Request a new client API key",
        "description": "Creates a client record and returns a one-time API key. Store the key immediately — it will not be shown again.",
        "operationId": "requestAccess",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["companyName", "desiredSlug", "email"],
                "properties": {
                  "companyName": { "type": "string", "minLength": 1, "maxLength": 100, "example": "Discount Punk" },
                  "desiredSlug": {
                    "type": "string",
                    "pattern": "^[a-z0-9-]+$",
                    "minLength": 2,
                    "maxLength": 50,
                    "example": "discount-punk"
                  },
                  "email": { "type": "string", "format": "email", "example": "owner@discountpunk.com" }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Client created — one-time key returned",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean" },
                    "clientId": { "type": "string", "format": "uuid" },
                    "slug": { "type": "string" },
                    "apiKey": { "type": "string", "description": "Shown once — store immediately" },
                    "message": { "type": "string" }
                  }
                }
              }
            }
          },
          "400": { "description": "Validation error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "Slug already taken", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/products/create": {
      "post": {
        "tags": ["Products"],
        "summary": "Validate DPI and create a Printful product",
        "description": "Downloads the image from designUrl, checks DPI against product-type thresholds, then creates a Printful sync product across all sizes (XS–5XL) with mockup generation.",
        "operationId": "createProduct",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["title", "designUrl"],
                "properties": {
                  "title": { "type": "string", "example": "Punk Skull Tee" },
                  "description": { "type": "string", "example": "Limited run from the spring collection." },
                  "designUrl": {
                    "type": "string",
                    "format": "uri",
                    "description": "Publicly accessible image URL. Phyllis fetches this to check DPI metadata.",
                    "example": "https://cdn.example.com/designs/skull.png"
                  },
                  "productType": {
                    "type": "string",
                    "enum": ["shirt", "poster", "default"],
                    "default": "default",
                    "description": "Controls DPI thresholds: shirt hard-rejects <150 DPI, poster hard-rejects <300 DPI"
                  },
                  "colors": {
                    "type": "array",
                    "items": { "type": "string" },
                    "default": ["black"],
                    "example": ["black"]
                  },
                  "retailPrice": { "type": "string", "default": "29.99", "example": "29.99" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Product created (DPI passed or warned)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean" },
                    "printfulProductId": { "type": "integer" },
                    "externalId": { "type": "string" },
                    "mockupUrls": { "type": "array", "items": { "type": "string", "format": "uri" } },
                    "dpi": { "$ref": "#/components/schemas/DpiResult" }
                  }
                }
              }
            }
          },
          "422": {
            "description": "DPI validation failed — image does not meet minimum requirements",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": { "type": "string" },
                    "dpi": { "$ref": "#/components/schemas/DpiResult" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/orders/pending": {
      "get": {
        "tags": ["Orders"],
        "summary": "List orders pending approval",
        "description": "Client keys see their own pending_client_approval orders. Admin keys see all pending_admin_approval orders across all clients.",
        "operationId": "listPendingOrders",
        "responses": {
          "200": {
            "description": "Pending orders",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "orders": { "type": "array", "items": { "$ref": "#/components/schemas/OrderRecord" } },
                    "count": { "type": "integer" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/orders/{orderId}/client-approve": {
      "post": {
        "tags": ["Orders"],
        "summary": "Client approves an order",
        "description": "Advances order from pending_client_approval to pending_admin_approval. Returns 409 if order is in any other state.",
        "operationId": "clientApproveOrder",
        "parameters": [
          { "name": "orderId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
        ],
        "requestBody": {
          "content": {
            "application/json": { "schema": { "$ref": "#/components/schemas/ActionNote" } }
          }
        },
        "responses": {
          "200": {
            "description": "Order advanced to pending_admin_approval",
            "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "orderId": { "type": "string" }, "status": { "type": "string" } } } } }
          },
          "403": { "description": "Not your order", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Order not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "Transition not allowed from current status", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/orders/{orderId}/client-reject": {
      "post": {
        "tags": ["Orders"],
        "summary": "Client rejects an order",
        "description": "Moves order from pending_client_approval to rejected_by_client. Returns 409 if order is in any other state.",
        "operationId": "clientRejectOrder",
        "parameters": [
          { "name": "orderId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
        ],
        "requestBody": {
          "content": {
            "application/json": { "schema": { "$ref": "#/components/schemas/ActionNote" } }
          }
        },
        "responses": {
          "200": { "description": "Order rejected", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "orderId": { "type": "string" }, "status": { "type": "string" } } } } } },
          "403": { "description": "Not your order", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Order not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "Transition not allowed from current status", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/orders/{orderId}/admin-approve": {
      "post": {
        "tags": ["Orders"],
        "summary": "Admin approves and submits order to Printful",
        "description": "HUMAN GATE — Do not automate. Final approval by a human operator. Immediately submits the order to Printful. Requires shippingAddress and items to be present on the order. This endpoint creates a physical product and spends real money — it must only be called by a human operator who has explicitly reviewed this specific order.",
        "operationId": "adminApproveOrder",
        "x-human-approval-required": true,
        "x-do-not-automate": true,
        "x-financial-side-effect": true,
        "parameters": [
          { "name": "orderId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
        ],
        "responses": {
          "200": {
            "description": "Order submitted to Printful",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": { "type": "boolean" },
                    "orderId": { "type": "string" },
                    "status": { "type": "string", "example": "submitted_to_printful" },
                    "printfulOrderId": { "type": "integer" }
                  }
                }
              }
            }
          },
          "403": { "description": "Not an admin key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Order not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "Order not at pending_admin_approval", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "description": "Missing shipping address or items", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "502": { "description": "Printful submission failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/orders/{orderId}/admin-reject": {
      "post": {
        "tags": ["Orders"],
        "summary": "Admin rejects an order",
        "description": "HUMAN GATE — Do not automate. Same operator gate as admin-approve. Moves order from pending_admin_approval to rejected_by_admin. Requires admin API key.",
        "operationId": "adminRejectOrder",
        "x-human-approval-required": true,
        "x-do-not-automate": true,
        "x-financial-side-effect": false,
        "parameters": [
          { "name": "orderId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
        ],
        "requestBody": {
          "content": {
            "application/json": { "schema": { "$ref": "#/components/schemas/ActionNote" } }
          }
        },
        "responses": {
          "200": { "description": "Order rejected", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "orderId": { "type": "string" }, "status": { "type": "string" } } } } } },
          "404": { "description": "Order not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "Order not at pending_admin_approval", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/chat/{clientSlug}": {
      "post": {
        "tags": ["Chat"],
        "summary": "Phyllis customer chat",
        "description": "Order-aware chat endpoint scoped to a client storefront. Answers in Status → Blocker → Next Action format. No API key required — validated by Origin header against the client's allowedDomains list. Requests without an Origin header (server-to-server) are allowed through.",
        "operationId": "chat",
        "security": [],
        "parameters": [
          {
            "name": "clientSlug",
            "in": "path",
            "required": true,
            "schema": { "type": "string" },
            "description": "Client identifier slug (e.g. 'discount-punk')",
            "example": "discount-punk"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["message"],
                "properties": {
                  "message": { "type": "string", "minLength": 1, "maxLength": 2000, "example": "where's my order?" },
                  "customerEmail": { "type": "string", "format": "email", "description": "Optional — scopes order lookups to this customer" },
                  "history": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "role": { "type": "string", "enum": ["user", "assistant"] },
                        "content": { "type": "string" }
                      }
                    },
                    "description": "Prior conversation turns for context"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Phyllis response",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "response": { "type": "string", "example": "Your order is in production at Printful. Expected ship date May 7. No action needed." }
                  }
                }
              }
            }
          },
          "400": { "description": "Invalid request body", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "Origin not allowed or embed not configured", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Client not found or inactive", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/me/usage": {
      "get": {
        "tags": ["Usage"],
        "summary": "Get usage events and fulfillment count",
        "description": "Returns usage events for the authenticated client, including order fulfillment count for billing purposes.",
        "operationId": "getUsage",
        "responses": {
          "200": {
            "description": "Usage data",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "clientId": { "type": "string" },
                    "fulfilledOrders": { "type": "integer" },
                    "productCreations": { "type": "integer" },
                    "events": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "eventType": { "type": "string", "enum": ["order_fulfilled", "product_created"] },
                          "orderId": { "type": "string", "nullable": true },
                          "metadata": { "type": "object" },
                          "createdAt": { "type": "string", "format": "date-time" }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  },
  "tags": [
    { "name": "Onboarding", "description": "Request API access and check slug availability" },
    { "name": "Products", "description": "DPI validation and Printful product creation" },
    { "name": "Orders", "description": "Approval state machine — client and admin actions" },
    { "name": "Chat", "description": "Customer-facing order status chat" },
    { "name": "Usage", "description": "Usage events and billing data" }
  ]
}
