Docs Webhook Server

Webhook Server

A webhook endpoint accepts requests from systems you do not control.

This changes the threat model.

Webhook requests may be:

  • Retries of the same event
  • Delivered out of order
  • Sent with invalid payloads
  • Spoofed or replayed
  • Temporarily bursty
  • Sent long after the original event occurred

A correct webhook server must treat every request as hostile by default.

Plumego provides the structure to do this explicitly.


Core Design Principles

A reliable webhook server must implement:

  1. Authentication / verification β€” is the sender legitimate?
  2. Integrity validation β€” was the payload tampered with?
  3. Idempotency β€” can the same event be processed multiple times safely?
  4. Explicit failure semantics β€” should the sender retry?
  5. Isolation from domain logic β€” transport concerns stay at the edge

None of these are optional in production systems.


Webhook Lifecycle (High Level)

A typical Plumego webhook request flows like this:

Incoming Webhook Request
β†’ Trace ID Middleware
β†’ Logging Middleware
β†’ Panic Recovery
β†’ Webhook Verification Middleware
β†’ Handler
β†’ Usecase
β†’ Domain

Verification happens before any business logic is executed.


Step 1: Define the Webhook Contract

Before writing code, define the contract:

  • Endpoint path (e.g. /webhooks/provider-x)
  • HTTP method(s)
  • Signature scheme
  • Required headers
  • Payload format
  • Retry behavior on non-2xx responses

This contract is part of your system’s external API and must be treated as stable.


Step 2: Signature Verification Middleware

Signature verification must be centralized.

Conceptual example:

func WebhookSignatureMiddleware(secret []byte) plumego.Middleware {
	return func(ctx *plumego.Context, next plumego.NextFunc) {
		signature := ctx.Request().Header.Get("X-Signature")
		if signature == "" {
			ctx.JSON(http.StatusUnauthorized, errorResponse("missing signature"))
			return
		}

		body := ctx.RawBody()
		if !verifySignature(body, signature, secret) {
			ctx.JSON(http.StatusUnauthorized, errorResponse("invalid signature"))
			return
		}

		next()
	}
}

Key properties:

  • Verification happens before handler execution
  • Raw body is used (no mutation before verification)
  • Failure stops request processing immediately

Never verify signatures inside handlers.


Step 3: Parsing the Payload Explicitly

Webhook payloads should be parsed only after verification.

Example (handler-level):

var event ProviderEvent
if err := ctx.BindJSON(&event); err != nil {
	ctx.JSON(http.StatusBadRequest, errorResponse("invalid payload"))
	return
}

Do not assume payload correctness.

Validation errors must result in explicit failure responses.


Step 4: Idempotency Handling

Most webhook providers retry aggressively.

Your system must be idempotent.

Common idempotency strategies

  • Event ID de-duplication
  • Idempotency keys
  • Payload hash storage

Example (conceptual usecase):

if store.Exists(event.ID) {
	return nil // already processed
}

processEvent(event)
store.MarkProcessed(event.ID)

Idempotency belongs in the application layer, not middleware.


Step 5: Failure Semantics and Retries

Webhook providers interpret responses differently.

General guidance:

  • 2xx β†’ event accepted, no retry
  • 4xx β†’ client error, often no retry
  • 5xx β†’ server error, retry expected

Be intentional:

  • Validation failures β†’ 400
  • Signature failures β†’ 401 / 403
  • Temporary failures β†’ 500

Accidental 2xx responses can cause data loss.


Step 6: Keep Handlers Thin

Webhook handlers should:

  • Validate payload structure
  • Invoke a usecase
  • Return an appropriate response

They should not:

  • Perform complex business logic
  • Handle retries manually
  • Encode provider-specific logic deep in the system

Translate at the boundary.


Webhook Security Considerations

Additional safeguards often include:

  • IP allowlists (when feasible)
  • Timestamp validation to prevent replay
  • Rate limiting
  • Separate webhook secrets per provider

Security layers should be additive, not exclusive.


Handling Large Payloads

Some providers send large payloads.

Mitigations include:

  • Request size limits
  • Streaming parsing (if necessary)
  • Early rejection before allocation

Do not blindly read unbounded request bodies.


Observability for Webhooks

Webhook endpoints require strong observability:

  • Always log Trace ID
  • Log provider event ID
  • Log verification failures
  • Track retry patterns

This is essential for diagnosing integration issues.


Testing Webhook Handling

You should test:

  • Valid signature + valid payload
  • Invalid signature
  • Replayed events
  • Out-of-order delivery
  • Malformed payloads

Webhook bugs often appear only under retry conditions.


Common Mistakes

Verifying After Parsing

Parsing mutates the body stream.

Always verify first.


Treating Webhooks Like Normal APIs

Webhook senders do not behave like clients.

Retry behavior must be assumed.


Encoding Provider Logic in Domain Code

Provider-specific quirks must stay at the boundary.


Summary

In Plumego, a correct webhook server is:

  • Explicitly verified
  • Idempotent by design
  • Clear in failure semantics
  • Isolated from core logic
  • Observable under failure

Webhook handling is integration work β€”
and integration work must be defensive.


Next

With inbound integrations handled safely, the next guide is:

β†’ WebSocket

This explains how to manage long-lived, stateful connections explicitly in Plumego.