Skip to main content

A Practical Web Hook Example: Secure Implementation Guide

Find a complete web hook example with code for Node.js, Python, and Go. Learn to securely verify signatures, prevent replay attacks, and debug your endpoints.

Martin Donadieu

Martin Donadieu

Content Marketer

A Practical Web Hook Example: Secure Implementation Guide

You’ve got a service that needs to react when something happens somewhere else. A payment clears. A customer record changes. A repo gets a push. You could poll an API every minute and waste cycles asking “anything new?” over and over, or you can let the source system call you when the event happens.

That’s where most web hook example articles stop. They show a route, print the JSON body, return 200, and call it done. That version works right up until someone sends a forged request, replays a valid one, or your handler breaks because the framework parsed the body before signature verification.

This guide takes the path you’ll use in production. The examples are small enough to copy, but they include the parts that matter: raw body handling, HMAC verification, timestamp checks, fast acknowledgment, and practical debugging.

Table of Contents

What Are Webhooks and Why Use Them

Your billing provider marks an invoice as paid at 02:13. If your app learns about it at 02:14, the customer gets access right away. If your app learns about it on the next polling cycle, they wait, support gets a ticket, and your logs fill with avoidable noise. Webhooks solve that timing problem by sending an HTTP callback when the event happens.

In practical terms, a webhook is an event-driven POST from one system to another. The provider detects a change, such as invoice.paid, order.created, or push, and sends the event data to a URL you control. That removes the constant “anything new yet?” loop that polling creates and cuts a lot of wasted requests.

This pattern shows up in real systems because it maps cleanly to business events. Stripe posts payment outcomes. GitHub posts repository activity. Shopify posts order updates. The shape is simple, but production behavior is not. A webhook that updates money, access, or inventory deserves the same care as any public API endpoint, especially once retries, duplicates, and untrusted traffic enter the picture.

The mental model that helps

A useful way to frame a webhook flow is as four parts working together:

  • Source system. The service that detects the event.
  • Destination endpoint. Your HTTP route that receives it.
  • Event. The named change that occurred, like invoice.paid or push.
  • Payload. The request body with the details your code needs.

The provider sends facts about something that already happened. Your job is to verify the sender, confirm the request is fresh, and apply the change once. That last part matters more than many basic tutorials admit. In production, duplicate delivery is normal behavior, not a corner case.

Practical rule: Use webhooks for event-driven updates. Use polling for scheduled reads, backfills, or providers that do not offer outbound events.

For teams building broader workflow automation and data integration, webhooks usually become the event layer that keeps systems in sync without unnecessary request traffic. If you work on integration-heavy services, Capgo’s backend development articles are useful context because core problems show up around retries, queues, observability, and failure handling.

What works and what fails in production

The setups that hold up well are usually boring by design. Subscribe only to the events you need. Keep endpoints scoped by provider or event family. Store event IDs so duplicate deliveries do not repeat side effects. Return a fast 2xx response once the request is validated and queued, then do slower business logic asynchronously.

The fragile version is easy to recognize. One generic endpoint handles everything. Signature checks get skipped during early testing and never come back. The handler writes directly to critical tables before checking whether the event is authentic or stale. That works in a demo and fails under retry storms, provider outages, or an attacker replaying old requests.

That trade-off defines the rest of this guide. The “hello world” version of a webhook receiver is small. The production-ready version adds signature verification, replay defense, duplicate handling, and debugging hooks from the start.

Anatomy of a Webhook HTTP Request

Before writing code, it helps to look at the request as raw HTTP instead of as a framework object. A typical webhook is just an HTTP POST to a public endpoint with headers and a JSON body.

A simple raw request

POST /webhooks/orders HTTP/1.1
Host: your-app.example
Content-Type: application/json
User-Agent: Provider-Webhooks/1.0
X-Webhook-Signature: sha256=abc123example
X-Webhook-Timestamp: 1712345678

{
  "event": "order.created",
  "id": "evt_123",
  "data": {
    "order_id": "ord_456",
    "status": "created"
  }
}

The important parts are straightforward:

  • Method. In practice, webhook deliveries are usually POST requests.
  • Content-Type. Most modern providers send JSON.
  • User-Agent. Helpful for debugging, but never enough for trust.
  • Signature header. Carries the provider’s authenticity check.
  • Timestamp header. Used to reject stale or replayed requests.

