Verify Webhook Signatures Securely: Common Pitfalls
Securely Verify SendPromptly Webhook Signatures (Common Pitfalls)
SendPromptly webhooks should only be trusted after verification. This guide shows how to verify webhook signature hmac sha256 in laravel middleware using raw request bytes, timestamp checks, and constant-time comparison.
You will implement a Laravel middleware, wire it to your webhook route, and test both valid and invalid requests with copy/paste commands.
If your endpoint is returning intermittent 401 invalid_signature responses, this page focuses on the exact byte-level mismatches that cause those failures.
Signature contract you must verify
Required headers (X-SP-Timestamp, X-SP-Signature)
Every webhook request must include:
X-SP-TimestampX-SP-Signature
Reject requests with missing headers before business logic. See Webhook delivery rules & signature headers.
Signed content format: {timestamp}.{raw_body}
Build the signed content exactly as:
| |
Computing HMAC-SHA256 (hex)
| |
Constant-time compare with hash_equals
For a php hash_equals webhook signature comparison example, use hash_equals($expected, $signature) exactly as shown above.
| |
After wiring this route, Send a test event and confirm delivery runs.
Minimal test snippet (expected behavior):
A) Generate a valid signature (bash + php one-liner)
| |
Expected response: HTTP/1.1 200 OK with {"ok":true}.
B) Confirm failure on modified body
Change one character in BODY and re-run with the old SIG.
Expected response: 401 with {"error":"invalid_signature"}.
Try it now: Open your Sample Project, copy the sample token, send one event, then verify your endpoint receives a signed webhook.
Timestamp tolerance (replay protection)
5-minute window recommendation
Use a strict replay window. A 5-minute tolerance (300 seconds) is a practical default for webhook receivers.
What to do if your servers drift (NTP)
Keep receiver clocks synced with NTP. Clock drift causes valid signatures to fail timestamp checks.
| Timestamp skew | Outcome | Action |
|---|---|---|
<= 300s | Request can pass signature verification | Process normally |
> 300s | Reject with timestamp_out_of_range | Fix time sync immediately |
| Frequent near-limit skew | Intermittent verification failures | Audit NTP on all nodes |
Response strategy (stop retries, keep processing async)
Return 2xx fast, queue real work
Verify signature, validate timestamp, then return 2xx quickly and queue downstream work. This avoids retry storms and keeps throughput stable. Pair this with How idempotency works on ingestion (24-hour TTL).
When to intentionally return non-2xx
Return non-2xx only when verification fails (missing headers, invalid signature, stale timestamp) or when your system cannot safely enqueue work yet.
Mini incident: A team processed webhooks inline and timed out on a slow dependency. Retries multiplied load and duplicated work. Moving to fast
2xx+ queue processing stabilized delivery.
Troubleshooting signature mismatches
A step-by-step “diff” checklist
Use this flow to debug webhook signature mismatch wrong secret vs wrong payload.
Common failure modes checklist (run top to bottom):
- Confirm
X-SP-TimestampandX-SP-Signatureare present. - Rebuild the exact string to sign:
{timestamp}.{raw_body}with no extra newline. - Verify raw bytes come from
$request->getContent()(not decoded/re-encoded JSON). - Confirm digest format is hex (not base64).
- Check timestamp skew is within
300seconds. - Verify the active environment secret is correct and fully rotated.
- Ensure comparison uses
hash_equals, never==.
Logging safely (don’t log secrets)
Log only safe diagnostics: timestamp, skew, signature length, request ID, and verification result. Do not log webhook secrets, full signatures, or sensitive payload fields. Use API + delivery error codes reference when mapping failures to response behavior.
Key takeaways
- Verify both required headers before processing.
- Sign and verify the exact
"{timestamp}.{raw_body}"byte sequence. - Use
hash_hmac('sha256', ...)andhash_equals(...)for secure comparison. - Enforce a 5-minute timestamp window and keep NTP healthy.
- Return
2xxquickly after verification, then process asynchronously.
Debug faster: Go to Message Log -> open the delivery run -> confirm your endpoint is returning 2xx and signature validation is passing.