Webhook payload validation without breaking delivery
Payload validation protects downstream code from malformed or unexpected data, but webhook schemas often vary by event type and evolve over time. Validate the envelope first, then apply event-specific rules.
Validation order
- Enforce request size and content-type limits.
- Preserve the raw body for signature verification.
- Authenticate the request before trusting payload fields.
- Parse the body once.
- Validate the common envelope and event identifier.
- Select an event-specific schema by type and version.
- Handle unknown event types without crashing the endpoint.
Strict where it matters
- Require identifiers and fields needed to make the operation safe.
- Treat documented optional fields as optional.
- Allow unknown fields when providers can add fields without a breaking version change.
- Validate enumerations defensively and log unsupported values.
- Do not log entire sensitive payloads when a field-level error is enough.
Example with Zod
typescript
import { z } from 'zod';
const envelope = z.object({
id: z.string().min(1),
type: z.string().min(1),
created: z.number().optional(),
data: z.unknown(),
}).passthrough();
const result = envelope.safeParse(payload);
if (!result.success) {
return reply.status(400).send({ error: 'invalid_event_envelope' });
}Diagnose validation failures
- Compare the captured payload with the provider event type and API version.
- Identify whether parsing, envelope validation, or event-specific validation failed.
- Check for null, omitted, renamed, or newly added fields.
- Confirm that test fixtures match real provider deliveries.
- Update schemas and contract tests, then send a new event.
Status and retry behavior
Return a non-2xx response only when a retry can help or the request must be rejected. If an authenticated event type is intentionally ignored, acknowledge it and record the decision rather than causing repeated delivery.