Why the body shape matters

Your code usually doesn’t care about every field. It cares about the event type, the event identifier, and the business object inside data. That’s why good handlers parse only what they need and log the rest for troubleshooting.

OpenAPI now models this pattern directly. OpenAPI 3.1.0 added first-class webhook support with a top-level webhooks object, where each webhook is described like a Path Item but is triggered by the provider. The canonical example uses a newPet webhook with a post operation, a JSON request body, and a 200 response to indicate receipt, as shown in the OpenAPI webhook example.

If you’re documenting your own receiver or provider contracts, strong examples help more than abstract schema prose. I like using references like SheetMergy’s API doc examples because they make it obvious how request examples, field descriptions, and expected responses fit together.

A webhook is simple at the transport layer. Most failures come from mismatched assumptions about headers, body encoding, or signing rules.

How to Securely Verify Webhook Signatures

A signed webhook answers one question: did this payload come from someone who knows the shared secret?

That’s different from asking whether the request is recent or whether you’ve already processed it. Signature verification is the first gate, not the last.

An infographic illustrating the six-step process for verifying webhook signatures to ensure request authenticity and security.

The verification flow

The usual HMAC flow looks like this:

  1. Read the signature from the provider’s header.
  2. Read the raw request body exactly as received.
  3. Load your webhook secret from secure config.
  4. Recompute the expected HMAC using the same algorithm.
  5. Compare the received signature and computed signature with a timing-safe comparison.
  6. Reject the request if they don’t match.

That raw-body step is where a lot of otherwise good implementations fail. If your framework parses JSON first, reformats whitespace, or changes encoding details before hashing, your computed signature won’t match the provider’s.

What to watch for in real code

These are the mistakes I see most often:

  • Hashing parsed JSON. Don’t do JSON.stringify(req.body) and expect it to match.
  • Using normal string equality. Use a timing-safe comparison.
  • Hardcoding secrets. Keep them in environment variables or a secrets manager.
  • Trusting headers alone. A signature header is only meaningful if you verify it.

For teams tightening secret handling across services, Capgo’s guide on API key security for app store compliance is relevant because the same discipline applies here. Secret rotation, scoped access, and avoiding leaks in logs all matter for webhook receivers too.

A generic verification example

const crypto = require('crypto');

function verifySignature(rawBody, receivedSignature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  const a = Buffer.from(receivedSignature, 'utf8');
  const b = Buffer.from(expected, 'utf8');

  if (a.length !== b.length) return false;
  return crypto.timingSafeEqual(a, b);
}

This is intentionally generic. Real providers often prefix signatures, combine timestamps into the signed content, or encode the digest differently. The rule stays the same. Follow the provider’s exact signing format, and always verify against the raw payload.

Protecting Against Replay Attacks

A signed webhook can still be dangerous if it arrives hours later and your handler treats it as new. That happens more often than teams expect. Proxies log traffic, request payloads leak into the wrong place, or a provider retries after a network failure and your endpoint processes the same event twice.

A checklist illustrating five key security measures to effectively prevent replay attacks in web applications.

Signature verification answers one question: did the sender create this payload with the shared secret? Replay protection answers a different one: should this request still be accepted right now? Production receivers need both.

The minimum check that actually matters

A practical replay defense starts with a signed timestamp. The provider includes a timestamp in headers or in the signed message, and your receiver rejects requests that fall outside a small tolerance window.

That flow should look like this:

  • Read the timestamp from the provider-defined location. Do not guess the header name.
  • Parse it as an integer or RFC-formatted date, based on the provider’s spec.
  • Compare it to your server clock.
  • Reject requests that are too old or too far in the future.
  • Verify the timestamp as part of the signature scheme when the provider supports it.

That last point matters. If the timestamp is not covered by the signature, an attacker can swap in a fresh timestamp and replay the original body. I always check the provider’s exact signing format before I trust the timestamp logic.

What to choose for the tolerance window

Five minutes is a common default. It is short enough to shrink the attack window, but long enough to survive small clock drift and normal network delay.

There is a trade-off here. A 30-second window sounds safer, but it breaks more often in real systems, especially when retries, queueing, or regional latency get involved. A 30-minute window is easier to operate, but it gives an attacker much more time if a signed request is exposed. Start with a few minutes, sync your servers with NTP, then tighten only if the provider’s delivery pattern supports it.

