{
  "openapi": "3.1.0",
  "info": {
    "title": "Form2Lead API",
    "version": "1.0.0",
    "summary": "Form backend & submission API: POST form leads to an endpoint, get email notifications.",
    "description": "Form2Lead is a developer-first form backend and submission API for websites without a server.\n\nPoint any HTML form or HTTP client at `POST /api/v1/f/{publicKey}` and Form2Lead validates the payload, blocks spam server-side, emails you the lead instantly, and stores every submission in your dashboard.\n\n**Authentication:** none. `publicKey` identifies the form — it is NOT a secret credential.\n**Canonical endpoint base:** this document describes the marketing-site origin; forms created in the dashboard display their exact canonical endpoint URL (production may use a dedicated ingestion host).\n\nAgent resources:\n- Site index for AI agents: `/llms.txt`\n- Markdown representations of every docs/marketing page: send `Accept: text/markdown` (`Vary: Accept` is honored)\n- Human documentation: `/docs/api-reference`",
    "contact": {
      "name": "Form2Lead Support",
      "url": "https://form2lead.com/contact"
    },
    "termsOfService": "https://form2lead.com/terms"
  },
  "servers": [
    {
      "url": "https://form2lead.com",
      "description": "Current deployment origin"
    }
  ],
  "tags": [
    {
      "name": "Submissions",
      "description": "Public form submission ingestion"
    },
    {
      "name": "Platform",
      "description": "Health and machine-readable platform metadata"
    }
  ],
  "paths": {
    "/api/v1/f/{publicKey}": {
      "post": {
        "operationId": "submitForm",
        "tags": [
          "Submissions"
        ],
        "summary": "Submit a form",
        "description": "Accepts a flat field payload as `application/json`, `application/x-www-form-urlencoded`, or `multipart/form-data`.\n\n- JSON values may be string, number, boolean, null, or arrays of those. Nested objects are rejected.\n- Duplicate urlencoded/multipart fields accumulate into one array field (max 20 items).\n- Field names starting with `_f2l_` are reserved and rejected.\n- File parts are not supported (400 `FILE_UPLOADS_NOT_SUPPORTED`).\n- Limits: max 50 fields; field names ≤ 100 chars; single values ≤ 10,000 chars; payload ≤ 256 KiB by default (per-form limit may be lower).\n\nClients that do NOT send `Accept: text/html` receive JSON responses. Native HTML form navigations receive a `303 See Other` redirect to the form’s server-configured success URL, or a static “Thank you” page.",
        "parameters": [
          {
            "name": "publicKey",
            "in": "path",
            "required": true,
            "description": "The form’s public endpoint key from the Form2Lead dashboard (identifies the form, not a secret).",
            "schema": {
              "type": "string",
              "examples": [
                "frm_pub_9f8e7d6c"
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SubmissionPayloadJson"
              },
              "example": {
                "name": "Jane Doe",
                "email": "jane@example.com",
                "message": "Project quote inquiry"
              }
            },
            "application/x-www-form-urlencoded": {
              "schema": {
                "$ref": "#/components/schemas/SubmissionPayloadForm"
              }
            },
            "multipart/form-data": {
              "schema": {
                "$ref": "#/components/schemas/SubmissionPayloadForm"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Native HTML navigation success without a configured redirect: static “Thank you” page (text/html).",
            "content": {
              "text/html": {}
            }
          },
          "201": {
            "description": "Submission accepted (JSON clients). The lead was recorded and notifications queued.",
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "access-control-allow-origin": {
                "$ref": "#/components/headers/CorsOrigin"
              },
              "vary": {
                "schema": {
                  "type": "string"
                },
                "description": "Origin"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SubmissionSuccess"
                }
              }
            }
          },
          "303": {
            "description": "Native HTML navigation success WITH a configured success redirect URL (server-side configured only).",
            "headers": {
              "location": {
                "schema": {
                  "type": "string",
                  "format": "uri"
                }
              }
            }
          },
          "400": {
            "description": "`INVALID_PAYLOAD` · `EMPTY_PAYLOAD` · `TOO_MANY_FIELDS` · `FIELD_NAME_TOO_LONG` · `NESTING_NOT_SUPPORTED` · `UNSUPPORTED_VALUE_TYPE` · `RESERVED_FIELD_NAME` · `FILE_UPLOADS_NOT_SUPPORTED`",
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "402": {
            "description": "`PLAN_LIMIT_REACHED` · `SUBSCRIPTION_EXPIRED`",
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "403": {
            "description": "`FORM_DISABLED` · `ORIGIN_NOT_ALLOWED`",
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "404": {
            "description": "`FORM_NOT_FOUND`",
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "413": {
            "description": "`VALUE_TOO_LARGE` · `PAYLOAD_TOO_LARGE`",
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "415": {
            "description": "`UNSUPPORTED_MEDIA_TYPE`",
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "429": {
            "description": "`RATE_LIMITED` (retryable after Retry-After)",
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "retry-after": {
                "$ref": "#/components/headers/RetryAfter"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          },
          "500": {
            "description": "`INTERNAL_ERROR` (retryable)",
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "rejectGetSubmission",
        "tags": [
          "Submissions"
        ],
        "summary": "Method not allowed",
        "description": "No public mutation exists through GET. Always answers 405 with a JSON error body.",
        "security": [],
        "parameters": [
          {
            "name": "publicKey",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "405": {
            "description": "`METHOD_NOT_ALLOWED` — this endpoint only accepts POST.",
            "headers": {
              "x-request-id": {
                "$ref": "#/components/headers/RequestId"
              },
              "allow": {
                "schema": {
                  "type": "string"
                },
                "description": "POST, OPTIONS"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorBody"
                }
              }
            }
          }
        }
      },
      "options": {
        "operationId": "preflightSubmission",
        "tags": [
          "Submissions"
        ],
        "summary": "CORS preflight",
        "description": "Answers `204` with least-permissive per-request CORS headers (`Access-Control-Allow-Methods: POST, OPTIONS`, `Access-Control-Allow-Headers: content-type`, `Access-Control-Max-Age: 86400`). Preflight never reveals whether a form exists.",
        "security": [],
        "parameters": [
          {
            "name": "publicKey",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Origin",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string",
              "format": "uri"
            }
          },
          {
            "name": "Access-Control-Request-Method",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Preflight answer; CORS headers reflect the form’s allow-list (or generic empty headers for unknown forms).",
            "headers": {
              "access-control-allow-origin": {
                "$ref": "#/components/headers/CorsOrigin"
              },
              "access-control-allow-methods": {
                "schema": {
                  "type": "string"
                }
              },
              "access-control-allow-headers": {
                "schema": {
                  "type": "string"
                }
              },
              "access-control-max-age": {
                "schema": {
                  "type": "integer"
                }
              }
            }
          }
        }
      }
    },
    "/api/health": {
      "get": {
        "operationId": "getHealth",
        "tags": [
          "Platform"
        ],
        "summary": "Process liveness probe",
        "description": "Returns `{ \"ok\": true }` with zero database work by default (safe to poll frequently). `?deep=1` additionally verifies the PostgreSQL connection and requires `Authorization: Bearer <CRON_SECRET>` in production; unauthenticated deep probes fall through to plain liveness (200). A failed deep probe answers 503.",
        "security": [],
        "parameters": [
          {
            "name": "deep",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ]
            },
            "description": "`1` verifies the database connection (bearer-authenticated in production)."
          }
        ],
        "responses": {
          "200": {
            "description": "Process (and optionally database) healthy.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthOk"
                },
                "examples": {
                  "liveness": {
                    "value": {
                      "ok": true
                    }
                  },
                  "deep": {
                    "value": {
                      "ok": true,
                      "database": "connected"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "Deep probe only: the database is unreachable.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "ok": false,
                  "database": "unavailable"
                }
              }
            }
          }
        }
      }
    },
    "/llms.txt": {
      "get": {
        "operationId": "getLlmstxt",
        "tags": [
          "Platform"
        ],
        "summary": "Agent-facing llms.txt site index",
        "description": "Markdown index of the Form2Lead site for AI agents (llms.txt). Served as text/markdown.",
        "security": [],
        "responses": {
          "200": {
            "description": "Markdown site index.",
            "content": {
              "text/markdown": {
                "example": {
                  "summary": "# Form2Lead — Developer Form Backend & Submission API …"
                }
              }
            }
          }
        }
      }
    },
    "/llms-full.txt": {
      "get": {
        "operationId": "getLlms-fulltxt",
        "tags": [
          "Platform"
        ],
        "summary": "Agent-facing llms-full.txt site index",
        "description": "Markdown index of the Form2Lead site for AI agents (llms-full.txt). Served as text/markdown.",
        "security": [],
        "responses": {
          "200": {
            "description": "Markdown site index.",
            "content": {
              "text/markdown": {
                "example": {
                  "summary": "# Form2Lead — Developer Form Backend & Submission API …"
                }
              }
            }
          }
        }
      }
    },
    "/openapi.json": {
      "get": {
        "operationId": "getOpenApiSpec",
        "tags": [
          "Platform"
        ],
        "summary": "This OpenAPI specification",
        "description": "Machine-readable contract of the Form2Lead public API (this document).",
        "security": [],
        "responses": {
          "200": {
            "description": "OpenAPI 3.1 document.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "submissionCreated": {
      "post": {
        "summary": "Submission delivered to your webhook endpoint",
        "description": "When you configure a webhook URL on a form, Form2Lead POSTs this payload for every valid submission and retries with backoff on non-2xx responses or timeouts. Configure the URL per form in the dashboard.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WebhookPayload"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Return any 2xx to acknowledge delivery."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {},
    "headers": {
      "RequestId": {
        "description": "Correlation ID echoed on every response; safe caller-supplied IDs ([A-Za-z0-9._:-]{1,64}) are honored.",
        "schema": {
          "type": "string",
          "examples": [
            "f2l_6f9c81a2b3d4"
          ]
        }
      },
      "CorsOrigin": {
        "description": "Exact allowed origin echo (never `*`) when the request origin is allow-listed for the form.",
        "schema": {
          "type": "string",
          "format": "uri"
        }
      },
      "RetryAfter": {
        "description": "Seconds until the rate-limit window resets. Present on 429 responses only.",
        "schema": {
          "type": "integer",
          "minimum": 0
        }
      }
    },
    "schemas": {
      "SubmissionPayloadJson": {
        "type": "object",
        "description": "Flat object of submission fields. Values: string | number | boolean | null | array of those. Keys starting with `_f2l_` are reserved. Max 50 fields.",
        "additionalProperties": {
          "oneOf": [
            {
              "type": "string"
            },
            {
              "type": "number"
            },
            {
              "type": "boolean"
            },
            {
              "type": "null"
            },
            {
              "type": "array",
              "maxItems": 20,
              "items": {
                "oneOf": [
                  {
                    "type": "string"
                  },
                  {
                    "type": "number"
                  },
                  {
                    "type": "boolean"
                  },
                  {
                    "type": "null"
                  }
                ]
              }
            }
          ]
        }
      },
      "SubmissionPayloadForm": {
        "type": "object",
        "description": "Native HTML form fields. Duplicate field names accumulate into an array in submission order.",
        "additionalProperties": true
      },
      "SubmissionSuccess": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true,
            "description": "Always true on success."
          },
          "submissionId": {
            "type": "string",
            "format": "uuid"
          }
        },
        "required": [
          "ok",
          "submissionId"
        ]
      },
      "ErrorBody": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "const": false,
            "description": "Always false on errors."
          },
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "INVALID_PAYLOAD",
                  "EMPTY_PAYLOAD",
                  "TOO_MANY_FIELDS",
                  "FIELD_NAME_TOO_LONG",
                  "VALUE_TOO_LARGE",
                  "PAYLOAD_TOO_LARGE",
                  "NESTING_NOT_SUPPORTED",
                  "UNSUPPORTED_VALUE_TYPE",
                  "RESERVED_FIELD_NAME",
                  "FILE_UPLOADS_NOT_SUPPORTED",
                  "UNSUPPORTED_MEDIA_TYPE",
                  "METHOD_NOT_ALLOWED",
                  "FORM_NOT_FOUND",
                  "FORM_DISABLED",
                  "ORIGIN_NOT_ALLOWED",
                  "RATE_LIMITED",
                  "PLAN_LIMIT_REACHED",
                  "SUBSCRIPTION_EXPIRED",
                  "INTERNAL_ERROR"
                ],
                "description": "Stable machine-readable error code — treat like a breaking API change."
              },
              "message": {
                "type": "string",
                "description": "Safe visitor-facing message; show verbatim. Never contains internals."
              }
            },
            "required": [
              "code",
              "message"
            ]
          }
        },
        "required": [
          "ok",
          "error"
        ]
      },
      "HealthOk": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "const": true
          },
          "database": {
            "type": "string",
            "enum": [
              "connected"
            ],
            "description": "Present on deep probes only."
          }
        },
        "required": [
          "ok"
        ]
      },
      "WebhookPayload": {
        "type": "object",
        "description": "Webhook deliveries contain the submission id, form id, timestamp, and every named form field as key-value pairs.",
        "properties": {
          "submissionId": {
            "type": "string",
            "format": "uuid"
          },
          "formId": {
            "type": "string"
          },
          "submittedAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "additionalProperties": {
          "description": "Named form fields."
        },
        "required": [
          "submissionId"
        ]
      }
    }
  },
  "externalDocs": {
    "url": "https://form2lead.com/docs/api-reference",
    "description": "Full human-readable API reference"
  }
}