Webhooks are simple enough that people ship them carelessly, and then the support burden arrives. The difference between a good and a bad implementation is almost entirely in the operational details.
Publish a catalog of event types, each with a documented schema — and describe them in a
machine-readable contract. OpenAPI 3.1 has a webhooks object for exactly this, and there are
providers using it properly; Adyen is a good example of what that looks like in practice. AsyncAPI is
the other route.
Sign every payload so the receiver can verify it came from you, and document the verification procedure with a working code sample. An unsigned webhook endpoint is an unauthenticated write path into your consumer’s system.
Send useful delivery headers: an event type, a unique delivery ID, a signature, and a timestamp. GitHub’s headers are the pattern worth copying. The delivery ID is what makes idempotency possible on the receiving end, and consumers will get duplicates.
Be explicit about what you expect back — a 2xx quickly, with the real work done asynchronously — and about what you will do when you do not get it: how many retries, over what window, with what backoff, and when you disable the endpoint. Give consumers a delivery log they can inspect and a way to replay. And ship a ping event so a developer can prove the plumbing works before the first real event fires at three in the morning.