Replay defense is not just a timestamp check

Timestamp validation blocks stale requests. It does not stop duplicate processing inside the valid window. If the same signed event is delivered twice within that window, your application still needs to recognize it.

Use a second layer:

  • Track event IDs or delivery IDs in a short-lived store such as Redis.
  • Treat handlers as idempotent so repeated deliveries do not create duplicate orders, emails, or billing actions.
  • Log rejected stale requests with reason codes, but never log secrets or full sensitive payloads.
  • Return a fast response after validation and queue heavy work elsewhere.

Teams that already think about expiry windows and revocation will recognize the pattern. Capgo’s guide to token revocation patterns in Capacitor apps covers the same operational idea. A credential or request that was valid once should not stay trusted forever.

Signed and stale is still unsafe.

Building a Webhook Receiver in Node.js

Node with Express is still the quickest way to get a serious receiver online, but there’s one trap that matters more than any other. You need access to the raw body before Express turns it into an object.

A laptop on a wooden desk displaying Node.js receiver code in a VS Code editor environment.

A production-minded Express example

const express = require('express');
const crypto = require('crypto');

const app = express();
const PORT = process.env.PORT || 3000;
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;

// Capture raw body for signature verification
app.use(
  express.json({
    verify: (req, res, buf) => {
      req.rawBody = buf;
    },
  })
);

function safeEqual(a, b) {
  const aBuf = Buffer.from(a, 'utf8');
  const bBuf = Buffer.from(b, 'utf8');
  if (aBuf.length !== bBuf.length) return false;
  return crypto.timingSafeEqual(aBuf, bBuf);
}

function verifySignature(rawBody, secret, receivedSignature) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  return safeEqual(expected, receivedSignature);
}

function isFresh(timestampHeader, toleranceSeconds = 300) {
  const timestamp = Number(timestampHeader);
  if (!Number.isFinite(timestamp)) return false;

  const now = Math.floor(Date.now() / 1000);
  return Math.abs(now - timestamp) <= toleranceSeconds;
}

app.post('/webhooks/example', async (req, res) => {
  const signature = req.get('x-webhook-signature');
  const timestamp = req.get('x-webhook-timestamp');

  if (!WEBHOOK_SECRET) {
    return res.status(500).send('Webhook secret is not configured');
  }

  if (!signature || !timestamp) {
    return res.status(400).send('Missing required security headers');
  }

  if (!isFresh(timestamp)) {
    return res.status(401).send('Stale webhook');
  }

  const valid = verifySignature(req.rawBody, WEBHOOK_SECRET, signature);
  if (!valid) {
    return res.status(401).send('Invalid signature');
  }

  // Acknowledge quickly
  res.status(200).send('OK');

  // Process after acknowledgement
  try {
    const event = req.body;
    console.log('Accepted event:', event.event, event.id);
    // enqueueJob(event)
  } catch (err) {
    console.error('Post-ack processing failed:', err);
  }
});

app.listen(PORT, () => {
  console.log(`Webhook receiver listening on ${PORT}`);
});

Why this structure holds up

A few choices here are deliberate:

  • Raw body capture happens in middleware. That preserves the original bytes for hashing.
  • Timestamp is checked before business logic. No point doing work for stale traffic.
  • The route returns 200 quickly. Long-running work belongs in a queue or background task.
  • Post-ack processing is isolated. Even if downstream logic fails, the receiver path stays small.

Secrets are the weak point in a lot of webhook implementations. Don’t keep them in source, don’t paste them into test fixtures, and don’t echo them in logs. If you need a broader process around rotation and CI handling, Capgo’s guide to managing secrets in CI/CD pipelines covers the operational side well.

A short walkthrough helps if you want to see the moving pieces in action:

What I’d change for a live system

For a real provider integration, I’d add event ID deduplication in persistent storage, structured logs with request IDs, and a queue behind the acknowledgment path. I’d also avoid a single generic endpoint if multiple providers use different signature formats. Separate handlers are easier to reason about and harder to break.

Building a Webhook Receiver in Python

Flask is a good fit for a clean web hook example because the request handling is explicit and Python’s standard library already gives you what you need for HMAC.

The main thing to remember is the same as in Node. Verify against the raw request bytes, not the parsed JSON dict.

