[{"data":1,"prerenderedAt":550},["ShallowReactive",2],{"blog-comparisons/vs-netlify-forms":3},{"id":4,"title":5,"body":6,"category":513,"date":514,"dateModified":515,"description":516,"draft":517,"extension":518,"faq":519,"featured":517,"keywords":535,"meta":536,"navigation":537,"ogDescription":538,"ogTitle":539,"path":540,"readTime":541,"schemaOrg":542,"schemaType":543,"seo":544,"sitemap":545,"stem":546,"tags":547,"twitterCard":548,"__hash__":549},"blog/blog/comparisons/vs-netlify-forms.md","Netlify Forms Alternative: OperatorStack vs Netlify Forms",{"type":7,"value":8,"toc":503},"minimark",[9,13,16,19,25,30,33,44,55,61,72,75,79,82,85,91,98,106,109,122,126,129,137,144,151,155,162,168,175,178,184,195,198,221,233,246,249,253,377,381,384,390,404,410,420,424,427,430,478],[10,11,12],"p",{},"On April 14, 2026, Netlify changed what a form submission costs. The changelog is one line: \"Now form submissions are free across all Credit plans. Previously, each form submission cost 1 credit.\" Most of the \"Netlify Forms is getting expensive\" posts you can find were written before that.",[10,14,15],{},"So this comparison does not get to be about price, the same way the Tally comparison does not. If you deploy on Netlify and you want a contact form that emails you, Netlify Forms is two HTML attributes and costs nothing, and you should use it.",[10,17,18],{},"The argument for an alternative is about who creates the form endpoint. With Netlify, you do not. Your build does, by reading your HTML at deploy time, and everything awkward about Netlify Forms follows from that one fact.",[20,21,22],"tldr",{},[10,23,24],{},"Netlify Forms costs nothing now, so the reason to look elsewhere is architectural. The endpoint is created by Netlify's build system parsing your deployed HTML, which means client-rendered forms need a hidden duplicate form you maintain by hand, the form cannot exist anywhere but a Netlify deploy, and submissions land in a dashboard that has never seen your visitor. OperatorStack forms post to a hosted endpoint from your own page and arrive already joined to the visitor, the contact record, and the analytics session.",[26,27,29],"h2",{"id":28},"your-build-creates-the-form-not-you","Your Build Creates the Form, Not You",[10,31,32],{},"Turning on a Netlify form is one attribute:",[34,35,41],"pre",{"className":36,"code":38,"language":39,"meta":40},[37],"language-html","\u003Cform name=\"contact\" method=\"POST\" data-netlify=\"true\">\n  \u003Cinput name=\"email\" type=\"email\" />\n  \u003Cbutton>Send\u003C/button>\n\u003C/form>\n","html","",[42,43,38],"code",{"__ignoreMap":40},[10,45,46,47,50,51,54],{},"At deploy time, Netlify's post-processing parses the static HTML it is about to serve, finds forms carrying ",[42,48,49],{},"data-netlify=\"true\""," (or the bare ",[42,52,53],{},"netlify"," attribute), strips that attribute out, and injects a hidden field:",[34,56,59],{"className":57,"code":58,"language":39,"meta":40},[37],"\u003Cinput type=\"hidden\" name=\"form-name\" value=\"contact\" />\n",[42,60,58],{"__ignoreMap":40},[10,62,63,64,67,68,71],{},"That injected field is what routes the submission. The ",[42,65,66],{},"name"," on the ",[42,69,70],{},"\u003Cform>"," becomes the form's identity in the Netlify UI, and two forms with the same name are the same form.",[10,73,74],{},"This is genuinely elegant for a hand-written HTML page. It is also the source of every Netlify Forms problem people actually hit, because a parser that reads your built HTML can only see forms that exist in your built HTML.",[26,76,78],{"id":77},"the-hidden-duplicate-form","The Hidden Duplicate Form",[10,80,81],{},"If your form is rendered by React, Vue, Svelte, or any client-side framework, the parser sees nothing at deploy time. No endpoint gets created. The form renders fine in the browser, the submit handler fires, and the POST lands on a route that does not handle it.",[10,83,84],{},"Netlify's documented fix is to ship a second, hidden copy of the form in your static HTML:",[34,86,89],{"className":87,"code":88,"language":39,"meta":40},[37],"\u003Cform name=\"pizzaOrder\" data-netlify=\"true\" style=\"display:none;\">\n  \u003Cinput name=\"order\" type=\"text\" />\n  \u003Cinput name=\"form-name\" type=\"hidden\" value=\"pizzaOrder\" />\n\u003C/form>\n",[42,90,88],{"__ignoreMap":40},[10,92,93,94,97],{},"Then your real component has to include the ",[42,95,96],{},"form-name"," field itself:",[34,99,104],{"className":100,"code":102,"language":103,"meta":40},[101],"language-jsx","\u003Cform name=\"pizzaOrder\" method=\"post\" onSubmit={handleSubmit}>\n  \u003Cinput type=\"hidden\" name=\"form-name\" value=\"pizzaOrder\" />\n  \u003Cinput name=\"order\" type=\"text\" onChange={handleChange} />\n  \u003Cinput type=\"submit\" />\n\u003C/form>\n","jsx",[42,105,102],{"__ignoreMap":40},[10,107,108],{},"Two declarations of the same form, in two files, in two languages, that have to agree on the form name and on every field name.",[110,111,112],"warning-box",{},[10,113,114,115,118,119,121],{},"The failure mode is silent. Add a ",[42,116,117],{},"company"," field to your React component and forget the hidden form, and submissions keep succeeding. The ",[42,120,117],{}," value is simply not among the fields Netlify recorded, because that field was never registered. You find out when you export the CSV and a column you expected is missing.",[26,123,125],{"id":124},"the-ajax-body-is-not-json","The AJAX Body Is Not JSON",[10,127,128],{},"The other thing that costs people an afternoon: Netlify's form endpoint takes URL-encoded bodies, not JSON. The documented submit handler is this shape, and the encoding is not optional:",[34,130,135],{"className":131,"code":133,"language":134,"meta":40},[132],"language-js","const handleSubmit = (event) => {\n  event.preventDefault();\n  const formData = new FormData(event.target);\n\n  fetch(\"/\", {\n    method: \"POST\",\n    headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n    body: new URLSearchParams(formData).toString(),\n  });\n};\n","js",[42,136,133],{"__ignoreMap":40},[10,138,139,140,143],{},"Note the target: ",[42,141,142],{},"\"/\"",", your own site root. That is the whole architecture in one line. There is no form service URL, because the endpoint is your deployment. Netlify's edge intercepts the POST before your app sees it.",[10,145,146,147,150],{},"Which answers the portability question before you ask it. Move the site to Vercel or Cloudflare Pages and that ",[42,148,149],{},"fetch(\"/\")"," starts posting into your own app, which has no idea what to do with it.",[26,152,154],{"id":153},"where-the-submission-goes","Where the Submission Goes",[10,156,157,158,161],{},"An OperatorStack form has no build step in it. You create a form in the dashboard, get a key like ",[42,159,160],{},"frm_abc123",", and point markup at the endpoint:",[34,163,166],{"className":164,"code":165,"language":39,"meta":40},[37],"\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",[42,167,165],{"__ignoreMap":40},[10,169,170,171,174],{},"The ",[42,172,173],{},"_redirect"," field is the no-JavaScript path: the endpoint answers with a 303 and the browser lands on your own thank-you page. Underscore-prefixed fields are treated as instructions and stripped from the stored data.",[10,176,177],{},"Or through the SDK, if the script tag is already on the page:",[34,179,182],{"className":180,"code":181,"language":134,"meta":40},[132],"await OperatorStack.submitForm(\"frm_abc123\", {\n  email: \"founder@example.com\",\n  company: \"Acme\",\n});\n",[42,183,181],{"__ignoreMap":40},[10,185,186,187,190,191,194],{},"The SDK call is the version that matters for this comparison, because of what it adds on the way out. ",[42,188,189],{},"submitForm"," attaches ",[42,192,193],{},"visitor_id: getOrCreateVisitorId()"," to the body before sending. That is the same visitor ID your page views were recorded under, so the submission arrives already attached to the session that produced it.",[10,196,197],{},"On the server, three things happen to that submission that do not happen on Netlify:",[199,200,202],"step",{"number":201},"1",[10,203,204,208,209,212,213,216,217,220],{},[205,206,207],"strong",{},"A contact is created or updated."," If the payload contains an ",[42,210,211],{},"email",", the endpoint upserts a unified contact with ",[42,214,215],{},"source: \"form\""," and ",[42,218,219],{},"source_detail"," set to the form's name, then links the visitor ID to it. The same person filling your waitlist form and your research form is one contact record, not two rows in two dashboards.",[199,222,224],{"number":223},"2",[10,225,226,229,230,232],{},[205,227,228],{},"The schema updates itself."," Fields are inferred from what you actually sent. Add ",[42,231,117],{}," to your markup and it becomes a filterable column on the next submission, with no schema edit and no hidden duplicate form to keep in sync.",[199,234,236],{"number":235},"3",[10,237,238,241,242,245],{},[205,239,240],{},"An analytics event is written."," A ",[42,243,244],{},"form_submission"," event lands on the same timeline as your page views, carrying the same visitor ID, so \"which page did people fill this form from\" is a question the data can answer.",[10,247,248],{},"Netlify's submissions are a good list of submissions. They are just a list that has never seen your traffic, your referral codes, or your waitlist.",[26,250,252],{"id":251},"side-by-side","Side by Side",[254,255,256,271],"table",{},[257,258,259],"thead",{},[260,261,262,265,268],"tr",{},[263,264],"th",{},[263,266,267],{},"Netlify Forms",[263,269,270],{},"OperatorStack",[272,273,274,286,301,312,323,334,345,356,367],"tbody",{},[260,275,276,280,283],{},[277,278,279],"td",{},"Cost per submission",[277,281,282],{},"Free on Credit plans since 2026-04-14",[277,284,285],{},"Free tier, then plan limits",[260,287,288,291,294],{},[277,289,290],{},"How the endpoint is created",[277,292,293],{},"Build-time HTML parsing",[277,295,296,297,300],{},"Created in the dashboard, returns a ",[42,298,299],{},"frm_..."," key",[260,302,303,306,309],{},[277,304,305],{},"Client-rendered forms",[277,307,308],{},"Hidden duplicate form, maintained by hand",[277,310,311],{},"Same endpoint, no duplicate",[260,313,314,317,320],{},[277,315,316],{},"Works off your host",[277,318,319],{},"No, the endpoint is your Netlify deploy",[277,321,322],{},"Yes, any host or none",[260,324,325,328,331],{},[277,326,327],{},"AJAX body",[277,329,330],{},"URL-encoded only",[277,332,333],{},"JSON, URL-encoded, or multipart",[260,335,336,339,342],{},[277,337,338],{},"Spam filtering",[277,340,341],{},"Akismet by default, plus honeypot and reCAPTCHA",[277,343,344],{},"Honeypot, timing gate, per-IP rate limit",[260,346,347,350,353],{},[277,348,349],{},"File uploads",[277,351,352],{},"Yes, 8 MB request cap, 30s timeout",[277,354,355],{},"No, non-string fields are dropped",[260,357,358,361,364],{},[277,359,360],{},"Submission linked to visitor",[277,362,363],{},"No",[277,365,366],{},"Yes, via the shared visitor ID",[260,368,369,372,374],{},[277,370,371],{},"Creates a contact record",[277,373,363],{},[277,375,376],{},"Yes, on any payload with an email",[26,378,380],{"id":379},"when-netlify-forms-is-the-right-call","When Netlify Forms Is the Right Call",[10,382,383],{},"Three cases, and they are not edge cases.",[10,385,386,389],{},[205,387,388],{},"You need file uploads."," OperatorStack's endpoint parses multipart bodies but keeps only the string values, so a file is dropped rather than stored. Netlify takes the upload, up to 8 MB per request with a 30-second timeout and one file per input field. If your form collects a resume or a screenshot, this comparison is over.",[10,391,392,395,396,399,400,403],{},[205,393,394],{},"You want spam handled for you."," Every Netlify submission runs through Akismet before it reaches your verified list, and you can add a honeypot with one attribute, ",[42,397,398],{},"netlify-honeypot=\"bot-field\"",", or a reCAPTCHA with ",[42,401,402],{},"data-netlify-recaptcha=\"true\"",". That is a better out-of-the-box spam story than a honeypot and a rate limit.",[10,405,406,409],{},[205,407,408],{},"Your site is hand-written HTML on Netlify and the form emails you."," Two attributes, no script tag, no account anywhere else. Adding a tool to replace that is a downgrade.",[411,412,413],"tip-box",{},[10,414,415,416,419],{},"The split most founders end up with is not one tool. Keep Netlify Forms for the careers page and the upload form, and put OperatorStack on the signup and research forms, where knowing which landing page produced the submission is the point. One script tag does not conflict with a ",[42,417,418],{},"data-netlify"," attribute on the same site.",[26,421,423],{"id":422},"the-actual-decision","The Actual Decision",[10,425,426],{},"Ask what you want to do with a submission a week after it arrives.",[10,428,429],{},"If the answer is \"read it\" or \"reply to it,\" Netlify Forms already does that and costs nothing. If the answer involves the word \"which\" (\"which campaign\", \"which page\", \"which of these people also joined the waitlist\"), you need the submission joined to something, and Netlify's form store has nothing to join it to.",[431,432,433,440,446,455,472],"faq-section",{},[434,435,437],"faq-item",{"question":436},"Do I have to pick one for the whole site?",[10,438,439],{},"No. Netlify Forms is an HTML attribute and OperatorStack is a script tag, so they can run on the same page without interfering. Route each form to whichever one fits what you plan to do with the answers.",[434,441,443],{"question":442},"What happens to my existing Netlify submissions if I switch?",[10,444,445],{},"Export them from the Netlify UI as CSV first. Netlify keeps submissions per form in its own store, and there is no migration path into another tool, so the CSV is the handoff. Imported contacts arrive without a visitor ID, which means historical submissions will not join to analytics sessions even after you switch.",[434,447,449],{"question":448},"Is the form key in the action URL a secret?",[10,450,451,452,454],{},"No. The ",[42,453,299],{}," key is public by design so plain HTML forms can post to it. It 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.",[434,456,458],{"question":457},"Can I keep using a plain HTML form with no JavaScript?",[10,459,460,461,463,464,467,468,471],{},"Yes, on both. Netlify wants the ",[42,462,418],{}," attribute and OperatorStack wants a ",[42,465,466],{},"method=\"POST\""," and an ",[42,469,470],{},"action"," pointing at your form key. The tradeoff is that without the script tag, an OperatorStack submission has no visitor ID attached, so it still creates a contact but will not join to an analytics session.",[434,473,475],{"question":474},"Does the build-time parsing affect Astro or Next.js too?",[10,476,477],{},"It affects anything whose forms are not in the built HTML. A static Astro page with a form in the markup is parsed fine. A Next.js page that renders its form on the client is not, and needs the same hidden duplicate form as a React SPA.",[479,480,481,482],"content-related-articles",{},"\n  ",[483,484,481,488],"contentrelatedcard",{"href":485,"title":486,"description":487},"/blog/comparisons/vs-tally","Tally Alternative: OperatorStack vs Tally Compared","The other forms comparison where price is not the argument, and the cross-origin iframe is the deciding factor instead.",[483,489,481,493],{"href":490,"title":491,"description":492},"/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.",[483,494,498],{"href":495,"title":496,"description":497},"/blog/comparisons/vs-formspree","Formspree Alternative: OperatorStack vs Formspree","The forms comparison where the endpoint is trivial to set up and the submission still arrives knowing nothing about your traffic.",[499,500],"cta-box",{"href":501,"label":502},"/","Get Started Free",{"title":40,"searchDepth":504,"depth":504,"links":505},2,[506,507,508,509,510,511,512],{"id":28,"depth":504,"text":29},{"id":77,"depth":504,"text":78},{"id":124,"depth":504,"text":125},{"id":153,"depth":504,"text":154},{"id":251,"depth":504,"text":252},{"id":379,"depth":504,"text":380},{"id":422,"depth":504,"text":423},"comparisons","2026-09-15",null,"Netlify made form submissions free in April 2026, so price is not the argument. The argument is that your form only exists where you deploy it.",false,"md",[520,523,526,529,532],{"question":521,"answer":522},"Is Netlify Forms still limited to 100 submissions a month?","Not on the credit-based plans. Netlify's April 14, 2026 pricing update states that form submissions are free across all Credit plans, where each submission previously cost 1 credit. Most comparison posts still quote the old 100-per-month free cap, so check your own billing page rather than a blog before you plan around a number.",{"question":524,"answer":525},"Why does my React form work locally but collect nothing after deploy?","Netlify detects forms by parsing the static HTML your build produces. A form that only exists after React renders is invisible to that parser, so no endpoint is ever created and the POST goes nowhere. The fix is shipping a hidden duplicate form in your static HTML with a matching name and a matching input for every field, which you then keep in sync by hand.",{"question":527,"answer":528},"Can I use Netlify Forms if I host somewhere else?","No. The endpoint is your deployed Netlify site. The form posts to your own URL and Netlify's infrastructure intercepts it before it reaches your app. Move the site to Vercel, Cloudflare Pages, or a VPS and the form silently starts posting to a path that does not handle it.",{"question":530,"answer":531},"Does OperatorStack accept file uploads like Netlify Forms does?","No. The form endpoint parses multipart bodies but keeps only the string fields, so an attached file is dropped rather than stored. Netlify handles uploads properly, with an 8 MB cap on the request and a 30-second upload timeout. If your form takes a portfolio PDF or a screenshot, that is a real reason to stay.",{"question":533,"answer":534},"Can I run both on the same site?","Yes. Netlify Forms lives in your HTML markup and OperatorStack lives in a script tag, so they do not collide. A reasonable split is Netlify Forms for a file-upload or careers form and OperatorStack for the signup and research forms whose submissions need to join your analytics and contact list.","netlify forms alternative,netlify forms vs operatorstack,netlify forms limits,static site form handling,form submissions contact list,netlify forms react spa",{},true,"Netlify Forms is free now. The tradeoff is a form endpoint your build system creates, tied to the host, holding submissions that nothing else on your page can see.","Netlify Forms Alternative: OperatorStack Compared","/blog/comparisons/vs-netlify-forms","7 min","[object Object]","BlogPosting",{"title":5,"description":516},{"loc":540},"blog/comparisons/vs-netlify-forms",[],"summary_large_image","jczwLAR5oiZHQGVOHGPoLheB1DdkJyDmuOqdYhBiF5Q",1790937870387]