{
  "openapi": "3.1.0",
  "info": {
    "title": "Formward API",
    "version": "1.0.0",
    "description": "The complete Formward HTTP API. It has two surfaces:\n\n1. The public **ingestion** endpoint `POST /f/{formId}` on `https://forms.formward.eu`, which accepts form submissions over HTTPS in urlencoded, multipart, or JSON encodings. No API key is required to submit; access is gated by the form's origin allowlist, optional Cloudflare Turnstile, rate limits, and quota. Classic vs AJAX behaviour is content-negotiated from the `Accept` / `X-Requested-With` headers.\n\n2. The authenticated **REST API** under `https://app.formward.eu/api/v1`, for reading and managing your forms and submissions. It requires the Professional plan or above and is authenticated with an API key (`Authorization: Bearer <key>` or `X-Api-Key: <key>`), scoped to the organizations the key's user belongs to. Each key is rate limited per fixed window; responses carry `X-RateLimit-*` headers and 429s include `Retry-After`.\n\nScopes: each key carries a least-privilege scope set from { forms:read, forms:write, submissions:read, triage:write }. Read endpoints require the matching :read scope; write endpoints require the listed scope. A key missing the required scope gets 403 with error code `insufficient_scope` and a `required` field. There is intentionally NO REST endpoint to create submissions: inbound submissions must go through the public ingestion endpoint so spam scoring and quota enforcement always apply.",
    "contact": { "name": "Formward", "url": "https://formward.eu", "email": "team@formward.eu" }
  },
  "servers": [
    { "url": "https://forms.formward.eu", "description": "Public ingestion endpoint (form submissions, no API key)" },
    { "url": "https://app.formward.eu/api/v1", "description": "Authenticated REST API (forms & submissions, API key required)" }
  ],
  "tags": [
    { "name": "Ingestion", "description": "Public submission endpoint. Server: https://forms.formward.eu" },
    { "name": "Forms", "description": "REST API. Server: https://app.formward.eu/api/v1" },
    { "name": "Submissions", "description": "REST API. Server: https://app.formward.eu/api/v1" }
  ],
  "paths": {
    "/f/{formId}": {
      "servers": [{ "url": "https://forms.formward.eu", "description": "Public ingestion endpoint" }],
      "parameters": [
        { "name": "formId", "in": "path", "required": true, "description": "The form's ID, from the dashboard.", "schema": { "type": "string" } }
      ],
      "post": {
        "tags": ["Ingestion"],
        "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 (an empty body is treated as no fields; a JSON body that is not an object is rejected with 400). Control fields starting with an underscore (_redirect, _subject, _replyto/_reply_to, _consent, _gotcha) are read for their effect and stripped before storage. The honeypot field _gotcha and the Turnstile token field cf-turnstile-response are read and never persisted. No API key is required.",
        "operationId": "submitForm",
        "security": [],
        "requestBody": {
          "required": false,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "additionalProperties": true,
                "description": "Arbitrary form fields plus optional underscore-prefixed control fields.",
                "properties": {
                  "_gotcha": { "type": "string", "description": "Honeypot. Any non-empty value silently marks the submission as spam and returns a success-shaped response." },
                  "_redirect": { "type": "string", "description": "Classic-mode redirect target. Relative path, or an absolute URL whose origin is allowed; otherwise ignored." },
                  "_subject": { "type": "string", "description": "Notification email subject line." },
                  "_replyto": { "type": "string", "format": "email", "description": "Sets Reply-To on the notification email." },
                  "_consent": { "type": "string", "description": "GDPR consent checkbox. Truthy: on/true/1/yes. Required forms 422 without it." },
                  "cf-turnstile-response": { "type": "string", "description": "Cloudflare Turnstile token, when the form has Turnstile enabled. Stripped before storage." }
                }
              }
            },
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "additionalProperties": true,
                "description": "Same fields as urlencoded, plus file parts. Requires the form's uploads_enabled flag and the Professional plan. Defaults: max 5 files, 10 MiB per file, 25 MiB total, MIME allowlist (images, PDF, text/CSV, Office, OpenDocument)."
              }
            },
            "application/json": {
              "schema": { "type": "object", "additionalProperties": true, "description": "A JSON object of form fields. Arrays, strings, numbers, and null are rejected with 400." }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Submission accepted. AJAX requests (Accept: application/json or X-Requested-With) 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", "description": "The stored submission ID (absent for honeypot/blocked spam, which still returns ok: true)." },
                    "files": { "type": "integer", "description": "Number of attachments stored." }
                  },
                  "required": ["ok"]
                },
                "example": { "ok": true, "id": "sub_123", "files": 0 }
              },
              "text/html": { "schema": { "type": "string" } }
            }
          },
          "302": { "description": "Classic submission accepted; the browser is redirected to the resolved target (the _redirect field or the form's configured redirect URL)." },
          "400": { "description": "Malformed request body (a JSON body that is not an object), too many files, or a multipart upload sent where uploads are unavailable.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/IngestError" } } } },
          "402": { "description": "Monthly submission quota reached (including any plan overage buffer).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpgradeError" }, "example": { "ok": false, "error": "submission limit reached", "upgrade": true } } } },
          "403": { "description": "Origin not in the form's allowed-origins list, Turnstile challenge failed, or file uploads disabled / not in plan.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/IngestError" } } } },
          "404": { "description": "Form not found or inactive.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/IngestError" }, "example": { "ok": false, "error": "form not found" } } } },
          "413": { "description": "A file, the total upload, or the (non-multipart) request body is too large.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/IngestError" } } } },
          "415": { "description": "An uploaded file's MIME type is not in the allowed list.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/IngestError" }, "example": { "ok": false, "error": "file type not allowed: application/zip" } } } },
          "422": { "description": "Required GDPR consent was not given on a form that requires it.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/IngestError" }, "example": { "ok": false, "error": "consent_required" } } } },
          "429": {
            "description": "Per-IP or per-form 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/IngestError" }, "example": { "ok": false, "error": "rate limit" } } }
          },
          "500": { "description": "Internal error (e.g. the processing job could not be enqueued). The submission row may already exist and be re-processed by the worker sweep; a retry is safe.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/IngestError" } } } }
        }
      },
      "get": {
        "tags": ["Ingestion"],
        "summary": "Method not allowed (POST only)",
        "description": "The endpoint only accepts POST submissions. A browser GET returns 405: a friendly HTML page for browsers, or a JSON 405 when Accept includes application/json. It does not reveal whether a given form id exists.",
        "operationId": "submitFormGet",
        "security": [],
        "responses": {
          "405": {
            "description": "This endpoint only accepts POST submissions.",
            "content": {
              "application/json": { "schema": { "$ref": "#/components/schemas/IngestError" }, "example": { "ok": false, "error": "This endpoint only accepts POST submissions. Submit your form with method=\"POST\"." } },
              "text/html": { "schema": { "type": "string" } }
            }
          }
        }
      },
      "options": {
        "tags": ["Ingestion"],
        "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 (or, for hosted forms, a first-party hosted origin); omitted otherwise so the browser blocks the request.",
        "operationId": "submitFormPreflight",
        "security": [],
        "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" } }
            }
          }
        }
      }
    },

    "/forms": {
      "get": {
        "tags": ["Forms"],
        "operationId": "listForms",
        "summary": "List forms",
        "description": "Every form in any organization the key's user belongs to. Requires the forms:read scope.",
        "responses": {
          "200": {
            "description": "List of forms.",
            "headers": { "$ref": "#/components/headers/RateLimit" },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/Form" } }
                  },
                  "required": ["data"]
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/ApiError" },
          "403": { "$ref": "#/components/responses/ApiError" },
          "429": { "$ref": "#/components/responses/ApiError" }
        }
      },
      "post": {
        "tags": ["Forms"],
        "operationId": "createForm",
        "summary": "Create a form",
        "description": "Create a form in an organization the key's user is an owner or admin of. Requires the forms:write scope. 403 if the user is not a manager of the target org.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "orgId": { "type": "string", "description": "Target organization id (user must be owner/admin)." },
                  "name": { "type": "string" },
                  "notifyEmail": { "type": "string", "format": "email" },
                  "allowedOrigins": { "type": "array", "items": { "type": "string" } },
                  "redirectUrl": { "type": ["string", "null"] },
                  "routingInstructions": { "type": ["string", "null"] }
                },
                "required": ["orgId", "name", "notifyEmail"]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The created form.",
            "headers": { "$ref": "#/components/headers/RateLimit" },
            "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Form" } }, "required": ["data"] } } }
          },
          "400": { "$ref": "#/components/responses/ApiError" },
          "401": { "$ref": "#/components/responses/ApiError" },
          "403": { "$ref": "#/components/responses/InsufficientScope" },
          "429": { "$ref": "#/components/responses/ApiError" }
        }
      }
    },

    "/forms/{formId}": {
      "parameters": [
        { "name": "formId", "in": "path", "required": true, "schema": { "type": "string" }, "description": "The form id." }
      ],
      "get": {
        "tags": ["Forms"],
        "operationId": "getForm",
        "summary": "Get one form",
        "description": "Public info for a single form, including a submission count. Requires the forms:read scope. 404 if the key's user is not a member of the form's organization.",
        "responses": {
          "200": {
            "description": "The form.",
            "headers": { "$ref": "#/components/headers/RateLimit" },
            "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/FormWithCount" } }, "required": ["data"] } } }
          },
          "401": { "$ref": "#/components/responses/ApiError" },
          "403": { "$ref": "#/components/responses/ApiError" },
          "404": { "$ref": "#/components/responses/ApiError" },
          "429": { "$ref": "#/components/responses/ApiError" }
        }
      },
      "patch": {
        "tags": ["Forms"],
        "operationId": "updateForm",
        "summary": "Update a form",
        "description": "Update the public-writable fields of a form the key's user has org access to. Requires the forms:write scope. Secrets and internal toggles are not settable here. 404 if the form is not in any of the caller's organizations.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": { "type": "string" },
                  "notifyEmail": { "type": "string", "format": "email" },
                  "redirectUrl": { "type": ["string", "null"] },
                  "routingInstructions": { "type": ["string", "null"] },
                  "active": { "type": "boolean" },
                  "allowedOrigins": { "type": "array", "items": { "type": "string" } }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated form.",
            "headers": { "$ref": "#/components/headers/RateLimit" },
            "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Form" } }, "required": ["data"] } } }
          },
          "400": { "$ref": "#/components/responses/ApiError" },
          "401": { "$ref": "#/components/responses/ApiError" },
          "403": { "$ref": "#/components/responses/InsufficientScope" },
          "404": { "$ref": "#/components/responses/ApiError" },
          "429": { "$ref": "#/components/responses/ApiError" }
        }
      },
      "delete": {
        "tags": ["Forms"],
        "operationId": "deleteForm",
        "summary": "Delete a form",
        "description": "Delete a form and all its submissions. Requires the forms:write scope AND owner/admin of the form's organization. Attachment blobs are removed. 404 if the form does not exist or the caller is not a manager of its organization.",
        "responses": {
          "200": {
            "description": "Deletion result.",
            "headers": { "$ref": "#/components/headers/RateLimit" },
            "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "properties": { "id": { "type": "string" }, "deleted": { "type": "boolean" } }, "required": ["id", "deleted"] } }, "required": ["data"] } } }
          },
          "401": { "$ref": "#/components/responses/ApiError" },
          "403": { "$ref": "#/components/responses/InsufficientScope" },
          "404": { "$ref": "#/components/responses/ApiError" },
          "429": { "$ref": "#/components/responses/ApiError" }
        }
      }
    },

    "/forms/{formId}/submissions": {
      "get": {
        "tags": ["Submissions"],
        "operationId": "listSubmissions",
        "summary": "List a form's submissions",
        "description": "Cursor-paginated, newest first. Supports status and since filtering. Requires the submissions:read scope. 404 if the key's user is not a member of the form's organization.",
        "parameters": [
          { "name": "formId", "in": "path", "required": true, "schema": { "type": "string" }, "description": "The form id." },
          { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 }, "description": "Max rows to return (1..100)." },
          { "name": "before", "in": "query", "required": false, "schema": { "type": "string", "format": "date-time" }, "description": "Cursor: return rows strictly older than this createdAt. Use nextBefore from the previous page." },
          { "name": "status", "in": "query", "required": false, "schema": { "type": "string", "enum": ["received", "processed", "spam", "held", "failed"] }, "description": "Only submissions with this status." },
          { "name": "since", "in": "query", "required": false, "schema": { "type": "string", "format": "date-time" }, "description": "Only submissions at or after this createdAt (ISO 8601). Use it to poll for new submissions: carry the newest createdAt from the previous response forward as the next since. Combine with status to poll a single status." }
        ],
        "responses": {
          "200": {
            "description": "Page of submissions.",
            "headers": { "$ref": "#/components/headers/RateLimit" },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": { "type": "array", "items": { "$ref": "#/components/schemas/Submission" } },
                    "nextBefore": { "type": ["string", "null"], "format": "date-time", "description": "Cursor for the next page, or null when no rows were returned." }
                  },
                  "required": ["data", "nextBefore"]
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ApiError" },
          "401": { "$ref": "#/components/responses/ApiError" },
          "403": { "$ref": "#/components/responses/ApiError" },
          "404": { "$ref": "#/components/responses/ApiError" },
          "429": { "$ref": "#/components/responses/ApiError" }
        }
      }
    },

    "/submissions/{id}": {
      "parameters": [
        { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "The submission id." }
      ],
      "get": {
        "tags": ["Submissions"],
        "operationId": "getSubmission",
        "summary": "Get one submission",
        "description": "A single submission. Requires the submissions:read scope. 404 if it does not exist or the key's user is not a member of its form's organization.",
        "responses": {
          "200": {
            "description": "The submission.",
            "headers": { "$ref": "#/components/headers/RateLimit" },
            "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Submission" } }, "required": ["data"] } } }
          },
          "401": { "$ref": "#/components/responses/ApiError" },
          "403": { "$ref": "#/components/responses/ApiError" },
          "404": { "$ref": "#/components/responses/ApiError" },
          "429": { "$ref": "#/components/responses/ApiError" }
        }
      }
    },

    "/submissions/{id}/tags": {
      "post": {
        "tags": ["Submissions"],
        "operationId": "setSubmissionTags",
        "summary": "Replace a submission's tags",
        "description": "Replace a submission's tags wholesale. Requires the triage:write scope. Tags are normalised (trim, lowercase, dedupe, max 20 tags / 40 chars each). 404 if the submission is not in any of the caller's organizations.",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "The submission id." }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "tags": { "type": "array", "items": { "type": "string" } } }, "required": ["tags"] } } } },
        "responses": {
          "200": { "description": "Tags updated.", "headers": { "$ref": "#/components/headers/RateLimit" }, "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "properties": { "id": { "type": "string" }, "updated": { "type": "boolean" } }, "required": ["id", "updated"] } }, "required": ["data"] } } } },
          "400": { "$ref": "#/components/responses/ApiError" },
          "401": { "$ref": "#/components/responses/ApiError" },
          "403": { "$ref": "#/components/responses/InsufficientScope" },
          "404": { "$ref": "#/components/responses/ApiError" },
          "429": { "$ref": "#/components/responses/ApiError" }
        }
      }
    },

    "/submissions/{id}/assignee": {
      "post": {
        "tags": ["Submissions"],
        "operationId": "assignSubmission",
        "summary": "Assign a submission",
        "description": "Assign a submission to a user, or clear it with null. Requires the triage:write scope. The assignee must be a member of the submission's form organization. 404 if the submission is not accessible or the assignee is not a member of its organization.",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "The submission id." }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "assigneeUserId": { "type": ["string", "null"], "description": "User id to assign, or null to clear." } }, "required": ["assigneeUserId"] } } } },
        "responses": {
          "200": { "description": "Assignment updated.", "headers": { "$ref": "#/components/headers/RateLimit" }, "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "properties": { "id": { "type": "string" }, "assigneeUserId": { "type": ["string", "null"] } }, "required": ["id", "assigneeUserId"] } }, "required": ["data"] } } } },
          "400": { "$ref": "#/components/responses/ApiError" },
          "401": { "$ref": "#/components/responses/ApiError" },
          "403": { "$ref": "#/components/responses/InsufficientScope" },
          "404": { "$ref": "#/components/responses/ApiError" },
          "429": { "$ref": "#/components/responses/ApiError" }
        }
      }
    },

    "/submissions/{id}/notes": {
      "post": {
        "tags": ["Submissions"],
        "operationId": "addSubmissionNote",
        "summary": "Add an internal note",
        "description": "Add an internal note to a submission, authored by the key's user. Requires the triage:write scope. The body is trimmed and capped at 5000 chars; empty is 400. 404 if the submission is not in any of the caller's organizations.",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "The submission id." }],
        "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "body": { "type": "string" } }, "required": ["body"] } } } },
        "responses": {
          "201": { "description": "Note created.", "headers": { "$ref": "#/components/headers/RateLimit" }, "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "properties": { "id": { "type": "string", "description": "The new note id." }, "submissionId": { "type": "string" }, "createdAt": { "type": "string", "format": "date-time" } }, "required": ["id", "submissionId", "createdAt"] } }, "required": ["data"] } } } },
          "400": { "$ref": "#/components/responses/ApiError" },
          "401": { "$ref": "#/components/responses/ApiError" },
          "403": { "$ref": "#/components/responses/InsufficientScope" },
          "404": { "$ref": "#/components/responses/ApiError" },
          "429": { "$ref": "#/components/responses/ApiError" }
        }
      }
    }
  },

  "components": {
    "securitySchemes": {
      "bearerAuth": { "type": "http", "scheme": "bearer", "description": "Send the API key as 'Authorization: Bearer <key>'. Used by the REST API only." },
      "apiKeyHeader": { "type": "apiKey", "in": "header", "name": "X-Api-Key", "description": "Alternatively, send the API key as 'X-Api-Key: <key>'. Used by the REST API only." }
    },
    "headers": {
      "RateLimit": {
        "X-RateLimit-Limit": { "schema": { "type": "integer" }, "description": "Requests allowed per window for this key." },
        "X-RateLimit-Remaining": { "schema": { "type": "integer" }, "description": "Requests remaining in the current window." },
        "X-RateLimit-Reset": { "schema": { "type": "integer" }, "description": "Epoch seconds at which the current window resets." }
      }
    },
    "responses": {
      "ApiError": { "description": "Error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiError" } } } },
      "InsufficientScope": { "description": "Insufficient scope. The plan includes the API but this key lacks the required scope; the body carries `required`.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ApiError" } } } }
    },
    "schemas": {
      "IngestError": {
        "type": "object",
        "description": "Error body returned by the public ingestion endpoint.",
        "properties": {
          "ok": { "type": "boolean", "const": false },
          "error": { "type": "string" }
        },
        "required": ["ok", "error"]
      },
      "UpgradeError": {
        "type": "object",
        "description": "Ingestion error carrying an upgrade hint (402 quota / 403 plan-gated upload).",
        "properties": {
          "ok": { "type": "boolean", "const": false },
          "error": { "type": "string" },
          "upgrade": { "type": "boolean", "const": true }
        },
        "required": ["ok", "error"]
      },
      "ApiError": {
        "type": "object",
        "description": "Error body returned by the REST API.",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": { "type": "string", "enum": ["unauthorized", "forbidden", "insufficient_scope", "not_found", "rate_limited", "bad_request"] },
              "message": { "type": "string" }
            },
            "required": ["code", "message"]
          },
          "required": { "type": "string", "description": "Present only on insufficient_scope (403): the scope this key must have for the request." }
        },
        "required": ["error"]
      },
      "Form": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "name": { "type": "string" },
          "createdAt": { "type": "string", "format": "date-time" },
          "endpoint": { "type": "string", "format": "uri", "description": "The public submission endpoint, e.g. https://forms.formward.eu/f/<id>." }
        },
        "required": ["id", "name", "createdAt", "endpoint"]
      },
      "FormWithCount": {
        "allOf": [
          { "$ref": "#/components/schemas/Form" },
          { "type": "object", "properties": { "submissionCount": { "type": "integer" } }, "required": ["submissionCount"] }
        ]
      },
      "Submission": {
        "type": "object",
        "description": "A form submission. Only the safe, read-only field set is exposed.",
        "properties": {
          "id": { "type": "string", "description": "Submission id." },
          "payload": { "type": "object", "additionalProperties": true, "description": "The submitted form fields as a JSON object." },
          "status": { "type": "string", "enum": ["received", "processed", "spam", "held", "failed"], "description": "Processing status." },
          "createdAt": { "type": "string", "format": "date-time", "description": "When the submission was received." },
          "spamScore": { "type": ["integer", "null"], "description": "Spam score 0..100, or null if not scored." },
          "aiSummary": { "type": ["string", "null"], "description": "AI-generated one-line summary, or null." }
        },
        "required": ["id", "payload", "status", "createdAt"]
      }
    }
  }
}