A Flask example with signature and timestamp checks

import os
import time
import hmac
import hashlib
from flask import Flask, request, jsonify

app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get("WEBHOOK_SECRET", "")

def is_fresh(timestamp_header, tolerance_seconds=300):
    try:
        timestamp = int(timestamp_header)
    except (TypeError, ValueError):
        return False

    now = int(time.time())
    return abs(now - timestamp) <= tolerance_seconds

def verify_signature(raw_body, secret, received_signature):
    expected = hmac.new(
        secret.encode("utf-8"),
        raw_body,
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(expected, received_signature)

@app.route("/webhooks/example", methods=["POST"])
def webhook():
    if not WEBHOOK_SECRET:
        return "Webhook secret is not configured", 500

    signature = request.headers.get("X-Webhook-Signature")
    timestamp = request.headers.get("X-Webhook-Timestamp")

    if not signature or not timestamp:
        return "Missing required security headers", 400

    if not is_fresh(timestamp):
        return "Stale webhook", 401

    raw_body = request.get_data()

    if not verify_signature(raw_body, WEBHOOK_SECRET, signature):
        return "Invalid signature", 401

    payload = request.get_json(silent=True) or {}

    # Acknowledge receipt
    response = jsonify({"status": "ok"})

    # In production, queue payload here instead of heavy sync work
    print("Accepted event:", payload.get("event"), payload.get("id"))

    return response, 200

if __name__ == "__main__":
    app.run(port=5000, debug=True)

Flask-specific details that matter

request.get_data() is the key call here. It gives you the raw bytes of the body. If you jump straight to request.json, you’ve already crossed the line where signature mismatches become confusing.

A few implementation notes:

  • Use hmac.compare_digest instead of plain equality.
  • Treat missing headers as a client failure and reject early.
  • Use silent=True for JSON parsing if you want to control error handling instead of letting Flask raise.
  • Keep the route thin. Enqueue work if the payload triggers anything expensive.

Don’t debug signature mismatches by relaxing security checks. Debug them by printing exactly what bytes you hashed and exactly what format the provider expects.

Where teams usually get stuck

The common failure path is testing with a hand-built JSON body, then switching to a real provider and finding the signature no longer matches. That usually means one of three things: the provider signs a timestamped envelope, the signature is encoded differently than you assumed, or middleware changed the body before verification.

When that happens, stop changing the crypto code at random. Capture the raw headers and raw body, reproduce the hash in a tiny isolated script, and only then put it back into the Flask route.

Building a Webhook Receiver in Go

Go is a great choice for webhook receivers because the standard library is enough. You don’t need a framework to get a small, reliable handler, and the code is easy to keep honest.

The one thing to be careful with is body handling. r.Body is a stream. Read it once, hash the bytes you got, and then unmarshal from those same bytes.

A standard library example

package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"crypto/subtle"
	"encoding/hex"
	"encoding/json"
	"io"
	"log"
	"net/http"
	"os"
	"strconv"
	"time"
)

type WebhookPayload struct {
	Event string          `json:"event"`
	ID    string          `json:"id"`
	Data  json.RawMessage `json:"data"`
}

func isFresh(timestampHeader string, toleranceSeconds int64) bool {
	ts, err := strconv.ParseInt(timestampHeader, 10, 64)
	if err != nil {
		return false
	}

	now := time.Now().Unix()
	diff := now - ts
	if diff < 0 {
		diff = -diff
	}

	return diff <= toleranceSeconds
}

func verifySignature(rawBody []byte, secret string, received string) bool {
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write(rawBody)
	expected := hex.EncodeToString(mac.Sum(nil))

	if len(expected) != len(received) {
		return false
	}

	return subtle.ConstantTimeCompare([]byte(expected), []byte(received)) == 1
}

