{
  "openapi": "3.1.0",
  "info": {
    "title": "fast24.dev order API for web studios and AI agents",
    "version": "1.0.0",
    "description": "Get a fixed price, read the studio's brief questions, and order a website built in 24 hours. The studio pays only after seeing the finished site. Prices are public: see https://fast24.dev/llms.txt. EUR and USD use the same numbers, PLN is 4x. Human-readable reference: https://fast24.dev/studios/api/. Instructions for an AI agent: https://fast24.dev/studios/api/agent.md.",
    "contact": { "url": "https://fast24.dev/studios/" }
  },
  "servers": [{ "url": "https://fast24.dev" }],
  "paths": {
    "/api/studio-order": {
      "get": {
        "operationId": "getQuote",
        "summary": "Price quote without placing an order, or the studio's brief questions with brief=1",
        "description": "With no query parameters, returns a short service description. With brief=1, returns the studio's brief question list (see BriefResponse) instead of a price — pass lang for the client's language and pages to see which optional questions become required for that project size. With any other parameter, returns a price quote.",
        "parameters": [
          { "name": "type", "in": "query", "schema": { "type": "string", "enum": ["landing", "website", "shop", "redesign"] } },
          { "name": "pages", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 500 } },
          { "name": "languages", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 20 } },
          { "name": "payments", "in": "query", "description": "On/off: send 1, true, yes or on to turn it on; leave the parameter out for off", "schema": { "type": "boolean" } },
          { "name": "subscriptions", "in": "query", "description": "On/off: send 1, true, yes or on to turn it on; leave the parameter out for off", "schema": { "type": "boolean" } },
          { "name": "booking", "in": "query", "description": "On/off: send 1, true, yes or on to turn it on; leave the parameter out for off", "schema": { "type": "boolean" } },
          { "name": "accounts", "in": "query", "description": "On/off: send 1, true, yes or on to turn it on; leave the parameter out for off", "schema": { "type": "boolean" } },
          { "name": "extra_roles", "in": "query", "schema": { "type": "integer", "minimum": 0, "maximum": 10 } },
          { "name": "integrations", "in": "query", "schema": { "type": "integer", "minimum": 0, "maximum": 10 } },
          { "name": "currency", "in": "query", "schema": { "type": "string", "enum": ["EUR", "USD", "PLN"] } },
          { "name": "brief", "in": "query", "description": "Set to 1 to get brief_questions instead of a price", "schema": { "type": "boolean" } },
          { "name": "lang", "in": "query", "description": "Language for brief question text", "schema": { "type": "string", "enum": ["en", "ru", "pl", "de", "fr", "es", "pt", "by"] } }
        ],
        "responses": {
          "200": { "description": "A short service description with no query parameters, brief_questions when brief=1 is set, or a price quote for any other parameter", "content": { "application/json": { "schema": { "oneOf": [
            { "$ref": "#/components/schemas/ServiceInfoResponse" }, { "$ref": "#/components/schemas/BriefResponse" }, { "$ref": "#/components/schemas/QuoteResponse" }
          ] } } } }
        }
      },
      "post": {
        "operationId": "placeOrder",
        "summary": "Order a website with brief answers, resupply a brief by order_id, or test with dry_run",
        "description": "A plain request (no order_id) creates an order and returns order_id, price and, if the brief is not yet enough to start, a missing list. Send the same request again with order_id and new brief answers to resupply a brief already on file — new answers are merged with the ones already stored, and the price does not change. Add dry_run:true (or ?dry_run=1) to check a request exactly like a real one, with the same response shape, but create nothing and send no message.",
        "parameters": [
          { "name": "dry_run", "in": "query", "description": "Same as dry_run:true in the body", "schema": { "type": "boolean" } }
        ],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OrderRequest" } } }
        },
        "responses": {
          "200": { "description": "Brief resupply accepted (order_id was given)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OrderResponse" } } } },
          "201": { "description": "Order received", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/OrderResponse" } } } },
          "400": { "description": "Missing contact or bad JSON" },
          "404": { "description": "order_id given for resupply was not found" },
          "413": { "description": "Request body over 20,000 bytes" },
          "429": { "description": "Too many orders from one address in an hour" }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Project": {
        "type": "object",
        "description": "In a response, the module flags (payments, subscriptions, booking, accounts, extra_roles, integrations) are always flat properties of project, as shown here. When sending an order, the same flat properties are accepted directly on project, or nested one level under a \"modules\" object for convenience — both are read the same way.",
        "properties": {
          "type": { "type": "string", "enum": ["landing", "website", "shop", "redesign"], "default": "website" },
          "pages": { "type": "integer", "minimum": 1, "maximum": 500, "default": 1 },
          "languages": { "type": "integer", "minimum": 1, "maximum": 20, "default": 1 },
          "payments": { "type": "boolean", "description": "On/off module. As input: 1, true, yes or on turns it on; leave it out for off", "default": false },
          "subscriptions": { "type": "boolean", "description": "On/off module. As input: 1, true, yes or on turns it on; leave it out for off", "default": false },
          "booking": { "type": "boolean", "description": "On/off module. As input: 1, true, yes or on turns it on; leave it out for off", "default": false },
          "accounts": { "type": "boolean", "description": "On/off module. As input: 1, true, yes or on turns it on; leave it out for off", "default": false },
          "extra_roles": { "type": "integer", "minimum": 0, "maximum": 10, "default": 0 },
          "integrations": { "type": "integer", "minimum": 0, "maximum": 10, "default": 0 },
          "business": { "type": "string", "description": "What the end client's business does" },
          "goal": { "type": "string", "description": "What a visitor should do on the site" },
          "content_ready": { "type": "boolean", "description": "As input: 1, true, yes or on means the texts are ready; leave it out for not ready yet" },
          "notes": { "type": "string" }
        }
      },
      "OrderRequest": {
        "type": "object",
        "description": "Either order_id (resupply) or studio (new order) is required (Се5/О12, 28.09.2026) — not only in words below.",
        "anyOf": [ { "required": ["order_id"] }, { "required": ["studio"] } ],
        "properties": {
          "studio": {
            "type": "object",
            "description": "Required for a new order (not for a resupply, where order_id is given instead): must contain email or telegram, so we can confirm the order",
            "properties": {
              "name": { "type": "string" },
              "website": { "type": "string" },
              "email": { "type": "string" },
              "telegram": { "type": "string" }
            },
            "anyOf": [ { "required": ["email"] }, { "required": ["telegram"] } ]
          },
          "project": { "$ref": "#/components/schemas/Project" },
          "brief": { "type": "object", "description": "Answers keyed by question id from brief_questions, e.g. {\"business\": {\"name\": \"…\", \"description\": \"…\"}, \"email\": \"client@example.com\"}. Also accepted nested under project.brief.", "additionalProperties": true },
          "lang": { "type": "string", "enum": ["en", "ru", "pl", "de", "fr", "es", "pt", "by"], "default": "en", "description": "Language for the missing list in the answer" },
          "currency": { "type": "string", "enum": ["EUR", "USD", "PLN"], "default": "EUR" },
          "order_id": { "type": "string", "description": "Set to resupply an existing order's brief instead of creating a new one; studio and project are then optional" },
          "dry_run": { "type": "boolean", "description": "true checks the request without creating an order or sending any message" }
        }
      },
      "Price": {
        "type": "object",
        "properties": {
          "amount": { "type": "number" },
          "currency": { "type": "string" },
          "total_pages": { "type": "integer" },
          "breakdown": { "type": "object", "additionalProperties": { "type": "number" } }
        }
      },
      "Conditions": {
        "type": "object",
        "description": "Present at the top level of every GET and POST answer, worded exactly as on https://fast24.dev/studios/",
        "properties": {
          "delivery": { "type": "string", "description": "When the site goes live" },
          "payment": { "type": "string", "description": "When and how the studio pays" },
          "edits": { "type": "string", "description": "Source files, how the end client's text is changed, included rounds of changes and free fixes" },
          "terms": { "type": "string", "description": "Link to the contract terms" },
          "studios": { "type": "string", "description": "Link to the studio price list and rules" },
          "agent_guide": { "type": "string", "description": "Link to the full instructions for a program (present outside brief_questions calls)" }
        }
      },
      "ServiceInfoResponse": {
        "type": "object",
        "description": "GET with no query parameters at all",
        "allOf": [ { "$ref": "#/components/schemas/Conditions" } ],
        "properties": {
          "service": { "type": "string" },
          "quote": { "type": "string", "description": "Here, an example GET address as a string — not the Price object returned when a parameter is sent" },
          "order": { "type": "string", "description": "Example POST address and body shape" },
          "brief": { "type": "string", "description": "Example GET address for brief_questions" },
          "spec": { "type": "string", "description": "Link to this document" }
        }
      },
      "QuoteResponse": {
        "type": "object",
        "description": "All 8 fields are present in the live GET answer (Се5, 28.09.2026): quote, project and the 6 Conditions fields below.",
        "properties": {
          "quote": { "$ref": "#/components/schemas/Price" },
          "project": { "$ref": "#/components/schemas/Project" },
          "delivery": { "type": "string", "description": "When the site goes live" },
          "payment": { "type": "string", "description": "When and how the studio pays" },
          "edits": { "type": "string", "description": "Source files, how the end client's text is changed, included rounds of changes and free fixes" },
          "terms": { "type": "string", "description": "Link to the contract terms" },
          "studios": { "type": "string", "description": "Link to the studio price list and rules" },
          "agent_guide": { "type": "string", "description": "Link to the full instructions for a program (present outside brief_questions calls)" }
        }
      },
      "MissingItem": {
        "type": "object",
        "description": "One brief question still needed to reach the minimum to start the site",
        "properties": {
          "id": { "type": "string", "description": "Question id, or \"contact\" for the email/whatsapp/telegram group" },
          "need_one_of": { "type": "array", "items": { "type": "string" }, "description": "Present only for the contact group: any one of these ids answers it" },
          "text": { "type": "string" }
        }
      },
      "OrderResponse": {
        "type": "object",
        "allOf": [ { "$ref": "#/components/schemas/Conditions" } ],
        "properties": {
          "order_id": { "type": "string" },
          "status": { "type": "string", "enum": ["received", "brief_incomplete", "brief_complete"] },
          "dry_run": { "type": "boolean", "description": "Present and true only when dry_run was set in the request" },
          "price": { "$ref": "#/components/schemas/Price" },
          "project": { "$ref": "#/components/schemas/Project" },
          "missing": { "type": "array", "items": { "$ref": "#/components/schemas/MissingItem" }, "description": "Empty once the brief has everything required to start" },
          "next": { "type": "string" }
        }
      },
      "BriefField": {
        "type": "object",
        "description": "One sub-field of a \"fields\" question; the answer for such a question is an object keyed by these field keys",
        "properties": {
          "key": { "type": "string" },
          "type": { "type": "string", "enum": ["text", "choice", "multi_choice", "checkbox"] },
          "label": { "type": "string" },
          "options": { "type": "array", "items": { "type": "string" } }
        }
      },
      "BriefQuestion": {
        "type": "object",
        "properties": {
          "order": { "type": "integer", "description": "Ask in this order, most important first" },
          "id": { "type": "string" },
          "type": { "type": "string", "enum": ["text", "choice", "multi_choice", "checkbox", "fields"] },
          "text": { "type": "string" },
          "translated": { "type": "boolean", "description": "false means text is English because this language has no translation yet" },
          "required_for_start": { "type": "boolean" },
          "need_one_of": { "type": "array", "items": { "type": "string" }, "description": "Present only on email/whatsapp/telegram: only one of the group is needed" },
          "hint": { "type": "string" },
          "options": { "type": "array", "items": { "type": "string" } },
          "fields": { "type": "array", "items": { "$ref": "#/components/schemas/BriefField" } }
        }
      },
      "BriefResponse": {
        "type": "object",
        "allOf": [ { "$ref": "#/components/schemas/Conditions" } ],
        "properties": {
          "brief_questions": { "type": "array", "items": { "$ref": "#/components/schemas/BriefQuestion" } },
          "lang": { "type": "string" },
          "note": { "type": "string" },
          "how_to_answer": { "type": "string" }
        }
      }
    }
  }
}
