A webhook endpoint is a public URL that accepts instructions about money. Anyone can find it, and anyone can POST to it. The signature is the only thing separating a real event from an invented one, so it is worth spending ten minutes getting the verification exactly right.
Ureh signs every delivery with HMAC-SHA256 over the raw request body, using the signing secret issued with your webhook subscription. The signature travels in a header alongside the timestamp it was computed with.
Verify against the raw bytes
This is the failure we see most often, and it is the most frustrating to debug because everything looks correct. Frameworks routinely parse a JSON body, hand you a dictionary, and discard the original bytes. If you re-serialize that dictionary to recompute the signature, you will produce different bytes than we signed. Key order changes. Unicode escaping changes. Whitespace changes.
Capture the raw body before anything parses it, and verify against those exact bytes. In PHP that is file_get_contents('php://input'), read once and cached, which is precisely why our own inbound webhook handling keeps a raw copy separate from the parsed one.
Compare in constant time
Comparing two signature strings with == leaks information. String comparison returns as soon as it finds a difference, so a signature sharing the first six characters takes measurably longer to reject than one differing at the first. Over enough requests, that timing difference is enough to reconstruct a valid signature byte by byte.
Use your language's constant-time comparison: hash_equals in PHP, crypto.timingSafeEqual in Node, hmac.compare_digest in Python. This is a one word change with a real consequence.
Check the timestamp, then reject old deliveries
A valid signature stays valid forever. If an attacker captures one legitimate delivery, they can replay it indefinitely unless you bound how old a delivery may be.
The timestamp is part of the signed payload, so it cannot be altered without breaking the signature. Verify the signature first, then reject anything older than a tolerance window. Five minutes is a reasonable default. It is comfortably wider than any legitimate network delay and comfortably narrower than a useful replay window.
Then respond quickly and do the work later
Once a delivery is verified, acknowledge it. Do not process the order, call three internal services and send an email before returning a status code. Our retry schedule treats a slow endpoint the same as a broken one, so a handler that takes twelve seconds will start receiving duplicate deliveries.
Write the event to your own queue, return 200, and process asynchronously. Handlers should be idempotent as well: deliveries can arrive more than once by design, and the correct response to seeing an event you have already handled is to acknowledge it again and do nothing.