{
  "openapi": "3.1.0",
  "info": {
    "title": "uvrsforms",
    "version": "0.1.0",
    "description": "Form submission service for static sites. Accepts submissions, validates origin, applies rate limiting and honeypot spam filtering, persists to the uvrsforms DB schema, and sends notifications via Postmark."
  },
  "servers": [
    { "url": "https://forms.uvrs.xyz", "description": "Production" }
  ],
  "components": {
    "securitySchemes": {
      "InternalApiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "x-api-key",
        "description": "Shared secret (INTERNAL_API_KEY) for uvrs-command → uvrsforms management calls."
      }
    },
    "schemas": {
      "SenderDomain": {
        "type": "object",
        "properties": {
          "id":                   { "type": "string", "format": "uuid" },
          "domain":               { "type": "string", "example": "example.com" },
          "postmark_domain_id":   { "type": "integer", "nullable": true },
          "dkim_host":            { "type": "string", "nullable": true },
          "dkim_value":           { "type": "string", "nullable": true },
          "dkim_verified":        { "type": "boolean" },
          "return_path_host":     { "type": "string", "nullable": true },
          "return_path_value":    { "type": "string", "nullable": true },
          "return_path_verified": { "type": "boolean" },
          "last_verified_at":     { "type": "string", "format": "date-time", "nullable": true },
          "created_at":           { "type": "string", "format": "date-time" }
        }
      },
      "DnsRecords": {
        "type": "object",
        "properties": {
          "dkim": {
            "type": "object",
            "properties": {
              "host":     { "type": "string" },
              "value":    { "type": "string" },
              "verified": { "type": "boolean" }
            }
          },
          "return_path": {
            "type": "object",
            "properties": {
              "host":     { "type": "string" },
              "value":    { "type": "string" },
              "verified": { "type": "boolean" }
            }
          }
        }
      },
      "Form": {
        "type": "object",
        "properties": {
          "id":                        { "type": "string", "format": "uuid" },
          "name":                      { "type": "string", "example": "Igniteerp Contact" },
          "slug":                      { "type": "string", "example": "igniteerp-contact" },
          "type":                      { "type": "string", "enum": ["contact", "newsletter", "waitlist", "custom"] },
          "sender_domain_id":          { "type": "string", "format": "uuid", "nullable": true },
          "from_name":                 { "type": "string", "example": "Igniteerp" },
          "from_email":                { "type": "string", "format": "email", "example": "hello@igniteerp.com" },
          "reply_to":                  { "type": "string", "nullable": true },
          "recipient_emails":          { "type": "array", "items": { "type": "string", "format": "email" } },
          "allowed_origins":           { "type": "array", "items": { "type": "string" }, "example": ["https://igniteerp.com"] },
          "auto_reply":                { "type": "boolean" },
          "auto_reply_subject":        { "type": "string", "nullable": true },
          "auto_reply_body":           { "type": "string", "nullable": true },
          "brand_color":               { "type": "string", "example": "#000000" },
          "brand_logo_url":            { "type": "string", "nullable": true },
          "rate_limit_per_ip_per_hour":  { "type": "integer", "example": 5 },
          "rate_limit_per_form_per_day": { "type": "integer", "example": 1000 },
          "status":                    { "type": "string", "enum": ["active", "paused"] },
          "created_at":                { "type": "string", "format": "date-time" },
          "updated_at":                { "type": "string", "format": "date-time" }
        }
      },
      "Submission": {
        "type": "object",
        "properties": {
          "id":                   { "type": "string", "format": "uuid" },
          "form_id":              { "type": "string", "format": "uuid" },
          "type":                 { "type": "string" },
          "fields":               { "type": "object", "additionalProperties": true },
          "submitter_email":      { "type": "string", "nullable": true },
          "submitter_name":       { "type": "string", "nullable": true },
          "ip_address":           { "type": "string", "nullable": true },
          "origin":               { "type": "string", "nullable": true },
          "status":               { "type": "string", "enum": ["pending", "delivered", "failed", "spam"] },
          "notification_sent_at": { "type": "string", "format": "date-time", "nullable": true },
          "confirmation_sent_at": { "type": "string", "format": "date-time", "nullable": true },
          "created_at":           { "type": "string", "format": "date-time" }
        }
      },
      "SubmissionStats": {
        "type": "object",
        "properties": {
          "form_id":   { "type": "string", "format": "uuid" },
          "total":     { "type": "integer" },
          "delivered": { "type": "integer" },
          "failed":    { "type": "integer" },
          "spam":      { "type": "integer" }
        }
      },
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": { "error": { "type": "string" } }
      }
    }
  },
  "paths": {
    "/submit/{slug}": {
      "post": {
        "summary": "Submit a form",
        "description": "Public endpoint. Called from a static site form. Validates origin against the form's allowed_origins list, applies rate limiting, checks honeypot fields, persists the submission, and sends a Postmark notification to recipients.",
        "security": [],
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": { "type": "string" },
            "description": "Unique form slug as configured in uvrs-command."
          }
        ],
        "requestBody": {
          "description": "Any key/value fields. `name` and `email` are treated as the submitter identity if present. `_honey` and `_gotcha` are honeypot fields — include them as hidden inputs; submissions where either is non-empty are silently dropped.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name":    { "type": "string" },
                  "email":   { "type": "string", "format": "email" },
                  "_honey":  { "type": "string", "description": "Honeypot — leave empty" },
                  "_gotcha": { "type": "string", "description": "Honeypot — leave empty" }
                },
                "additionalProperties": true
              }
            },
            "application/x-www-form-urlencoded": {
              "schema": { "type": "object", "additionalProperties": true }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Submission accepted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "id": { "type": "string", "format": "uuid" }
                  }
                }
              }
            }
          },
          "403": { "description": "Origin not allowed or form paused", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Form not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "429": { "description": "Rate limit exceeded", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "503": { "description": "Sender domain not verified", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/health": {
      "get": {
        "summary": "Health check",
        "security": [],
        "responses": {
          "200": {
            "content": { "application/json": { "schema": { "type": "object", "properties": { "ok": { "type": "boolean" }, "service": { "type": "string" } } } } }
          }
        }
      }
    },
    "/internal/domains": {
      "get": {
        "summary": "List sender domains",
        "security": [{ "InternalApiKey": [] }],
        "responses": {
          "200": { "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/SenderDomain" } } } } }
        }
      },
      "post": {
        "summary": "Add a sender domain",
        "description": "Registers the domain with Postmark and stores the returned DKIM and Return-Path DNS records. The domain must then have DNS records added and be verified before it can send.",
        "security": [{ "InternalApiKey": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "type": "object", "required": ["domain"], "properties": { "domain": { "type": "string", "example": "igniteerp.com" } } }
            }
          }
        },
        "responses": {
          "201": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SenderDomain" } } } },
          "400": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "502": { "description": "Postmark API error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/internal/domains/{id}/verify": {
      "post": {
        "summary": "Trigger DNS verification",
        "description": "Calls Postmark to check whether DKIM and Return-Path DNS records have propagated. Updates the DB and invalidates the in-memory verification cache immediately.",
        "security": [{ "InternalApiKey": [] }],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }],
        "responses": {
          "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SenderDomain" } } } },
          "404": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "502": { "description": "Postmark API error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/internal/domains/{id}/dns": {
      "get": {
        "summary": "Fetch live DNS record values from Postmark",
        "security": [{ "InternalApiKey": [] }],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }],
        "responses": {
          "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DnsRecords" } } } },
          "404": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/internal/domains/{id}": {
      "delete": {
        "summary": "Remove a sender domain",
        "security": [{ "InternalApiKey": [] }],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }],
        "responses": {
          "200": { "content": { "application/json": { "schema": { "type": "object", "properties": { "deleted": { "type": "string" } } } } } },
          "404": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/internal/forms": {
      "get": {
        "summary": "List all forms",
        "security": [{ "InternalApiKey": [] }],
        "responses": {
          "200": { "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Form" } } } } }
        }
      },
      "post": {
        "summary": "Create a form",
        "security": [{ "InternalApiKey": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name", "slug", "from_name", "from_email", "recipient_emails"],
                "properties": {
                  "name":                      { "type": "string" },
                  "slug":                      { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]{1,60}[a-z0-9]$" },
                  "type":                      { "type": "string", "enum": ["contact", "newsletter", "waitlist", "custom"], "default": "contact" },
                  "sender_domain_id":          { "type": "string", "format": "uuid" },
                  "from_name":                 { "type": "string" },
                  "from_email":                { "type": "string", "format": "email" },
                  "reply_to":                  { "type": "string" },
                  "recipient_emails":          { "type": "array", "items": { "type": "string", "format": "email" }, "minItems": 1 },
                  "allowed_origins":           { "type": "array", "items": { "type": "string" } },
                  "auto_reply":                { "type": "boolean" },
                  "auto_reply_subject":        { "type": "string" },
                  "auto_reply_body":           { "type": "string" },
                  "rate_limit_per_ip_per_hour":  { "type": "integer", "default": 5 },
                  "rate_limit_per_form_per_day": { "type": "integer", "default": 1000 }
                }
              }
            }
          }
        },
        "responses": {
          "201": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Form" } } } },
          "400": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "409": { "description": "Slug already taken", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/internal/forms/{id}": {
      "get": {
        "summary": "Get a form",
        "security": [{ "InternalApiKey": [] }],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }],
        "responses": {
          "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Form" } } } },
          "404": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "patch": {
        "summary": "Update a form",
        "security": [{ "InternalApiKey": [] }],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name":                      { "type": "string" },
                  "from_name":                 { "type": "string" },
                  "from_email":                { "type": "string", "format": "email" },
                  "reply_to":                  { "type": "string" },
                  "status":                    { "type": "string", "enum": ["active", "paused"] },
                  "sender_domain_id":          { "type": "string", "format": "uuid" },
                  "recipient_emails":          { "type": "array", "items": { "type": "string" } },
                  "allowed_origins":           { "type": "array", "items": { "type": "string" } },
                  "auto_reply":                { "type": "boolean" },
                  "auto_reply_subject":        { "type": "string" },
                  "auto_reply_body":           { "type": "string" },
                  "brand_color":               { "type": "string" },
                  "rate_limit_per_ip_per_hour":  { "type": "integer" },
                  "rate_limit_per_form_per_day": { "type": "integer" }
                }
              }
            }
          }
        },
        "responses": {
          "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Form" } } } },
          "404": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "delete": {
        "summary": "Delete a form and all its submissions",
        "security": [{ "InternalApiKey": [] }],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }],
        "responses": {
          "200": { "content": { "application/json": { "schema": { "type": "object", "properties": { "deleted": { "type": "string" } } } } } },
          "404": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/internal/submissions": {
      "get": {
        "summary": "List submissions",
        "security": [{ "InternalApiKey": [] }],
        "parameters": [
          { "name": "form_id", "in": "query", "schema": { "type": "string", "format": "uuid" } },
          { "name": "status",  "in": "query", "schema": { "type": "string", "enum": ["pending", "delivered", "failed", "spam"] } },
          { "name": "limit",   "in": "query", "schema": { "type": "integer", "default": 50, "maximum": 500 } },
          { "name": "offset",  "in": "query", "schema": { "type": "integer", "default": 0 } }
        ],
        "responses": {
          "200": { "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/Submission" } } } } }
        }
      }
    },
    "/internal/submissions/stats": {
      "get": {
        "summary": "Submission counts grouped by form",
        "security": [{ "InternalApiKey": [] }],
        "responses": {
          "200": { "content": { "application/json": { "schema": { "type": "array", "items": { "$ref": "#/components/schemas/SubmissionStats" } } } } }
        }
      }
    },
    "/internal/submissions/{id}": {
      "get": {
        "summary": "Get a single submission",
        "security": [{ "InternalApiKey": [] }],
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }],
        "responses": {
          "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Submission" } } } },
          "404": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/internal/submissions/export/{formId}": {
      "get": {
        "summary": "Export submissions as CSV",
        "description": "Downloads all submissions for a form as a CSV file. Columns are dynamically derived from the union of all field keys across submissions.",
        "security": [{ "InternalApiKey": [] }],
        "parameters": [{ "name": "formId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }],
        "responses": {
          "200": {
            "description": "CSV file",
            "content": { "text/csv": { "schema": { "type": "string" } } }
          },
          "404": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    }
  }
}
