{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "generatedFrom": "live configuration and code constants at request time",
  "service": {
    "name": "FreelyForm",
    "baseUrl": "https://freelyform.com",
    "whatItIs": "A hosted form backend: it receives HTML form posts, validates them, stores them, delivers notifications and webhooks, and records what each delivery did.",
    "whatItIsNot": "Not a form builder (no editor), not a CRM, not a mail server, and not a payment processor."
  },
  "contact": {
    "support": "support@freelyform.com",
    "abuse": "abuse@freelyform.com"
  },
  "billing": {
    "enabled": false,
    "provider": "Paddle (merchant of record)",
    "note": "No tier is charged today. The per-tier monthly allowance still applies."
  },
  "groups": [
    {
      "id": "ingest",
      "title": "Ingest endpoint",
      "facts": [
        {
          "key": "method and path",
          "value": "POST https://freelyform.com/f/:formId",
          "note": "The form id is public; it is not a secret."
        },
        {
          "key": "accepted payloads",
          "value": "application/x-www-form-urlencoded, multipart/form-data, application/json"
        },
        {
          "key": "request body limit",
          "value": "65,536 bytes"
        },
        {
          "key": "rate limit",
          "value": "120 requests per 1 minute, per form and client IP",
          "note": "Keyed on the client address from CF-Connecting-IP, not on the proxy hop."
        },
        {
          "key": "response formats",
          "value": "HTML success page, redirect, or JSON when the client sends Accept: application/json"
        }
      ]
    },
    {
      "id": "storage",
      "title": "Storage and retention",
      "facts": [
        {
          "key": "every submission is stored first",
          "value": "yes — before any delivery is attempted"
        },
        {
          "key": "archive expiry",
          "value": "none on any tier"
        },
        {
          "key": "attachment retention",
          "value": "90 days by default, configurable per form"
        },
        {
          "key": "export",
          "value": "CSV per form, filterable by status; management API for the same data"
        },
        {
          "key": "deletion",
          "value": "per submission, including its attachments"
        }
      ]
    },
    {
      "id": "uploads",
      "title": "File uploads",
      "facts": [
        {
          "key": "files per submission",
          "value": "5 by default, configurable 1–10 per form"
        },
        {
          "key": "size per file",
          "value": "25 MB, also capped by the form’s own setting"
        },
        {
          "key": "content policy",
          "value": "MIME type and extension allowlist; images, PDF, text, Office documents and ZIP"
        },
        {
          "key": "tier requirement",
          "value": "Personal and above; the free tier refuses uploads with 402 uploads_require_plan"
        }
      ]
    },
    {
      "id": "delivery",
      "title": "Delivery",
      "facts": [
        {
          "key": "channels",
          "value": "owner email, autoresponse to the submitter, webhook (HTTP, Discord, Telegram)"
        },
        {
          "key": "per-channel records",
          "value": "one status per channel, with provider message id and error text"
        },
        {
          "key": "recipient requirement",
          "value": "owner addresses must be confirmed; an unconfirmed address leaves the submission pending_verification"
        },
        {
          "key": "webhook attempts",
          "value": "5, with backoff, then recorded as failed"
        },
        {
          "key": "webhook signature",
          "value": "HMAC-SHA256 over `<timestamp>.<raw body>`, header x-freelyform-signature: t=<unix seconds>,v1=<hex>"
        },
        {
          "key": "replay",
          "value": "a failed webhook delivery can be replayed from the dashboard or the API"
        },
        {
          "key": "suppression",
          "value": "hard bounces and complaints are suppressed per account, before the next send"
        }
      ]
    },
    {
      "id": "spam",
      "title": "Spam classification",
      "facts": [
        {
          "key": "verdict threshold",
          "value": "score ≥ 100"
        },
        {
          "key": "time trap",
          "value": "submissions arriving within 2000 ms of the page render"
        },
        {
          "key": "link flood",
          "value": "more than 2 URLs in the payload"
        },
        {
          "key": "vocabulary signals",
          "value": "15 junk phrases and 8 disposable mail domains"
        },
        {
          "key": "what happens to a spam verdict",
          "value": "stored with status spam and the reasons attached; never delivered, never counted against the allowance"
        },
        {
          "key": "captcha",
          "value": "optional per form (Cloudflare Turnstile); off by default on new forms"
        }
      ]
    },
    {
      "id": "quota",
      "title": "Allowance and over-limit behaviour",
      "facts": [
        {
          "key": "allowance source",
          "value": "per-account override, otherwise the plan’s monthly submission count"
        },
        {
          "key": "over the allowance",
          "value": "submissions are stored as over_limit, no notification is sent, nothing is counted"
        },
        {
          "key": "visitor experience over the allowance",
          "value": "identical to a normal success — visitors are never rejected for the owner’s quota"
        },
        {
          "key": "reprocessing",
          "value": "held submissions can be reprocessed once there is room; that counts them and sends the notification"
        },
        {
          "key": "excluded from the count",
          "value": "spam verdicts and unfinished drafts"
        },
        {
          "key": "usage notices",
          "value": "emails at 50%, 75%, 90% and 100% of the allowance, once each per period"
        }
      ]
    }
  ],
  "plans": [
    {
      "id": "free",
      "name": "Free",
      "monthlyUsd": 0,
      "yearlyUsd": 0,
      "submissionsPerMonth": 80,
      "yearlyPerMonthEquivalent": 0
    },
    {
      "id": "personal",
      "name": "Personal",
      "monthlyUsd": 12,
      "yearlyUsd": 96,
      "submissionsPerMonth": 300,
      "yearlyPerMonthEquivalent": 8
    },
    {
      "id": "professional",
      "name": "Professional",
      "monthlyUsd": 24,
      "yearlyUsd": 192,
      "submissionsPerMonth": 2500,
      "yearlyPerMonthEquivalent": 16
    },
    {
      "id": "business",
      "name": "Business",
      "monthlyUsd": 72,
      "yearlyUsd": 576,
      "submissionsPerMonth": 30000,
      "yearlyPerMonthEquivalent": 48
    },
    {
      "id": "custom",
      "name": "Custom",
      "monthlyUsd": null,
      "yearlyUsd": null,
      "submissionsPerMonth": null,
      "yearlyPerMonthEquivalent": null
    }
  ],
  "entitlements": [
    {
      "feature": "Forms per account",
      "includedFrom": "free"
    },
    {
      "feature": "Submission archive",
      "includedFrom": "free"
    },
    {
      "feature": "Spam controls: honeypot, heuristics, Turnstile",
      "includedFrom": "free"
    },
    {
      "feature": "Email notifications with delivery records",
      "includedFrom": "free"
    },
    {
      "feature": "Autoresponse to the submitter",
      "includedFrom": "free"
    },
    {
      "feature": "Custom redirect after submit",
      "includedFrom": "free"
    },
    {
      "feature": "REST API and CSV export",
      "includedFrom": "free"
    },
    {
      "feature": "File uploads (5 × 25 MB per submission)",
      "includedFrom": "personal"
    },
    {
      "feature": "Rules: require, route, block",
      "includedFrom": "professional"
    },
    {
      "feature": "Signed webhooks with retries and replay",
      "includedFrom": "professional"
    },
    {
      "feature": "Priority support by email",
      "includedFrom": "professional"
    },
    {
      "feature": "Your own sending domain over SMTP",
      "includedFrom": "business"
    },
    {
      "feature": "Deliverability dashboard, bounces and suppressions",
      "includedFrom": "business"
    },
    {
      "feature": "Written SLA and named contact",
      "includedFrom": "custom"
    }
  ],
  "controlFields": [
    {
      "field": "_gotcha",
      "meaning": "Honeypot. A filled value is stored as spam, never delivered."
    },
    {
      "field": "_ts",
      "meaning": "Page render timestamp in epoch milliseconds; enables the time trap."
    },
    {
      "field": "_redirect",
      "meaning": "Where to send the visitor after a successful submission."
    },
    {
      "field": "_subject",
      "meaning": "Overrides the notification subject for this submission."
    },
    {
      "field": "_replyto",
      "meaning": "Sets the reply-to address on the notification."
    },
    {
      "field": "_partial",
      "meaning": "Save a draft and return a resume link instead of delivering."
    },
    {
      "field": "_resume",
      "meaning": "Resume a draft with its token; the completed answers merge into the original record."
    },
    {
      "field": "_idempotency",
      "meaning": "Client-supplied key; a repeat with the same key returns the existing record."
    },
    {
      "field": "cf-turnstile-response",
      "meaning": "Captcha token, required only on forms with captcha enabled."
    }
  ],
  "errorCodes": [
    {
      "code": "form_not_found",
      "status": 404,
      "meaning": "No active form with that id, or the account is suspended."
    },
    {
      "code": "origin_not_allowed",
      "status": 403,
      "meaning": "The Origin header is not in the form’s allowlist."
    },
    {
      "code": "invalid_submission",
      "status": 400,
      "meaning": "The payload could not be parsed into fields."
    },
    {
      "code": "turnstile_required",
      "status": 400,
      "meaning": "The form requires a captcha token and none was sent."
    },
    {
      "code": "turnstile_failed",
      "status": 403,
      "meaning": "A captcha token was sent but did not verify."
    },
    {
      "code": "rule_rejected",
      "status": 422,
      "meaning": "A require rule failed, or a block rule matched."
    },
    {
      "code": "partial_not_enabled",
      "status": 400,
      "meaning": "A draft was sent to a form that does not accept drafts."
    },
    {
      "code": "resume_not_found",
      "status": 404,
      "meaning": "The resume token is invalid, consumed, or expired."
    },
    {
      "code": "already_completed",
      "status": 409,
      "meaning": "The draft was already completed; its resume token was used."
    },
    {
      "code": "uploads_not_enabled",
      "status": 400,
      "meaning": "Files were attached to a form that does not accept them."
    },
    {
      "code": "uploads_require_plan",
      "status": 402,
      "meaning": "The owning account is on a tier without file uploads."
    },
    {
      "code": "uploads_require_complete_submission",
      "status": 400,
      "meaning": "Files were attached to a draft or a resume."
    },
    {
      "code": "too_many_files",
      "status": 400,
      "meaning": "More files than the form allows in one submission."
    },
    {
      "code": "upload_too_large",
      "status": 413,
      "meaning": "A file exceeds the form’s per-file limit."
    },
    {
      "code": "upload_type_not_allowed",
      "status": 400,
      "meaning": "A file’s type or extension is not on the allowlist."
    },
    {
      "code": "quota_exceeded",
      "status": 429,
      "meaning": "Legacy hard-stop path. Current behaviour stores the submission as over_limit instead."
    }
  ],
  "notProvided": [
    "team roles and multi-user accounts",
    "a marketplace of third-party integrations (Sheets, Slack, Zapier, CRM)",
    "card payments on a form",
    "AI triage of submissions",
    "e-signature",
    "SSO",
    "a public status page",
    "a published uptime SLA",
    "claims about delivery rates, customer counts, or revenue"
  ],
  "honesty": [
    "No uptime percentage, delivery rate, or customer count is published anywhere on this site.",
    "Limits change; each document is generated on request from the running configuration."
  ]
}