API & Automation·7 min read·

Designing Webhook Endpoints Clients Can Trust

Webhooks fail, arrive twice, and get spoofed. Sign payloads, retry with backoff, and make handlers idempotent so clients build on them safely.

NB

Netbay Developer Relations

Netbay Engineering

On this page

A webhook is your API calling your client's URL when something happens — a server provisioned, an invoice paid, a certificate renewed. It is also, from the client's perspective, an unauthenticated stranger asking your code to run. Trust is therefore the whole design problem: prove the message is real, deliver it even when receivers are flaky, and make handling it twice harmless. Three mechanisms carry the weight: signatures, retries, and idempotent handlers.

Payloads that age well

Design the event payload like a resource you version. Include an event id, a type, and a timestamp, and keep the business data under a clearly named field. Version events explicitly so future changes do not silently change what existing handlers process: an event type such as vm.provisioned.v1 signals its own contract. Send the minimal payload that makes the receiver useful, and point at a full resource they can fetch if they need more. A receiver should never have to guess whether a field means what it looks like it means.

Signatures prove it came from you

Anyone who can learn your client's URL can forge a POST to it. Signatures close that hole. Compute an HMAC-SHA256 of the raw request body using a secret shared only with the client, and send the digest in a header such as X-Webhook-Signature. The receiver recomputes the digest from the exact bytes it received and compares in constant time. Verify on the raw body before any parsing, and never compare hex strings with a plain equality operator — timing-safe comparison is the whole point.

python
import hashlib
import hmac

def verify(secret: str, body: bytes, sig_header: str) -> bool:
    expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest("sha256=" + expected, sig_header)

Retries and delivery guarantees

At-least-once is the honest standard for webhooks — no general transport offers exactly-once. So the sender must retry and the receiver must be able to tolerate duplicates. The usual shape is exponential backoff across a day or two: five minutes, then fifteen, then forty-five, giving up at a maximum attempt count after a few days. Each retry window is a chance for a receiver that was down to rejoin. When retries are exhausted, surface the failure out-of-band — a dead-letter list, a dashboard, a support alert — instead of silently dropping events.

Receivers control the retry schedule with their response: a 2xx acknowledges and stops retries; a 4xx says stop, the payload is bad; a 5xx or a timeout means retry. The receiver should acknowledge fast and process asynchronously, rather than holding the webhook response hostage to slow work.

Handlers that survive replay

Because duplicates are guaranteed, the handler must be idempotent. Deduplicate on the event id — keep a set of processed ids with a generous window, because a late retry can arrive hours later — and make the actual write an idempotent upsert keyed on a stable business identifier. That combination converts at-least-once delivery from a liability into a strength.

javascript
// Handler: verify, dedupe, ack fast, process async
const processed = new Set();

app.post("/webhooks/vm-events", async (req, res) => {
  if (!verify(process.env.WEBHOOK_SECRET, req.rawBody, req.header("X-Webhook-Signature"))) {
    return res.status(401).end();
  }
  const event = JSON.parse(req.rawBody);
  if (processed.has(event.id)) return res.status(200).end();
  processed.add(event.id);
  await upsertByBusinessId(event.data);   // idempotent write
  res.status(200).end();                  // ack fast, never retried
});
sign → deliver → verify → ack (retries with backoff) app event id · type · ts sign HMAC-SHA256 client verifies dedupe by id 200 ack 2xx stops timeout / 5xx → retry backoff 5m → 15m → 45m, then dead-letter list

Takeaway

A webhook is a promise you make over an unreliable transport. Sign every payload, retry with expanding backoff, and make handlers idempotent — do all three and receivers can build real automation on your events instead of bolting on guesses.

The same discipline applies in miniature to any event-driven setup, including the lifecycle events you can automate against the Netbay V1 API; start your next webhook experiment with the docs at netbayhosts.in.

Keep reading

Follow along on a real VPS

Deploy Linux in under 60 seconds

These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.

Deploy an instance