{
  "openapi": "3.1.0",
  "info": {
    "title": "Rizzness User API",
    "version": "1.0.0",
    "description": "Rizzness is a form backend as a service. Point any HTML form or JSON POST at a Rizzness ingest endpoint; submissions are stored, spam-filtered, and delivered to email, webhooks, Slack, and other plugins. This spec covers the customer-facing User API: form ingest, forms, submissions, plugins, stats, and the unauthenticated public endpoints. Authenticated endpoints return HATEOAS-style hypermedia links in a top-level `links` array. Full agent guide: https://www.rizzness.com/agents.md\n\nVersioning and deprecation policy: the current User API is the stable v1 surface at /api (unversioned paths). We do not make breaking changes to documented endpoints; additive changes (new fields, new endpoints) may appear at any time and agents must ignore unknown fields. Any future breaking change ships under a new path prefix (/api/v2), with its deprecation announced in this spec and at https://www.rizzness.com/agents.md.\n\nErrors: every 4xx/5xx JSON response uses the Error schema (ok=false plus error or errors[]).",
    "contact": {
      "name": "Rizzness Support",
      "url": "https://www.rizzness.com/contact"
    },
    "termsOfService": "https://www.rizzness.com/privacy-policy"
  },
  "servers": [
    { "url": "https://www.rizzness.com", "description": "User API" },
    { "url": "https://forms.rizzness.com", "description": "Form ingest" }
  ],
  "security": [{ "apiKey": [] }],
  "tags": [
    { "name": "ingest", "description": "Public form submission endpoints. No authentication. Served on forms.rizzness.com." },
    { "name": "forms", "description": "Create and manage forms on the authenticated account." },
    { "name": "submissions", "description": "Read received submissions on the authenticated account." },
    { "name": "plugins", "description": "Delivery integrations per form: webhook, email, Slack, and more." },
    { "name": "stats", "description": "Account-scoped counts and spam stats." },
    { "name": "public", "description": "Unauthenticated public endpoints." }
  ],
  "paths": {
    "/f/{endpoint_token}": {
      "post": {
        "operationId": "submitForm",
        "tags": ["ingest"],
        "summary": "Submit a form (HTML form encoding)",
        "description": "Public ingest endpoint for HTML forms. Point any `<form method=\"POST\">` here. Include a hidden `_hp` honeypot input left empty; a filled honeypot flags the submission as spam. No authentication. Use server https://forms.rizzness.com.",
        "security": [],
        "servers": [{ "url": "https://forms.rizzness.com" }],
        "parameters": [{ "$ref": "#/components/parameters/EndpointToken" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "description": "Any field names you choose. Special fields get extra handling: email, name, message, _hp (honeypot).",
                "additionalProperties": true
              }
            }
          }
        },
        "responses": {
          "200": { "description": "Submission accepted (HTML response or redirect to the form's success URL)." },
          "404": { "$ref": "#/components/responses/NotFound" },
          "422": { "$ref": "#/components/responses/Unprocessable" }
        }
      }
    },
    "/json/{endpoint_token}": {
      "post": {
        "operationId": "submitFormJson",
        "tags": ["ingest"],
        "summary": "Submit a form (JSON)",
        "description": "Public JSON ingest endpoint. POST a JSON object of field names and values. Append `?test=true` to run plugin deliveries synchronously and get each delivery result in the response — use this to verify a pipeline in one call. No authentication. Use server https://forms.rizzness.com.",
        "security": [],
        "servers": [{ "url": "https://forms.rizzness.com" }],
        "parameters": [
          { "$ref": "#/components/parameters/EndpointToken" },
          {
            "name": "test",
            "in": "query",
            "required": false,
            "description": "When \"true\", plugin deliveries run synchronously and the response reports each delivery result.",
            "schema": { "type": "string", "enum": ["true"] }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Any field names you choose, e.g. {\"name\":\"Test\",\"email\":\"test@example.com\",\"message\":\"hello\"}.",
                "additionalProperties": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Submission accepted.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/IngestResult" } } }
          },
          "404": { "$ref": "#/components/responses/NotFound" },
          "422": { "$ref": "#/components/responses/Unprocessable" }
        }
      }
    },
    "/api": {
      "get": {
        "operationId": "getApiRoot",
        "tags": ["stats"],
        "summary": "API root",
        "description": "Returns the account id and a hypermedia link list of the operations your API key permits. Start here to discover what your key can do.",
        "responses": {
          "200": {
            "description": "Account id plus hypermedia links.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HypermediaEnvelope" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/forms": {
      "get": {
        "operationId": "listForms",
        "tags": ["forms"],
        "summary": "List forms",
        "description": "Lists all forms on the authenticated account, newest first.",
        "responses": {
          "200": {
            "description": "Array of forms.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "payload": { "type": "array", "items": { "$ref": "#/components/schemas/Form" } },
                    "links": { "type": "array", "items": { "$ref": "#/components/schemas/Link" } }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      },
      "post": {
        "operationId": "createForm",
        "tags": ["forms"],
        "summary": "Create a form",
        "description": "Creates a form and returns its endpoint_token, submission URLs, and ready-to-paste embed HTML. Requires an API key with can_create_forms.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name"],
                "properties": { "name": { "type": "string", "description": "Descriptive form name." } }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The created form.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FormEnvelope" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "422": { "$ref": "#/components/responses/Unprocessable" }
        }
      }
    },
    "/api/forms/{id}": {
      "get": {
        "operationId": "getForm",
        "tags": ["forms"],
        "summary": "Get a form",
        "description": "Fetch one form by id or endpoint_token.",
        "parameters": [{ "$ref": "#/components/parameters/FormId" }],
        "responses": {
          "200": {
            "description": "The form.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FormEnvelope" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "patch": {
        "operationId": "updateForm",
        "tags": ["forms"],
        "summary": "Update a form",
        "description": "Update name, success_redirect_url, is_active, allowed_domains, or notification_email_addresses. Requires can_create_forms.",
        "parameters": [{ "$ref": "#/components/parameters/FormId" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": { "type": "string" },
                  "success_redirect_url": { "type": "string", "format": "uri" },
                  "is_active": { "type": "boolean" },
                  "allowed_domains": { "type": "array", "items": { "type": "string" } },
                  "notification_email_addresses": { "type": "array", "items": { "type": "string", "format": "email" } }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated form.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FormEnvelope" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "422": { "$ref": "#/components/responses/Unprocessable" }
        }
      }
    },
    "/api/submissions": {
      "get": {
        "operationId": "listSubmissions",
        "tags": ["submissions"],
        "summary": "List submissions",
        "description": "Lists non-spam submissions across the account, newest first. Filter by form, search text, or time range. Requires can_read_submissions.",
        "parameters": [
          { "name": "form_id", "in": "query", "schema": { "type": "string" }, "description": "Form id or endpoint_token; \"all\" or omitted for every form." },
          { "name": "q", "in": "query", "schema": { "type": "string" }, "description": "Full-text search over submission payloads." },
          { "name": "range", "in": "query", "schema": { "type": "string", "default": "24h" }, "description": "Time range, e.g. 24h, 7d." },
          { "name": "page", "in": "query", "schema": { "type": "integer", "minimum": 1 } },
          { "name": "per_page", "in": "query", "schema": { "type": "integer", "minimum": 1 } }
        ],
        "responses": {
          "200": {
            "description": "Array of submissions.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "payload": { "type": "array", "items": { "$ref": "#/components/schemas/Submission" } },
                    "links": { "type": "array", "items": { "$ref": "#/components/schemas/Link" } }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/submissions/spam": {
      "get": {
        "operationId": "listSpamSubmissions",
        "tags": ["submissions"],
        "summary": "List spam submissions",
        "description": "Lists spam-flagged submissions. Same filters as listSubmissions. Requires can_read_spam_submissions.",
        "parameters": [
          { "name": "form_id", "in": "query", "schema": { "type": "string" } },
          { "name": "page", "in": "query", "schema": { "type": "integer", "minimum": 1 } },
          { "name": "per_page", "in": "query", "schema": { "type": "integer", "minimum": 1 } }
        ],
        "responses": {
          "200": {
            "description": "Array of spam submissions.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "payload": { "type": "array", "items": { "$ref": "#/components/schemas/Submission" } },
                    "links": { "type": "array", "items": { "$ref": "#/components/schemas/Link" } }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/submissions/{id}": {
      "get": {
        "operationId": "getSubmission",
        "tags": ["submissions"],
        "summary": "Get a submission",
        "description": "Fetch one submission with its full payload. Requires can_read_submissions.",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Submission id." }
        ],
        "responses": {
          "200": {
            "description": "The submission.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "payload": { "$ref": "#/components/schemas/Submission" },
                    "links": { "type": "array", "items": { "$ref": "#/components/schemas/Link" } }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/forms/{form_id}/plugins": {
      "get": {
        "operationId": "listFormPlugins",
        "tags": ["plugins"],
        "summary": "List a form's plugins",
        "description": "Lists the delivery plugins configured on one form.",
        "parameters": [{ "$ref": "#/components/parameters/FormIdPath" }],
        "responses": {
          "200": {
            "description": "Array of plugins.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "payload": { "type": "array", "items": { "$ref": "#/components/schemas/Plugin" } },
                    "links": { "type": "array", "items": { "$ref": "#/components/schemas/Link" } }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "post": {
        "operationId": "createFormPlugin",
        "tags": ["plugins"],
        "summary": "Add a plugin to a form",
        "description": "Adds a delivery plugin (e.g. webhook, slack, email) to a form. Config shape depends on plugin_type; for webhook: {\"url\": \"https://...\"}.",
        "parameters": [{ "$ref": "#/components/parameters/FormIdPath" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["plugin_type"],
                "properties": {
                  "plugin_type": { "type": "string", "description": "Plugin key, e.g. webhook, slack." },
                  "config": { "type": "object", "additionalProperties": true }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The created plugin.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PluginEnvelope" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "422": { "$ref": "#/components/responses/Unprocessable" }
        }
      }
    },
    "/api/forms/{form_id}/plugins/{id}": {
      "delete": {
        "operationId": "deleteFormPlugin",
        "tags": ["plugins"],
        "summary": "Remove a plugin from a form",
        "parameters": [
          { "$ref": "#/components/parameters/FormIdPath" },
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Plugin id." }
        ],
        "responses": {
          "200": { "description": "Removed." },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/forms/{form_id}/plugins/{id}/rotate_secret": {
      "post": {
        "operationId": "rotatePluginSecret",
        "tags": ["plugins"],
        "summary": "Rotate a webhook plugin's signing secret",
        "parameters": [
          { "$ref": "#/components/parameters/FormIdPath" },
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string" }, "description": "Plugin id." }
        ],
        "responses": {
          "200": {
            "description": "The plugin with a new secret.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PluginEnvelope" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/stats": {
      "get": {
        "operationId": "getAccountStats",
        "tags": ["stats"],
        "summary": "Account stats",
        "description": "Form counts and submission counts for the last 24 hours and 7 days. Session auth only.",
        "responses": {
          "200": {
            "description": "Counts.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "payload": {
                      "type": "object",
                      "properties": {
                        "totalForms": { "type": "integer" },
                        "active": { "type": "integer" },
                        "subs24h": { "type": "integer" },
                        "subs7d": { "type": "integer" }
                      }
                    },
                    "links": { "type": "array", "items": { "$ref": "#/components/schemas/Link" } }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" }
        }
      }
    },
    "/api/form_stats/{form_id}": {
      "get": {
        "operationId": "getFormStats",
        "tags": ["stats"],
        "summary": "Per-form stats",
        "parameters": [{ "$ref": "#/components/parameters/FormIdPath" }],
        "responses": {
          "200": {
            "description": "Stats for one form.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/HypermediaEnvelope" } } }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/api/public/spam_metrics": {
      "get": {
        "operationId": "getPublicSpamMetrics",
        "tags": ["public"],
        "summary": "Public spam metrics",
        "description": "Aggregate spam-blocking metrics across the platform. No authentication.",
        "security": [],
        "responses": {
          "200": {
            "description": "Aggregate metrics.",
            "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } }
          }
        }
      }
    },
    "/api/public/verify_domain": {
      "get": {
        "operationId": "verifyDomain",
        "tags": ["public"],
        "summary": "Verify a custom domain",
        "description": "Checks DNS configuration for a custom form-page domain. No authentication.",
        "security": [],
        "parameters": [
          { "name": "domain", "in": "query", "required": true, "schema": { "type": "string" }, "description": "Domain to verify." }
        ],
        "responses": {
          "200": {
            "description": "Verification result.",
            "content": { "application/json": { "schema": { "type": "object", "additionalProperties": true } } }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "frk_ API key",
        "description": "Bearer token: an account API key with the frk_ prefix. Create one in the Rizzness dashboard. Keys have granular permissions (can_create_forms, can_read_submissions, can_read_spam_submissions)."
      }
    },
    "parameters": {
      "EndpointToken": {
        "name": "endpoint_token",
        "in": "path",
        "required": true,
        "schema": { "type": "string" },
        "description": "The form's endpoint token, from createForm or the dashboard."
      },
      "FormId": {
        "name": "id",
        "in": "path",
        "required": true,
        "schema": { "type": "string" },
        "description": "Form id or endpoint_token."
      },
      "FormIdPath": {
        "name": "form_id",
        "in": "path",
        "required": true,
        "schema": { "type": "string" },
        "description": "Form id or endpoint_token."
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing or invalid API key.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "NotFound": {
        "description": "Resource not found.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "Unprocessable": {
        "description": "Validation failed.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    },
    "schemas": {
      "Link": {
        "type": "object",
        "description": "HATEOAS hypermedia link.",
        "properties": {
          "rel": { "type": "string" },
          "href": { "type": "string" },
          "method": { "type": "string" }
        }
      },
      "HypermediaEnvelope": {
        "type": "object",
        "properties": {
          "payload": { "type": "object", "additionalProperties": true },
          "links": { "type": "array", "items": { "$ref": "#/components/schemas/Link" } }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean", "const": false },
          "error": { "type": "string" },
          "errors": { "type": "array", "items": { "type": "string" } }
        }
      },
      "Form": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "name": { "type": "string" },
          "endpoint_token": { "type": "string" },
          "success_redirect_url": { "type": ["string", "null"], "format": "uri" },
          "is_active": { "type": "boolean" },
          "allowed_domains": { "type": "array", "items": { "type": "string" } },
          "notification_email_addresses": { "type": "array", "items": { "type": "string" } },
          "submission_url": { "type": "string", "format": "uri", "description": "POST target for HTML forms (forms.rizzness.com)." },
          "json_url": { "type": "string", "format": "uri", "description": "POST target for JSON submissions." },
          "embed_html": { "type": "string", "description": "Ready-to-paste HTML form markup including the _hp honeypot." },
          "autocomplete_script_url": { "type": "string", "format": "uri" },
          "submission_count": { "type": "integer" },
          "submission_spam_count": { "type": "integer" },
          "last_submission_at": { "type": ["string", "null"], "format": "date-time" },
          "examples": { "type": "object", "description": "Copy-paste curl examples.", "additionalProperties": { "type": "string" } },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" }
        }
      },
      "FormEnvelope": {
        "type": "object",
        "properties": {
          "payload": { "$ref": "#/components/schemas/Form" },
          "links": { "type": "array", "items": { "$ref": "#/components/schemas/Link" } }
        }
      },
      "Submission": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "form_id": { "type": "string" },
          "form_endpoint_token": { "type": "string" },
          "payload_json": { "type": "object", "description": "The submitted fields.", "additionalProperties": true },
          "special_normalized": { "type": ["object", "null"], "additionalProperties": true },
          "source_ip": { "type": ["string", "null"] },
          "user_agent": { "type": ["string", "null"] },
          "referrer": { "type": ["string", "null"] },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" }
        }
      },
      "Plugin": {
        "type": "object",
        "properties": {
          "id": { "type": "string" },
          "key": { "type": "string", "description": "Plugin type key, e.g. webhook, slack." },
          "name": { "type": "string" },
          "enabled": { "type": "boolean" },
          "config": { "type": "object", "additionalProperties": true },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" }
        }
      },
      "PluginEnvelope": {
        "type": "object",
        "properties": {
          "payload": { "$ref": "#/components/schemas/Plugin" },
          "links": { "type": "array", "items": { "$ref": "#/components/schemas/Link" } }
        }
      },
      "IngestResult": {
        "type": "object",
        "properties": {
          "ok": { "type": "boolean" },
          "id": { "type": "string", "description": "The stored submission id." },
          "deliveries": {
            "type": "array",
            "description": "Present with ?test=true: one entry per plugin delivery attempt.",
            "items": { "type": "object", "additionalProperties": true }
          },
          "links": { "type": "array", "items": { "$ref": "#/components/schemas/Link" } }
        }
      }
    }
  }
}