func webhookHandler(secret string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost {
			http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
			return
		}

		signature := r.Header.Get("X-Webhook-Signature")
		timestamp := r.Header.Get("X-Webhook-Timestamp")

		if signature == "" || timestamp == "" {
			http.Error(w, "missing required security headers", http.StatusBadRequest)
			return
		}

		if !isFresh(timestamp, 300) {
			http.Error(w, "stale webhook", http.StatusUnauthorized)
			return
		}

		rawBody, err := io.ReadAll(r.Body)
		if err != nil {
			http.Error(w, "failed to read body", http.StatusBadRequest)
			return
		}

		if !verifySignature(rawBody, secret, signature) {
			http.Error(w, "invalid signature", http.StatusUnauthorized)
			return
		}

		var payload WebhookPayload
		if err := json.Unmarshal(rawBody, &payload); err != nil {
			http.Error(w, "invalid json", http.StatusBadRequest)
			return
		}

		w.WriteHeader(http.StatusOK)
		w.Write([]byte("OK"))

		log.Printf("accepted event=%s id=%s", payload.Event, payload.ID)
	}
}

func main() {
	secret := os.Getenv("WEBHOOK_SECRET")
	if secret == "" {
		log.Fatal("WEBHOOK_SECRET is not set")
	}

	http.HandleFunc("/webhooks/example", webhookHandler(secret))

	log.Println("listening on :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Why Go feels solid here

A few benefits stand out:

  • The handler is explicit. No hidden middleware magic.
  • Typing helps at the edges. Header parsing, timestamp conversion, and JSON decoding all fail clearly.
  • The standard crypto packages are enough. No extra dependency for basic HMAC verification.

Operational notes

If webhook volume grows, Go’s concurrency model gives you room to fan out background work without changing your HTTP entry point. Even then, keep the receiver narrow. Accept, validate, acknowledge, then hand off.

The strongest Go webhook handlers I’ve seen stay boring. They don’t mix transport verification with business logic, and they don’t do database-heavy work before the response goes back.

Essential Debugging Techniques

A webhook bug usually shows up as a support message, not a stack trace. The provider says they delivered the event. Your endpoint says nothing reached the app, or signature verification failed on a request that looks valid at first glance. At that point, debugging is about reconstructing the exact HTTP exchange, byte for byte, and proving where it broke.

A list of five essential tools and techniques for debugging webhooks in a software development environment.

A practical debugging toolkit

Start with the wire format.

If a signature check fails, capture the raw request body exactly as received, along with the headers used for verification. In practice, the bug is often boring. A framework parsed JSON before hashing, a proxy changed encoding, or a test replay missed the original timestamp header. Logging the parsed object is not enough. You need the original bytes and the verification inputs.

These tools help isolate the problem fast:

  • Raw request capture. Log headers, content type, content length, and the unmodified body during investigation.
  • Request inspection endpoints. Services like webhook.site help confirm what the sender transmitted.
  • Local tunneling. ngrok and similar tools let you test against a local receiver while keeping the provider in the loop.
  • Manual replay. Rebuild the request with curl or Postman using the same body and headers. That is the quickest way to confirm whether your code or the provider payload is the issue.
  • Provider delivery logs. The sender dashboard often includes response codes, retry history, and request identifiers you can match against your logs.

The pattern matters. Work from the outside in. First verify the provider sent what you expected. Then verify your server received the same bytes. Then verify your code hashed the same bytes with the same secret and timestamp rules.

Logging that actually helps

Good webhook logs should answer three questions in one search:

QuestionUseful log field
Did the request arrive?route, method, received_at
Why was it rejected?missing_header, stale_timestamp, signature_failed
Can I correlate it later?event_id, provider_request_id

A fourth field helps in real systems. Add a local request_id generated by your receiver so you can follow the request through your app, queue, and worker logs.

Be selective about what you store. Never log secrets. Avoid dumping full production payloads if they include customer data, access tokens, or billing details. A safer pattern is to log metadata plus a short body hash. That still lets you compare retries and verify whether two deliveries were identical.

Reproduce failures with the original inputs

This is the part basic tutorials skip. If you cannot replay the failing request exactly, you are guessing.

Save a failing webhook as:

  • raw body bytes
  • all signature-related headers
  • request timestamp
  • content type
  • provider request ID

Then replay it against a staging endpoint. If the replay passes, compare what changed in transit. Common offenders include middleware that normalizes request bodies, character encoding mismatches, and load balancers that strip or rewrite headers. I have also seen failures caused by teams copying payloads from pretty-printed dashboard views instead of the actual request body. The whitespace difference alone was enough to break HMAC verification.

For broader release and mobile transport troubleshooting, the same debugging discipline shows up in Capgo’s guide to tools for debugging OTA updates in Capacitor. Different transport, same lesson. Capture the actual request path before changing application code.

If signature verification fails, inspect the raw bytes, the exact headers used in verification, and the timestamp value before touching the cryptography code.

A Checklist for Production-Ready Webhooks

A webhook handler usually looks fine in staging right up until the first retry storm, malformed payload, or signature mismatch at 2 a.m. The production bar is higher. The receiver has to reject forged requests, accept legitimate retries, and give operators enough signal to debug failures without exposing sensitive data.

Security and correctness checks

  • Verify every request signature. Endpoint URLs leak. Test URLs get shared in chat. Signature verification is the control that tells you the sender knew the shared secret.
  • Reject old requests. A valid signature on an old payload can still be replayed. Enforce a timestamp tolerance that matches the provider’s retry model.
  • Hash the raw body, not the parsed JSON. Middleware can reorder keys, normalize whitespace, or change encoding. Verification has to run against the exact bytes that arrived.
  • Keep signing secrets out of code. Environment variables are a baseline. A secrets manager is a better fit if you rotate credentials regularly or run across multiple environments.
  • Fail closed on auth errors. If the signature header is missing, malformed, or uses an unexpected scheme, reject the request and log the reason.

Reliability checks

  • Acknowledge fast. Providers usually treat any 2xx as success, so validate the request, persist what you need, and move slow work to a queue or worker.
  • Make handlers idempotent. The same event may arrive more than once. Key side effects off an event ID, delivery ID, or another stable provider identifier.
  • Return predictable error codes. Use 400 for malformed input, 401 or 403 for failed verification, and 5xx only when your system is the problem. This makes provider retry behavior easier to reason about.
  • Set limits before parsing. Cap request size, content type, and header count early. This prevents a webhook endpoint from turning into a generic ingestion hole.
  • Keep the contract narrow. Accept only the fields and event types you support. Loose parsing feels convenient at first and becomes expensive during provider API changes.

Observability checks

Good webhook operations look boring. Teams can answer three questions quickly: Did we receive it? Did we verify it? Did downstream processing succeed?

Use that standard:

  • Track receipt, verification, and processing as separate outcomes.
  • Log request IDs, event IDs, signature status, and timestamp skew.
  • Measure queue delay, handler latency, and retry volume.
  • Keep a safe replay path for staging or redelivery workflows.
  • Alert on pattern changes, such as a spike in signature failures or duplicate deliveries.

Capgo is a useful example of the broader operational point. It includes tooling around release delivery and observability in its update workflow, and parts of its ecosystem also touch webhook-related flows. The lesson is practical. Delivery systems need visibility from receipt through completion.

If a team covers the checks above, the webhook receiver is usually in good shape for production. If any item is missing, that gap tends to show up during an incident, not during the demo.

Frequently Asked Questions About Webhooks

What status code should I return?

Return a 2xx when you’ve accepted the webhook. If validation fails, return a client or auth error that matches the failure, such as 400 for malformed input or 401 for invalid authentication data. Keep that logic consistent so provider dashboards are easier to interpret.

Should I process the webhook synchronously?

Usually no. Validate it, acknowledge it, then push the actual work to a queue or background worker. That keeps the delivery path fast and reduces duplicate retries caused by slow downstream processing.

How should I handle retries?

Assume they’ll happen. Build idempotency into your handler so receiving the same event again doesn’t duplicate side effects. Event IDs or provider delivery IDs are the usual anchors for that.

What if events arrive out of order?

Design handlers to be order-tolerant when you can. If the business process requires sequence, persist enough state to detect stale transitions instead of assuming delivery order reflects event order.

How do I deal with webhook version changes?

Version your handler logic deliberately. Keep provider-specific parsing isolated, avoid scattering payload assumptions through your codebase, and add tests with real captured samples before rolling out support for a new format.


If your team ships Capacitor or Electron apps, Capgo is worth knowing about for a related reason. It gives teams a controlled way to deliver signed web updates, observe rollout behavior, and recover from incidents without waiting on app store review, which fits the same engineering instinct behind solid webhook design: validate inputs, keep release paths observable, and make recovery fast.

Live updates for Capacitor apps

When a web-layer bug is live, ship the fix through Capgo instead of waiting days for app store approval. Users get the update in the background while native changes stay in the normal review path.

Get Started Now

Latest from our Blog

Capgo gives you the best insights you need to create a truly professional mobile app.