Getting a form to submit is a solved problem. What happens in the next few seconds — and the next few years — is where the interesting failures live.
A submission does not stay in one place. It gets stored, emailed to one or more people, and quite often forwarded to a webhook that pushes it into a CRM, a chat channel, or a spreadsheet. Each of those is a copy, each copy has a lifespan, and each is a place that someone can later ask you to delete from.
Why do webhooks fail in ways HTTP requests normally do not?
Because a webhook is a request you cannot retry from the client. The visitor has closed the tab. If the delivery fails, the only party who can do anything about it is the sender, and the only party who will notice is you — eventually, when someone asks why a lead never appeared in the CRM.
Three properties separate a webhook receiver that works from one that mostly works:
- Respond fast, process later. Your receiver should acknowledge with a 2xx as soon as it has durably accepted the payload, then do the real work asynchronously. A receiver that calls three downstream APIs before responding will eventually exceed the sender's timeout, and a timeout looks identical to a failure.
- Be idempotent. Any sender worth using retries on failure, which means you will sometimes receive the same submission twice — particularly when your own receiver was slow rather than broken. Key on the submission id and make a duplicate delivery a no-op.
- Return honest status codes. A receiver that returns 200 for everything, including payloads it could not process, is a receiver that has disabled retries. A 500 means "try again"; a 400 means "do not bother."
How do you verify a webhook is really from your form backend?
Your webhook URL is a public endpoint that accepts POSTs containing structured data about your customers. Anyone who learns the URL can send you whatever they like unless you check.
The standard mechanism is an HMAC signature. You configure a shared secret; the sender computes an HMAC over the request body using that secret and sends it in a header — SimpleForm uses X-SimpleForm-Signature — and your receiver recomputes it and compares.
Two implementation details cause most of the broken verifications we see:
- Sign the raw body, not the parsed object. Compute the HMAC over the exact bytes as received, before any JSON parsing. Frameworks that eagerly parse the body and hand you an object have already discarded the thing you needed. Re-serialising it will produce different bytes — different key order, different whitespace, different Unicode escaping — and the comparison will fail every time.
- Compare in constant time. A plain string comparison returns early on the first differing byte, which leaks timing information about how much of a forged signature was correct. Every language's standard library has a constant-time comparison for exactly this; use it.
If verification fails, reject with a 401 and log it. Do not process the payload "just in case" — an unverified webhook payload is untrusted input from an unknown sender.
What does GDPR actually require from a contact form?
Less ceremony than most sites perform, and more discipline than most sites practise.
Start with the thing that is usually over-thought. A contact form does not generally need a consent checkbox for the message itself. When somebody deliberately fills in a form to ask you something, processing that message in order to reply is justified without consent — it is either a step taken at their request or a straightforward legitimate interest. Adding a mandatory "I consent to my data being processed" tickbox in front of a message nobody can send without ticking is not consent in any meaningful sense, because it is not freely given.
Consent becomes genuinely relevant one step later: when you want to use the address for something the person did not ask for. A newsletter subscription bundled into a contact form needs its own unticked, optional checkbox, separate from the message.
What is more often under-done:
- Tell people what happens to the data. A short line near the submit button, linking to your privacy notice, saying who receives the message and roughly how long you keep it. This is a genuine requirement and it takes one sentence.
- Name your processors. If submissions pass through a hosted form backend, an email provider, and a CRM, those are processors acting on your behalf and your privacy notice should reflect that.
- Collect less. The most reliable way to reduce your obligations is to not have the data. A contact form asking for a phone number, a company size, and a job title in order to answer a question is collecting for a purpose that has not happened yet.
- Decide a retention period and enforce it. This is the one nearly everybody skips.
How long should you actually keep submissions?
The legal standard is that you keep personal data for no longer than is necessary for the purpose you collected it for. That is deliberately not a number, because the right number depends on what the form is.
The useful reframing is to ask what the submission is still good for. A general enquiry that was answered three months ago is of no further use to anyone; keeping it is pure liability. A job application may need to be retained for a defined period for recruitment-record reasons. A quote request might reasonably be kept for the length of a sales cycle.
What matters more than picking the perfect duration is that a duration exists and something enforces it. A retention policy that lives only in a document, while the database keeps every submission since launch, is worse than having no policy — it is a written record that you knew what you should be doing.
Practically: pick a period per form, write it in the privacy notice, and make deletion automatic. Manual quarterly clean-ups do not survive a busy quarter.
Where does the data actually end up?
This is the question to answer before you need it, because you will be asked it under time pressure.
A single submission on a well-integrated form typically exists in at least four places: the form backend's database, the notification email in one or more inboxes, whatever system the webhook delivered it into, and any backup of the above. A deletion request covers all of them.
The notification email is the copy people forget. It sits in an inbox, possibly forwarded, possibly in a shared mailbox, indefinitely. If your retention policy is enforced perfectly in the database and the same data is sitting in three people's mail clients from 2024, the policy is decorative.
Draw the map once — form, store, email, webhook destinations, backups — and keep it with the privacy notice. It takes an afternoon and it turns "can you delete everything you have about me" from an investigation into a checklist.
What is the minimum responsible setup?
- Collect only the fields you need to answer the message.
- Say what happens to the data, near the button, with a link to the detail.
- Keep marketing opt-in separate, optional, and unticked.
- Verify webhook signatures against the raw body, in constant time.
- Make webhook receivers fast, idempotent, and honest about failure.
- Set a retention period per form and automate the deletion.
- Know every place a submission lands, including inboxes.
None of that requires a compliance project. It requires deciding once, writing it down, and building the enforcement into the system rather than into someone's calendar.
Frequently asked questions
Usually not for the message itself. Responding to someone who deliberately contacted you generally rests on legitimate interests or on steps taken at their request, not on consent. Consent becomes relevant when you want to do something extra with the address, such as adding it to a marketing list.
Compute the HMAC over the exact raw request body before any parsing, using your shared secret, and compare it to the header value with a constant-time comparison function. Parsing first and re-serialising will change the bytes and break the comparison.
Long enough for the purpose that justified collecting them, and no longer. For a general contact form that is usually months, not years. The requirement is that you have decided a period and that you actually enforce it, rather than keeping everything forever by default.
Delete it from every place it landed, not just the primary store. A submission that fired a webhook into a CRM and generated a notification email exists in at least three systems. Knowing that map in advance is what makes the request answerable in days rather than weeks.