{
  "openapi": "3.1.0",
  "info": {
    "title": "Formward Ingest API",
    "version": "1.0.0",
    "description": "The Formward submission endpoint. Accepts form submissions over HTTPS in urlencoded, multipart, or JSON encodings and stores them for the form owner. Classic vs AJAX behaviour is content-negotiated from the Accept / X-Requested-With headers.",
    "contact": { "name": "Formward", "url": "https://formward.eu", "email": "privacy@formward.eu" }
  },
  "servers": [{ "url": "https://forms.formward.eu", "description": "Production ingestion endpoint" }],
  "paths": {
    "/f/{formId}": {
      "parameters": [
        { "name": "formId", "in": "path", "required": true, "description": "The form's ID, from the dashboard.", "schema": { "type": "string" } }
      ],
      "post": {
        "summary": "Submit a form",
        "description": "Submit a form. The body may be application/x-www-form-urlencoded, multipart/form-data (for file uploads), or an application/json object. Control fields starting with an underscore (_redirect, _subject, _replyto, _consent, _gotcha) are read for their effect and stripped before storage.",
        "operationId": "submitForm",
        "requestBody": {
          "required": false,
          "content": {
            "application/x-www-form-urlencoded": { "schema": { "type": "object", "additionalProperties": true } },
            "multipart/form-data": { "schema": { "type": "object", "additionalProperties": true } },
            "application/json": { "schema": { "type": "object", "additionalProperties": true } }
          }
        },
        "responses": {
          "200": {
            "description": "Submission accepted. AJAX requests receive a JSON body; classic requests with no redirect target receive a minimal HTML thanks page.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean", "const": true },
                    "id": { "type": "string" },
                    "files": { "type": "integer" }
                  },
                  "required": ["ok"]
                }
              },
              "text/html": { "schema": { "type": "string" } }
            }
          },
          "302": { "description": "Classic submission accepted; the browser is redirected to the resolved target." },
          "400": { "description": "Malformed request body, too many files, or uploads not supported.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "402": { "description": "Monthly submission quota reached (including any plan overage buffer).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpgradeError" } } } },
          "403": { "description": "Origin not allowed, Turnstile challenge failed, or file uploads disabled / not in plan.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Form not found or inactive.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "413": { "description": "A file, the total upload, or the request body is too large.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "415": { "description": "An uploaded file's MIME type is not allowed.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "description": "Required GDPR consent was not given.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "ok": false, "error": "consent_required" } } } },
          "429": {
            "description": "Rate limit exceeded. Includes a Retry-After header (seconds).",
            "headers": { "Retry-After": { "description": "Seconds to wait before retrying.", "schema": { "type": "integer" } } },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" }, "example": { "ok": false, "error": "rate limit" } } }
          }
        }
      },
      "options": {
        "summary": "CORS preflight",
        "description": "CORS preflight. Returns 204. Access-Control-Allow-Origin reflects the request Origin only when it is permitted by the form's allowed-origins list.",
        "operationId": "submitFormPreflight",
        "responses": {
          "204": {
            "description": "Preflight response.",
            "headers": {
              "Access-Control-Allow-Origin": { "description": "Reflected request Origin when allowed; omitted otherwise.", "schema": { "type": "string" } },
              "Access-Control-Allow-Methods": { "schema": { "type": "string", "example": "POST, OPTIONS" } },
              "Access-Control-Allow-Headers": { "schema": { "type": "string", "example": "content-type" } },
              "Vary": { "schema": { "type": "string", "example": "Origin" } }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean", "const": false },
          "error": { "type": "string" }
        },
        "required": ["ok", "error"]
      },
      "UpgradeError": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean", "const": false },
          "error": { "type": "string" },
          "upgrade": { "type": "boolean", "const": true }
        },
        "required": ["ok", "error"]
      }
    }
  }
}
