{
 "openapi": "3.1.0",
 "info": {
  "title": "Custom AI Systems: request a Blueprint",
  "version": "1.1.0",
  "summary": "Request a free one-page Blueprint for a company, for its owner and with the owner's permission.",
  "description": "A person reads every request. A name, a company, a working email and the owner's answers to ten questions are required; an answer in the owner's own words is kept, and only a missing one is refused. The reply's route says what happens next: fit (book a walkthrough call), review (the owner replies by email within one business day) or later (a build does not fit yet). No automatic email is sent. Plain-language instructions: https://contractoros.tech/llms.txt",
  "contact": {
   "name": "Custom AI Systems",
   "email": "info@contractoros.tech",
   "url": "https://contractoros.tech/"
  }
 },
 "servers": [
  {
   "url": "https://contractoros.tech"
  }
 ],
 "paths": {
  "/api/blueprint": {
   "post": {
    "operationId": "requestBlueprint",
    "summary": "Request a Blueprint",
    "description": "Send only what the owner agreed to share. A retry with the same idempotency key and the same answers returns the first reference and stores nothing twice; changed answers under the same key are stored as a new request with a new reference. Put each answer in its own field once; do not repeat one text across gain, loss and tried. Send Accept: application/json to get JSON for every reply, and a User-Agent header that names your tool: the host refuses a few generic library signatures (Python's default urllib among them) with a 403 at the edge.",
    "parameters": [
     {
      "name": "Idempotency-Key",
      "in": "header",
      "required": false,
      "description": "Optional retry key, the same as the idempotency_key field.",
      "schema": {
       "type": "string",
       "pattern": "^[A-Za-z0-9._:-]{8,100}$"
      }
     }
    ],
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/BlueprintRequest"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Stored. Give the person the reference and the next step the route names.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Received"
        }
       }
      }
     },
     "400": {
      "description": "The body could not be read. Send JSON or a form post.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Refusal"
        }
       }
      }
     },
     "403": {
      "description": "The request came with an Origin header from another site. Send it without one, from contractoros.tech, or by email.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Refusal"
        }
       }
      }
     },
     "405": {
      "description": "Only POST (and OPTIONS) are accepted.",
      "headers": {
       "Allow": {
        "schema": {
         "type": "string"
        }
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Refusal"
        }
       }
      }
     },
     "413": {
      "description": "The body is larger than 32 KB. Shorten the answers, or email it.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Refusal"
        }
       }
      }
     },
     "422": {
      "description": "A name, a company, a working email or one of the ten answers is missing, or the email does not look complete (then missing is empty and message says so). choices lists the form's answers for each missing one. Ask the person, then send again.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Refusal"
        }
       }
      }
     },
     "429": {
      "description": "Too many requests from one connection. Wait the seconds in Retry-After, then send again.",
      "headers": {
       "Retry-After": {
        "schema": {
         "type": "integer"
        }
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Refusal"
        }
       }
      }
     },
     "503": {
      "description": "Not stored. Send it again after Retry-After, or email it to info@contractoros.tech.",
      "headers": {
       "Retry-After": {
        "schema": {
         "type": "integer"
        }
       }
      },
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Refusal"
        }
       }
      }
     }
    }
   }
  },
  "/api/book": {
   "get": {
    "operationId": "walkthroughTimes",
    "summary": "The open walkthrough call times",
    "description": "For a request whose route is fit. Times are Pacific; no email is sent to anyone.",
    "parameters": [
     {
      "name": "r",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string",
       "pattern": "^CAS-[2-9A-HJKMNP-Z]{6}$"
      }
     },
     {
      "name": "k",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string",
       "pattern": "^[0-9a-f]{32}$"
      }
     }
    ],
    "responses": {
     "200": {
      "description": "The open times, or the call already booked.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Openings"
        }
       }
      }
     },
     "403": {
      "description": "This request is answered by email.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Refusal"
        }
       }
      }
     },
     "404": {
      "description": "No booking matches the reference and key.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Refusal"
        }
       }
      }
     },
     "410": {
      "description": "The link is older than 30 days.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Refusal"
        }
       }
      }
     }
    }
   },
   "post": {
    "operationId": "bookWalkthrough",
    "summary": "Book one walkthrough call time",
    "description": "Only with the owner's permission. The owner gets a phone call at that time; no email is sent to anyone.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "$ref": "#/components/schemas/Booking"
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Booked."
     },
     "409": {
      "description": "The time was just taken (fresh times come back as slots), or the request already has a call.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Refusal"
        }
       }
      }
     },
     "422": {
      "description": "The phone is missing.",
      "content": {
       "application/json": {
        "schema": {
         "$ref": "#/components/schemas/Refusal"
        }
       }
      }
     }
    }
   }
  }
 },
 "components": {
  "schemas": {
   "BlueprintRequest": {
    "type": "object",
    "required": [
     "name",
     "company",
     "email",
     "site",
     "role",
     "decider",
     "kind",
     "size",
     "revenue",
     "goal",
     "tools",
     "when",
     "ready"
    ],
    "properties": {
     "name": {
      "type": "string",
      "description": "The person's name."
     },
     "company": {
      "type": "string",
      "description": "The company's name."
     },
     "email": {
      "type": "string",
      "format": "email",
      "description": "Where the reply goes."
     },
     "phone": {
      "type": "string",
      "description": "Optional. The booking asks for the number the owner wants the call on."
     },
     "site": {
      "type": "string",
      "format": "uri",
      "description": "The company's website, or its Google listing. The intake checks that it answers, as it checks that the email's domain can receive mail; neither check refuses a request."
     },
     "role": {
      "type": "string",
      "description": "The sender's role. One of: Owner; Partner or officer; Manager; Other. Other words are kept as written.",
      "examples": [
       "Owner",
       "Partner or officer",
       "Manager",
       "Other"
      ]
     },
     "decider": {
      "type": "string",
      "description": "Who decides on a project like this. One of: I do; I and a partner; Someone else.",
      "examples": [
       "I do",
       "I and a partner",
       "Someone else"
      ]
     },
     "kind": {
      "type": "string",
      "description": "Kind of business. One of: Property management; Construction or trades; Insurance, tax or accounting office; Supplier or distributor; Something else. Other words are kept as written.",
      "examples": [
       "Property management",
       "Construction or trades",
       "Insurance, tax or accounting office",
       "Supplier or distributor",
       "Something else"
      ]
     },
     "size": {
      "type": [
       "integer",
       "string"
      ],
      "description": "Team size: a head count such as 12, or one of 1 to 4; 5 to 20; 21 to 50; 51 to 100; More than 100.",
      "examples": [
       12,
       "1 to 4",
       "5 to 20",
       "21 to 50",
       "51 to 100",
       "More than 100"
      ]
     },
     "revenue": {
      "type": "string",
      "description": "The company's yearly sales. One of: Under 500 thousand; 500 thousand to 2 million; 2 to 5 million; 5 to 20 million; More than 20 million; Prefer not to say. An amount in any words is placed in its band.",
      "examples": [
       "Under 500 thousand",
       "500 thousand to 2 million",
       "2 to 5 million",
       "5 to 20 million",
       "More than 20 million",
       "Prefer not to say"
      ]
     },
     "goal": {
      "type": "string",
      "description": "The result wanted most. One of: More business from the leads we already get; My people out of the inbox; Nothing falls through the cracks; A company that runs when I'm not there.",
      "examples": [
       "More business from the leads we already get",
       "My people out of the inbox",
       "Nothing falls through the cracks",
       "A company that runs when I'm not there"
      ]
     },
     "tools": {
      "type": [
       "array",
       "string"
      ],
      "items": {
       "type": "string"
      },
      "description": "What the company runs on, as a list or one comma-separated string: Gmail or Google Workspace; Outlook or Microsoft 365; QuickBooks; A CRM; Something else. A product name is kept as written.",
      "examples": [
       [
        "Gmail or Google Workspace",
        "QuickBooks"
       ]
      ]
     },
     "when": {
      "type": "string",
      "description": "When they want it running. One of: This month; This quarter; Just looking.",
      "examples": [
       "This month",
       "This quarter",
       "Just looking"
      ]
     },
     "ready": {
      "type": [
       "string",
       "boolean"
      ],
      "description": "Whether a five-figure build is within reach this year. One of: Yes; Not this year; Not sure. true and false work too.",
      "examples": [
       "Yes",
       "Not this year",
       "Not sure",
       true
      ]
     },
     "heard": {
      "type": "string",
      "description": "How they heard about the practice. One of: Google; An AI assistant; A referral; Saw it online; Something else. An assistant may name itself, such as ChatGPT.",
      "examples": [
       "Google",
       "An AI assistant",
       "A referral",
       "Saw it online",
       "Something else"
      ]
     },
     "systems": {
      "type": [
       "array",
       "string"
      ],
      "items": {
       "type": "string"
      },
      "description": "Names from the service menu (https://contractoros.tech/menu), as a list or one comma-separated string.",
      "examples": [
       [
        "Company brain",
        "Paperwork agent"
       ]
      ]
     },
     "gain": {
      "type": "string",
      "description": "What the company would gain. Plain text, up to 2000 characters."
     },
     "loss": {
      "type": "string",
      "description": "What slips through today. Plain text, up to 2000 characters."
     },
     "tried": {
      "type": "string",
      "description": "What they have tried, and what it cost them. Plain text, up to 2000 characters."
     },
     "needs": {
      "type": "string",
      "description": "Anything else, in the owner's words. Plain text, up to 2000 characters."
     },
     "via": {
      "type": "string",
      "description": "Who is sending, such as agent:chatgpt."
     },
     "on_behalf_of": {
      "type": "string",
      "description": "The person the assistant acts for, such as Dana Ortiz, owner."
     },
     "consent": {
      "type": "boolean",
      "description": "True when that person agreed to be contacted."
     },
     "idempotency_key": {
      "type": "string",
      "pattern": "^[A-Za-z0-9._:-]{8,100}$",
      "description": "Optional retry key."
     }
    }
   },
   "Received": {
    "type": "object",
    "required": [
     "ok",
     "reference",
     "reply_to",
     "route",
     "next"
    ],
    "properties": {
     "ok": {
      "const": true
     },
     "reference": {
      "type": "string",
      "pattern": "^CAS-[2-9A-HJKMNP-Z]{6}$"
     },
     "reply_to": {
      "type": "string",
      "format": "email"
     },
     "next": {
      "type": "string"
     },
     "route": {
      "enum": [
       "fit",
       "review",
       "later"
      ],
      "description": "fit: book a walkthrough call. review: the owner replies by email within one business day. later: a build does not fit yet."
     },
     "book": {
      "type": "object",
      "description": "Present when route is fit.",
      "properties": {
       "reference": {
        "type": "string"
       },
       "key": {
        "type": "string",
        "pattern": "^[0-9a-f]{32}$"
       },
       "url": {
        "type": "string",
        "format": "uri",
        "description": "Give this to the owner: the page shows the open times."
       },
       "slots": {
        "type": "string",
        "format": "uri",
        "description": "The open times as JSON (GET /api/book)."
       }
      }
     }
    }
   },
   "Refusal": {
    "type": "object",
    "required": [
     "ok",
     "message",
     "alternative"
    ],
    "properties": {
     "ok": {
      "const": false
     },
     "reason": {
      "type": "string"
     },
     "field": {
      "type": "string"
     },
     "missing": {
      "type": "array",
      "items": {
       "type": "string"
      }
     },
     "message": {
      "type": "string"
     },
     "alternative": {
      "type": "string",
      "format": "email"
     },
     "retry_after": {
      "type": "integer"
     },
     "choices": {
      "type": "object",
      "additionalProperties": {
       "type": "array",
       "items": {
        "type": "string"
       }
      },
      "description": "For each missing answer, the answers the form offers."
     }
    }
   },
   "Slot": {
    "type": "object",
    "required": [
     "start",
     "end",
     "day",
     "time"
    ],
    "properties": {
     "start": {
      "type": "string",
      "format": "date-time"
     },
     "end": {
      "type": "string",
      "format": "date-time"
     },
     "date": {
      "type": "string"
     },
     "day": {
      "type": "string",
      "examples": [
       "Tuesday, October 20"
      ]
     },
     "time": {
      "type": "string",
      "examples": [
       "9:00 AM"
      ]
     }
    }
   },
   "Openings": {
    "type": "object",
    "required": [
     "ok",
     "reference",
     "slots"
    ],
    "properties": {
     "ok": {
      "const": true
     },
     "reference": {
      "type": "string"
     },
     "minutes": {
      "type": "integer"
     },
     "zone": {
      "const": "Pacific"
     },
     "calendar": {
      "enum": [
       "ok",
       "stale"
      ]
     },
     "booked": {
      "oneOf": [
       {
        "type": "null"
       },
       {
        "$ref": "#/components/schemas/Slot"
       }
      ]
     },
     "slots": {
      "type": "array",
      "items": {
       "$ref": "#/components/schemas/Slot"
      }
     },
     "next": {
      "type": "string"
     }
    }
   },
   "Booking": {
    "type": "object",
    "required": [
     "r",
     "k",
     "start",
     "phone"
    ],
    "properties": {
     "r": {
      "type": "string",
      "description": "The reference."
     },
     "k": {
      "type": "string",
      "description": "The key from book."
     },
     "start": {
      "type": "string",
      "format": "date-time",
      "description": "One start from the open times."
     },
     "phone": {
      "type": "string",
      "description": "The number the owner wants the call on."
     },
     "note": {
      "type": "string",
      "description": "Optional, up to 300 characters."
     }
    }
   }
  }
 }
}
