Skip to content
SheetLink Forms

Reliability · 2026-09-09 · 8 min read · By Arden Talbot, founder of SheetLink

Webhook retries, and what receivers owe senders

A webhook is a contract with obligations on both sides. Most integration pain comes from one side quietly assuming the other will be careful.

A ledger-styled illustration of a message being handed across a counter, with a stamped receipt returned immediately.

Two systems, one assumption each

A webhook looks like the simplest integration there is. Something happens over there, an HTTP request arrives over here, you do something with it.

The trouble is that each side arrives with an assumption. The sender assumes the receiver will respond quickly and handle repeats. The receiver assumes the sender will deliver exactly once and never send anything unexpected.

Both assumptions are wrong, and the ways in which they are wrong account for most of the frustration people have with webhook integrations.

The fix is knowing what each side actually owes the other, which is a short list on both sides.

It is worth learning even if you never write a receiver, because the same list is what you should be evaluating when a platform offers webhooks as a feature. A sender that does not retry and does not sign is offering something considerably less useful than the word webhook implies.

What the sender owes

Three things. Retry on failure, because networks fail and receivers restart. Sign the payload, so the receiver can verify the request came from you. And document the payload shape and version it, so a change does not break every integration silently.

Retry behaviour is the part that varies most between platforms. Some retry a handful of times over minutes; some for hours; some not at all. It is worth knowing which you are dealing with, because it determines how bad a five-minute receiver outage actually is.

The signature matters more than people assume. A webhook endpoint is a public URL that accepts data, and without verification anyone who learns the URL can post anything to it.

Where a signature is not available, a secret in the path is a weaker but real alternative, provided the URL is treated as a credential.

What the receiver owes

Two things, and they are the ones most often missed.

Respond quickly, because the sender is usually holding a connection open with a short timeout. Anything that takes real work should be acknowledged first and processed afterwards. A receiver that does five seconds of processing before responding will be marked as failing by senders with a two-second timeout, and will then receive the same event again.

And be idempotent, because you will receive the same event more than once. Not occasionally, but routinely, as a direct consequence of the first obligation being imperfectly met somewhere.

A receiver that satisfies those two is compatible with essentially every sender. One that satisfies neither will appear to work in testing and produce duplicates and gaps in production.

Why duplicates are guaranteed

It is worth understanding why exactly-once delivery is not on offer, because people spend a lot of effort trying to obtain it.

The sender cannot distinguish between a request that failed to arrive and a request that arrived and whose response was lost. From its side both look like no response. The only safe behaviour is to retry, which means the receiver sees a repeat of something it already processed.

No amount of protocol design removes this. It is a property of unreliable networks, not a defect in any particular platform.

So the receiver must handle repeats, which in practice means using an event identifier from the payload to recognise something it has already seen and returning success without acting twice.

The response code conversation

Response codes are the only language a receiver has for telling the sender what to do next, and they get used carelessly.

A success code means do not send this again. It should be returned once the event is safely stored, even if downstream processing has not happened yet. A server error code means try again, and is the correct response to a temporary problem. A client error code means do not retry, because the request itself is unacceptable.

The expensive mistake is returning success for a payload you could not process. The sender concludes the event is delivered, never repeats it, and the data is gone with no record on either side.

The opposite mistake, returning a server error for a malformed payload you will never accept, produces a retry storm about something that can never succeed.

Form platform webhooks specifically

Site builders and form platforms that offer outbound webhooks vary considerably in these properties, and their documentation is often quiet about it.

Payload shapes differ substantially. Some send a flat object of field names and values, some nest the answers under a data key, some include form and submission metadata alongside. A receiver that accommodates the common shapes rather than demanding one is far less brittle, which is how our webhook ingestion is built.

Field naming is the other trap. Platforms with visual builders often send generated identifiers rather than the labels shown to the user, so a mapping step is needed to get sensible column headings rather than a row of opaque keys.

Neither problem is difficult, but both are discovered after going live if nobody tested with a real payload.

Testing a webhook properly

Send a real event, not a hand-written one. The single most common integration bug is a receiver built against a documented example that differs from what the platform actually sends.

This is not hypothetical. Payloads regularly include fields the documentation omits, omit fields the documentation promises, and encode empty values in ways nobody wrote down.

Once it works, test the failure paths deliberately: return an error and confirm the sender retries, send the same event twice and confirm only one row appears, send a malformed payload and confirm it is rejected rather than stored as garbage.

Those three tests take fifteen minutes and cover the failures that would otherwise appear during a busy week.

Security beyond the signature

Verify the signature if there is one, and reject anything that fails rather than logging and continuing.

Beyond that, treat the payload as untrusted input, because it is. A webhook endpoint accepts data from the internet, and the content ends up in your spreadsheet, so the same care about formula injection applies as for a public form.

Rate limiting is worth having too. A misconfigured sender in a retry loop can generate a surprising volume, and an endpoint with no limit will happily accept all of it.

None of this is exotic; it is the same posture as any public endpoint, applied to one that people tend to think of as private because the URL is not published.

The contract in one paragraph

The sender retries, signs and versions. The receiver responds fast, verifies, deduplicates by event identifier, and uses response codes to mean what they say.

When both sides hold up their end, webhook integrations are among the most reliable ways to move data between systems. When either side assumes the other is being careful, they produce exactly the intermittent, hard-to-reproduce problems that give webhooks their reputation.

If you are the receiver

The practical shortcut is to not be, where you have the option. Pointing a platform webhook at a service that already implements this contract removes the whole class of problem from your side.

The submission then joins the same queue, retry and logging path as anything else, which is the outcome you were trying to build anyway.

Where you genuinely must receive them yourself, the shortest safe implementation is three lines of behaviour: verify the signature, store the raw payload keyed by its event identifier, return success. Everything else, including parsing and writing to a destination, happens afterwards from your own queue where you control the retries and can see what failed.

FAQ

Why do I receive the same webhook twice?

Because the sender could not tell whether its previous attempt arrived. A lost response looks identical to a lost request, so retrying is the only safe behaviour and repeats are an expected part of the protocol.

How fast does a receiver need to respond?

Fast enough to beat the sender timeout, which is often a couple of seconds. Acknowledge first, process afterwards, and never do slow work before responding.

What status code should I return?

A success code once the event is safely stored, a server error for a temporary problem you want retried, and a client error for a payload you will never accept. Returning success for something you could not process silently loses the data.

Do I need to verify signatures?

Yes where the sender offers them. A webhook URL is a public endpoint, and without verification anyone who discovers the URL can post arbitrary data into your pipeline.

What if the platform does not sign requests?

Treat the URL itself as a secret, since it is effectively the credential. Keep it out of public repositories and client-side code, and rotate it if it is ever exposed.

Why are my column headings meaningless?

Because the platform is sending generated field identifiers rather than the labels shown to the user. A mapping step between the payload keys and your column names resolves it.

Should I retry from the receiving side?

Only after acknowledging the event. Once you have returned success you own the delivery, so it needs its own queue and retry rather than holding the sender open while you work.

How do I test without waiting for a real submission?

Most platforms include a test-send button, and it is worth using alongside a genuine submission. Test payloads sometimes differ from real ones, which is exactly the discrepancy that causes integrations to fail after launch.

Webhooks in, rows out

Point a platform webhook at SheetLink and submissions join the same screened, queued, retried pipeline as everything else.

Start freeSee the live demo

Designing forms for partial failureField naming conventions for clean columns