[{"data":1,"prerenderedAt":603},["ShallowReactive",2],{"blog-comparisons/vs-formspree":3},{"id":4,"title":5,"body":6,"category":570,"date":571,"dateModified":572,"description":573,"draft":574,"extension":575,"faq":576,"featured":574,"keywords":588,"meta":589,"navigation":590,"ogDescription":591,"ogTitle":592,"path":593,"readTime":594,"schemaOrg":595,"schemaType":596,"seo":597,"sitemap":598,"stem":599,"tags":600,"twitterCard":601,"__hash__":602},"blog/blog/comparisons/vs-formspree.md","Formspree Alternative: OperatorStack vs Formspree",{"type":7,"value":8,"toc":560},"minimark",[9,18,28,31,34,48,53,56,62,80,83,86,90,93,101,115,118,126,132,136,139,145,151,158,161,165,172,178,188,191,197,208,211,234,247,259,262,266,426,430,433,439,445,455,464,468,471,477,535],[10,11,12,13,17],"p",{},"A Formspree form is one attribute change. You paste an eight-character endpoint into your form's ",[14,15,16],"code",{},"action"," and you are collecting submissions:",[19,20,26],"pre",{"className":21,"code":23,"language":24,"meta":25},[22],"language-html","\u003Cform action=\"https://formspree.io/f/mayzgjbk\" method=\"POST\">\n  \u003Cinput type=\"email\" name=\"email\" required />\n  \u003Cbutton type=\"submit\">Send\u003C/button>\n\u003C/form>\n","html","",[14,27,23],{"__ignoreMap":25},[10,29,30],{},"No build step, no server, no framework requirement. That is a genuinely good design, and if what you want is a contact form that emails you, this comparison has a short answer: keep using it.",[10,32,33],{},"The argument for an alternative is about what arrives at the other end. Formspree's product is an email notification. Everything convenient and everything limiting about it follows from that one fact.",[35,36,37],"tldr",{},[10,38,39,40,43,44,47],{},"Formspree delivers a form submission as an email, so the submission arrives knowing only the fields you posted. Which page produced it, which campaign, and whether that person already joined your waitlist are all things you have to smuggle in as hidden inputs you maintain by hand. An OperatorStack form posts to ",[14,41,42],{},"/v1/f/{form_key}"," and the server upserts a contact, links the visitor ID, infers the schema, and writes a ",[14,45,46],{},"form_submission"," analytics event before it replies. Formspree still wins on file uploads and captcha.",[49,50,52],"h2",{"id":51},"what-formspree-actually-delivers","What Formspree Actually Delivers",[10,54,55],{},"Formspree's underscore-prefixed fields tell you what the product is built around. They are all email controls:",[19,57,60],{"className":58,"code":59,"language":24,"meta":25},[22],"\u003Cform action=\"https://formspree.io/f/mayzgjbk\" method=\"POST\">\n  \u003Cinput type=\"email\" name=\"email\" required />\n  \u003Cinput type=\"hidden\" name=\"_subject\" value=\"New demo request\" />\n  \u003Cinput type=\"hidden\" name=\"_cc\" value=\"sales@yoursite.com\" />\n  \u003Cinput type=\"text\" name=\"_gotcha\" style=\"display:none\" />\n  \u003Cbutton type=\"submit\">Send\u003C/button>\n\u003C/form>\n",[14,61,59],{"__ignoreMap":25},[10,63,64,67,68,71,72,75,76,79],{},[14,65,66],{},"_subject"," sets the subject line. ",[14,69,70],{},"_cc"," adds recipients. ",[14,73,74],{},"_gotcha"," is a honeypot. The naming an older Formspree used for its reply-to field, ",[14,77,78],{},"_replyto",", is the clearest tell: the mental model is that you are composing a message, and the form is the compose window.",[10,81,82],{},"This is exactly right for a contact form. An inquiry shows up, you hit reply, the conversation continues in the place conversations already happen. Nothing about an analytics pipeline improves that.",[10,84,85],{},"It stops being right the moment you want to do something with submissions in aggregate.",[49,87,89],{"id":88},"the-accept-header-that-decides-your-ux","The Accept Header That Decides Your UX",[10,91,92],{},"The single most common Formspree support question is why a form redirects away from the page. The answer is a request header, and it catches people because it is not the header you would guess.",[19,94,99],{"className":95,"code":97,"language":98,"meta":25},[96],"language-js","await fetch(\"https://formspree.io/f/mayzgjbk\", {\n  method: \"POST\",\n  headers: {\n    \"Content-Type\": \"application/json\",\n    Accept: \"application/json\",\n  },\n  body: JSON.stringify({ email }),\n});\n","js",[14,100,97],{"__ignoreMap":25},[10,102,103,104,107,108,111,112,114],{},"Drop that ",[14,105,106],{},"Accept"," line and Formspree answers with a redirect to its own hosted thank-you page instead of a JSON body. ",[14,109,110],{},"Content-Type"," describes what you are sending and does not affect the reply. ",[14,113,106],{}," is the one that decides, and a form that works perfectly in your handler will still navigate the user away without it.",[10,116,117],{},"Their React package exists largely to make this a non-issue:",[19,119,124],{"className":120,"code":122,"language":123,"meta":25},[121],"language-jsx","import { useForm, ValidationError } from \"@formspree/react\";\n\nfunction ContactForm() {\n  const [state, handleSubmit] = useForm(\"mayzgjbk\");\n  if (state.succeeded) return \u003Cp>Thanks.\u003C/p>;\n  return (\n    \u003Cform onSubmit={handleSubmit}>\n      \u003Cinput type=\"email\" name=\"email\" />\n      \u003CValidationError prefix=\"Email\" field=\"email\" errors={state.errors} />\n      \u003Cbutton type=\"submit\" disabled={state.submitting}>\n        Send\n      \u003C/button>\n    \u003C/form>\n  );\n}\n","jsx",[14,125,122],{"__ignoreMap":25},[127,128,129],"warning-box",{},[10,130,131],{},"Do not plan around a submission cap you read in a blog post, this one included. Form services change their plan limits and their overage behavior, and most comparison posts quote a number that was true when they were written. Open your own billing page and check what happens at the cap, because \"submissions stop being delivered\" and \"submissions queue\" are very different launch-week outcomes.",[49,133,135],{"id":134},"attribution-is-a-hidden-field-you-maintain","Attribution Is a Hidden Field You Maintain",[10,137,138],{},"Formspree records the fields you post. That is the whole data model, and it means attribution is your job:",[19,140,143],{"className":141,"code":142,"language":24,"meta":25},[22],"\u003Cinput type=\"hidden\" name=\"page\" />\n\u003Cinput type=\"hidden\" name=\"utm_source\" />\n\u003Cinput type=\"hidden\" name=\"referrer\" />\n",[14,144,142],{"__ignoreMap":25},[19,146,149],{"className":147,"code":148,"language":98,"meta":25},[96],"const params = new URLSearchParams(location.search);\nform.page.value = location.pathname;\nform.utm_source.value = params.get(\"utm_source\") || \"\";\nform.referrer.value = document.referrer;\n",[14,150,148],{"__ignoreMap":25},[10,152,153,154,157],{},"This works. It also has to be repeated on every form, kept in sync when you add a campaign parameter, and it captures only the visit that submitted. A person who read three blog posts last week, came back through a newsletter link today, and then filled the form arrives as ",[14,155,156],{},"utm_source: newsletter"," with no trace of the three posts that actually did the work.",[10,159,160],{},"The deeper problem is that the values are strings in an email. There is nothing on the other side to join them to, because Formspree never saw the page views.",[49,162,164],{"id":163},"where-an-operatorstack-submission-goes","Where an OperatorStack Submission Goes",[10,166,167,168,171],{},"The markup is comparable. You create a form in the dashboard, get a key like ",[14,169,170],{},"frm_abc123",", and point at the endpoint:",[19,173,176],{"className":174,"code":175,"language":24,"meta":25},[22],"\u003Cform method=\"POST\" action=\"https://api.operatorstack.dev/v1/f/frm_abc123\">\n  \u003Cinput name=\"email\" type=\"email\" required />\n  \u003Cinput name=\"company\" type=\"text\" />\n  \u003Cinput type=\"hidden\" name=\"_redirect\" value=\"https://yoursite.com/thanks\" />\n  \u003Cbutton>Send\u003C/button>\n\u003C/form>\n",[14,177,175],{"__ignoreMap":25},[10,179,180,181,183,184,187],{},"The endpoint takes URL-encoded, JSON, or multipart bodies, so there is no ",[14,182,106],{}," header to remember. ",[14,185,186],{},"_redirect"," answers with a 303 and the browser lands on your own thank-you page. Underscore-prefixed fields are treated as instructions and stripped before anything is stored.",[10,189,190],{},"Or through the SDK, if the script tag is already on the page:",[19,192,195],{"className":193,"code":194,"language":98,"meta":25},[96],"await OperatorStack.submitForm(\"frm_abc123\", {\n  email: \"founder@example.com\",\n  company: \"Acme\",\n});\n",[14,196,194],{"__ignoreMap":25},[10,198,199,200,203,204,207],{},"That call is the one that matters here, because of what it adds on the way out. ",[14,201,202],{},"submitForm"," attaches ",[14,205,206],{},"visitor_id: getOrCreateVisitorId()"," to the body before sending, and that is the same visitor ID your page views were already recorded under. The submission arrives attached to the session that produced it, with no hidden inputs involved.",[10,209,210],{},"Then three things happen server-side before you get a response:",[212,213,215],"step",{"number":214},"1",[10,216,217,221,222,225,226,229,230,233],{},[218,219,220],"strong",{},"A contact is created or updated."," If the payload has an ",[14,223,224],{},"email",", the endpoint upserts a unified contact with ",[14,227,228],{},"source: \"form\""," and ",[14,231,232],{},"source_detail"," set to the form's name, then links the visitor ID to it. The person who filled your research form and your waitlist form is one contact record, not two rows in two places.",[212,235,237],{"number":236},"2",[10,238,239,242,243,246],{},[218,240,241],{},"The schema updates itself."," Fields are inferred from what you actually sent. Add ",[14,244,245],{},"company"," to your markup and it becomes a filterable column on the next submission, with no schema edit anywhere.",[212,248,250],{"number":249},"3",[10,251,252,255,256,258],{},[218,253,254],{},"An analytics event is written."," A ",[14,257,46],{}," event lands on the same timeline as your page views, carrying the same visitor ID. \"Which page did people fill this form from\" becomes a question the data answers on its own, rather than one you prepared for in advance.",[10,260,261],{},"You still get the email. It is a notification about a record, rather than the record itself.",[49,263,265],{"id":264},"side-by-side","Side by Side",[267,268,269,284],"table",{},[270,271,272],"thead",{},[273,274,275,278,281],"tr",{},[276,277],"th",{},[276,279,280],{},"Formspree",[276,282,283],{},"OperatorStack",[285,286,287,303,314,331,341,352,363,376,386,397,408],"tbody",{},[273,288,289,293,298],{},[290,291,292],"td",{},"Setup",[290,294,295,296],{},"Paste an endpoint into ",[14,297,16],{},[290,299,300,301],{},"Paste a form key into ",[14,302,16],{},[273,304,305,308,311],{},[290,306,307],{},"Primary destination",[290,309,310],{},"Your inbox",[290,312,313],{},"A contact record, plus an email notification",[273,315,316,319,325],{},[290,317,318],{},"AJAX response format",[290,320,321,322,324],{},"Decided by the ",[14,323,106],{}," header",[290,326,327,328,330],{},"JSON by default, ",[14,329,186],{}," for a 303",[273,332,333,336,339],{},[290,334,335],{},"Accepted bodies",[290,337,338],{},"URL-encoded, JSON, multipart",[290,340,338],{},[273,342,343,346,349],{},[290,344,345],{},"Attribution",[290,347,348],{},"Hidden inputs you fill in and maintain",[290,350,351],{},"Visitor ID attached by the SDK",[273,353,354,357,360],{},[290,355,356],{},"Adding a field",[290,358,359],{},"Post it, it appears in the email",[290,361,362],{},"Post it, it becomes a filterable column",[273,364,365,368,371],{},[290,366,367],{},"Creates a contact record",[290,369,370],{},"No",[290,372,373,374],{},"Yes, on any payload with an ",[14,375,224],{},[273,377,378,381,383],{},[290,379,380],{},"Submission linked to visitor",[290,382,370],{},[290,384,385],{},"Yes, via the shared visitor ID",[273,387,388,391,394],{},[290,389,390],{},"Spam filtering",[290,392,393],{},"Honeypot, plus reCAPTCHA or hCaptcha",[290,395,396],{},"Honeypot, timing gate, 100 req/min per IP",[273,398,399,402,405],{},[290,400,401],{},"File uploads",[290,403,404],{},"Yes, on paid plans",[290,406,407],{},"No, non-string fields are dropped",[273,409,410,413,423],{},[290,411,412],{},"React support",[290,414,415,418,419,422],{},[14,416,417],{},"@formspree/react"," with a ",[14,420,421],{},"useForm"," hook",[290,424,425],{},"Plain SDK call, no framework package",[49,427,429],{"id":428},"when-formspree-is-the-right-call","When Formspree Is the Right Call",[10,431,432],{},"Three cases, and none of them are edge cases.",[10,434,435,438],{},[218,436,437],{},"You need file uploads."," OperatorStack's endpoint parses multipart bodies but keeps only the string values, so a file is dropped rather than stored. If your form takes a resume, a portfolio PDF, or a bug-report screenshot, the comparison ends here.",[10,440,441,444],{},[218,442,443],{},"You want a real captcha."," A honeypot and a rate limit stop opportunistic bots. They do not stop someone who has looked at your form. Formspree's reCAPTCHA and hCaptcha support is a better out-of-the-box answer if your form is being targeted rather than scraped.",[10,446,447,450,451,454],{},[218,448,449],{},"The form is a contact form and the job is replying."," If every submission ends in you writing back, the inbox is the correct destination and a contact record is overhead. Adding a tool to replace a working ",[14,452,453],{},"mailto"," replacement is a downgrade.",[456,457,458],"tip-box",{},[10,459,460,461,463],{},"Most founders end up splitting rather than switching. Formspree stays on the careers page and the upload form. OperatorStack goes on the signup and research forms, where the answers need to join a contact list. A script tag and a form ",[14,462,16],{}," URL do not conflict on the same page.",[49,465,467],{"id":466},"the-actual-decision","The Actual Decision",[10,469,470],{},"Ask what you want to do with a submission a week after it arrives.",[10,472,473,474,476],{},"If the answer is \"reply to it,\" Formspree already does that with less setup than anything else, and the ",[14,475,106],{}," header is the only real gotcha. If the answer contains the word \"which\" (which page, which campaign, which of these people also joined the waitlist), then the submission needs to be joined to something. An email has nothing to join to, and no amount of hidden inputs fixes that, because the thing you want to join to is traffic Formspree never saw.",[478,479,480,497,503,509,525],"faq-section",{},[481,482,484],"faq-item",{"question":483},"Why does my Formspree form redirect to a Formspree page instead of staying put?",[10,485,486,487,490,491,493,494,496],{},"The request did not carry an ",[14,488,489],{},"Accept: application/json"," header. Formspree picks its response format from that header: with it you get a JSON body your page can handle, without it you get a redirect to Formspree's hosted thank-you page. Setting ",[14,492,110],{}," to JSON is not enough, ",[14,495,106],{}," is the one that decides.",[481,498,500],{"question":499},"Can I see which landing page a Formspree submission came from?",[10,501,502],{},"Only if you put it there yourself. Formspree records the fields you post and nothing else, so page, campaign, and referrer have to be hidden inputs your own code fills in. The attribution is only as good as your discipline about keeping those inputs on every form.",[481,504,506],{"question":505},"Does OperatorStack accept file uploads like Formspree does?",[10,507,508],{},"No. The public endpoint parses multipart bodies but keeps only the string fields, so an attached file is dropped rather than stored. Formspree handles uploads on its paid plans, and that is a real reason to stay.",[481,510,512],{"question":511},"Do I need JavaScript on the page for an OperatorStack form?",[10,513,514,515,518,519,521,522,524],{},"No. A plain HTML form with ",[14,516,517],{},"method=\"POST\""," and an ",[14,520,16],{}," pointing at your form key works, and a ",[14,523,186],{}," field sends the browser to your own thank-you page with a 303. Without the script tag there is no visitor ID on the submission, so it still creates a contact but will not join to an analytics session.",[481,526,528],{"question":527},"Is the form key in the action URL a secret?",[10,529,530,531,534],{},"No, on either tool. Formspree's endpoint hash and OperatorStack's ",[14,532,533],{},"frm_..."," key are both public by design, so that plain HTML forms can post to them. The key grants nothing except the ability to submit to that one form. The protection is the rate limit, the honeypot, and the per-form enabled toggle.",[536,537,538,539],"content-related-articles",{},"\n  ",[540,541,538,545],"contentrelatedcard",{"href":542,"title":543,"description":544},"/blog/comparisons/vs-netlify-forms","Netlify Forms Alternative: OperatorStack vs Netlify Forms","The other forms comparison where price is not the argument, and build-time HTML parsing is the deciding factor instead.",[540,546,538,550],{"href":547,"title":548,"description":549},"/blog/comparisons/vs-tally","Tally Alternative: OperatorStack vs Tally Compared","What changes when the form is a cross-origin iframe that your page cannot see into.",[540,551,555],{"href":552,"title":553,"description":554},"/blog/how-to/forms-without-backend","Forms Without a Backend on Any Static or No-Code Site","Point a plain HTML form at a hosted endpoint and collect submissions with no server code and no host lock-in.",[556,557],"cta-box",{"href":558,"label":559},"/","Get Started Free",{"title":25,"searchDepth":561,"depth":561,"links":562},2,[563,564,565,566,567,568,569],{"id":51,"depth":561,"text":52},{"id":88,"depth":561,"text":89},{"id":134,"depth":561,"text":135},{"id":163,"depth":561,"text":164},{"id":264,"depth":561,"text":265},{"id":428,"depth":561,"text":429},{"id":466,"depth":561,"text":467},"comparisons","2026-09-25",null,"Formspree delivers form submissions to your inbox. The tradeoff is that an email cannot tell you which landing page or campaign produced it.",false,"md",[577,579,581,583,585],{"question":483,"answer":578},"Because the request did not carry an Accept: application/json header. Formspree decides its response format from that header: with it you get a JSON body and your page handles the result, without it you get a redirect to Formspree's own hosted thank-you page. Setting Content-Type to JSON is not enough, the Accept header is the one that decides.",{"question":499,"answer":580},"Only if you put it there yourself. Formspree records the fields you post and nothing else, so page, campaign, and referrer have to be hidden inputs that your own code fills in. The attribution is as good as your discipline about keeping those inputs on every form.",{"question":505,"answer":582},"No. The public endpoint parses multipart bodies but keeps only the string fields, so an attached file is dropped rather than stored. Formspree handles uploads on its paid plans. If your form takes a resume or a screenshot, that is a real reason to stay on Formspree.",{"question":511,"answer":584},"No. A plain HTML form with method=POST and an action pointing at your form key works, and a _redirect field sends the browser to your own thank-you page with a 303. The tradeoff is that without the script tag there is no visitor ID on the submission, so it creates a contact but does not join to an analytics session.",{"question":586,"answer":587},"Can I run Formspree and OperatorStack on the same site?","Yes. Formspree lives in a form action URL and OperatorStack lives in a script tag, so they do not collide. A reasonable split is Formspree for a careers or upload form and OperatorStack for the signup and research forms whose answers need to join your contact list.","formspree alternative,formspree vs operatorstack,formspree accept application/json,form endpoint without backend,form submission attribution,static site form handling",{},true,"Formspree turns a form into an email. OperatorStack turns it into a contact record joined to the analytics session that produced it.","Formspree Alternative: OperatorStack Compared","/blog/comparisons/vs-formspree","7 min","[object Object]","BlogPosting",{"title":5,"description":573},{"loc":593},"blog/comparisons/vs-formspree",[],"summary_large_image","61qkksyoDKY1dgbLLU4An3U4EO0toQKdZs0IWDFbPsY",1790937870387]